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/138] 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 6f1cfd6e4d..7fc6c9af30 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 b385507833..392f325257 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 0000000000..12015a5cdf --- /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 0000000000..63c1ca40b9 --- /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 0000000000..bafcdffbdb --- /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 0000000000..766fb754cf --- /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 0000000000..d83849d90a --- /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 72b4f2f15e..e807a3e58c 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 0000000000..d1cccc109f --- /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/138] 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 d1cccc109f..ed4e1738d6 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/138] 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 63c1ca40b9..9f78ffc603 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/138] 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 9f78ffc603..03e23f7b6a 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 ed4e1738d6..cbe0946dc3 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/138] 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 7fc6c9af30..73e9b1343c 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 12015a5cdf..28080f02ee 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 03e23f7b6a..f71f67d24c 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 bafcdffbdb..3f8fb7a6ac 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 0000000000..beaee97af5 --- /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 766fb754cf..840f1bd28c 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 cbe0946dc3..f1f8cc5fe5 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/138] 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 f71f67d24c..ee45be2cbc 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 3f8fb7a6ac..00a1c0f988 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 f1f8cc5fe5..872c2010e5 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/138] 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 ee45be2cbc..53766e133a 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 00a1c0f988..9d56851e59 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/138] 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 53766e133a..9d76a121ca 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/138] 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 9d76a121ca..dd3c96585d 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 9d56851e59..8745e4276b 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 840f1bd28c..d0347a903e 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 872c2010e5..1ee7828f91 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/138] 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 dd3c96585d..cd51f23992 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 8745e4276b..d50eb280af 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 1ee7828f91..fc67d73bc4 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/138] 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 cd51f23992..5932d1a3ab 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 fc67d73bc4..1cb1239828 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/138] 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 5932d1a3ab..fd747be38f 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 1cb1239828..1b98a5d477 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/138] 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 f861290229..072f779b07 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/138] 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 c53b7cdb19..7afbf33c08 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 dd64ee33e5..dbe9943407 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/138] 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 0000000000..93d71cbd2c --- /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 0000000000..badb77bd56 --- /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 5bd11e632e..4664b3164f 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 1be024d556..f770a74156 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 f75629d334..93fa533021 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 f7504c9b8d..c0d924c21d 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 5d89bb7977..f814c0ae58 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 69565f307f..8cfdc6eb5c 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 9ea56eb5e9..3c72a469aa 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 0a314c0c79..de5f76798b 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 3be6bfd6db..054c189c16 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 1a9fdaf5ff..c7aed7ef33 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 355665d174..a0dc0b72df 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 7f06a01d1e..16edf997d5 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 7e8c4809a7..f2803616ed 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 28d1c974d9..b0c0cbfee8 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 88f85e030b..cda45affa2 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 ec15644fac..23dbfc931b 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 421bba148c..6956b562ae 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 88fa62a254..099be843a4 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 946d170691..3a7b8b8f6a 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 ee0761fde6..817dd671ee 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 01ced6caa8..c9720fa6d9 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 06dd436767..48bfbf6b22 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 527cedbb78..2602944bd0 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/138] 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 93d71cbd2c..0000000000 --- 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 e81a0f5042..e59e35e5c9 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 f770a74156..927d075fc0 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 c7aed7ef33..2f04e83b51 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 16edf997d5..90ac9ae081 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/138] 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 e7cb7905fd..e987247e0b 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 3ef3dbf7bf..8dae61feba 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 d5eda8823c..6841a39fea 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 73cd4ff670..2493a86d9b 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 4bd15b2401..fa5cab7754 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 ee7ce61994..3921a73853 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/138] 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 0000000000..1f7417ba9f --- /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/138] 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 a557f826d7..3c2b656659 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/138] 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 1f7417ba9f..29b36582e8 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/138] 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 fd747be38f..aea3cb1315 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 d50eb280af..dcf6c30f4f 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 1b98a5d477..19b2aebcb1 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/138] 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 aea3cb1315..967827b3c3 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/138] 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 dcf6c30f4f..8f165e5890 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/138] 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 8f165e5890..43cc054c24 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/138] 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 43cc054c24..00af40b73e 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/138] 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 00af40b73e..941840d41d 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/138] 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 941840d41d..be24081ed7 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/138] 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 967827b3c3..0adcc191f1 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/138] 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 0adcc191f1..7d44c5d825 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/138] 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 7d44c5d825..5fff603c77 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/138] 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 5fff603c77..0f331eec90 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/138] 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 0f331eec90..9dad833937 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/138] 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 9dad833937..a601e4cec9 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/138] 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 a601e4cec9..b59603df61 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/138] 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 b59603df61..b9708f9136 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/138] 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 b9708f9136..a325043749 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/138] 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 be24081ed7..5f68e0d49c 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 19b2aebcb1..8143993f63 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/138] 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 28080f02ee..cdded122fd 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 a325043749..f377cd1893 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 5f68e0d49c..43485492a3 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 8143993f63..368713c566 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/138] 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 a325043749..5b5ccd4cee 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 8143993f63..794e1631b4 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/138] 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 28080f02ee..aa67ebd7d4 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 5b5ccd4cee..3c01f5d79a 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 d0347a903e..d9c135df02 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 794e1631b4..c9fab91da6 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 45114d99a6..9a8b6b2ba8 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/138] 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 f12fdc0d4b..fc0dcc5919 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 aa67ebd7d4..fd36fcb291 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 d483cd177d..96fa279ccf 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 4042308362..e2129a2bf6 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 3c01f5d79a..ead72e7c82 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 c9fab91da6..25c60834ad 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/138] 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 ead72e7c82..643120a87c 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 5f68e0d49c..36dcd539e5 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/138] 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 0947637f5f..bc02063fb9 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 36dcd539e5..643e8d0ad8 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/138] 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 fd36fcb291..7901a43c21 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 e2129a2bf6..c712a7b813 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 643120a87c..3adcfca5f4 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 25c60834ad..ef764bc6f7 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 9a8b6b2ba8..f87e518d27 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/138] 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 bc02063fb9..0947637f5f 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/138] 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 3adcfca5f4..332937df51 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 643e8d0ad8..899e36f47a 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/138] 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 332937df51..95fa505e81 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 899e36f47a..1ac74deaf8 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 d9c135df02..377ce324cf 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 ef764bc6f7..4c2d255b52 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/138] 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 95fa505e81..9b2e56539d 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 1ac74deaf8..82e8a6a738 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/138] 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 82e8a6a738..585a17ffda 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 4c2d255b52..fccac74806 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/138] 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 73e9b1343c..6f1cfd6e4d 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/138] 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 3c2b656659..6fbcb32d0a 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 29b36582e8..a0c794be19 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/138] 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 0000000000..54dc1e634d --- /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/138] 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 0000000000..bcbd8ad0c3 --- /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/138] 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 585a17ffda..95181065d3 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 fccac74806..0000000000 --- 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 54dc1e634d..792b84a4bb 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 bcbd8ad0c3..6f8ec3efe8 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/138] 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 792b84a4bb..2278e6959f 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/138] 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 6f8ec3efe8..658ed1621d 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/138] 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 6fbcb32d0a..ac45dc9c07 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 a3dc06f483..8f6e425792 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/138] 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 5a21bbe3ae..0a4ac5b7af 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/138] 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 ac45dc9c07..71c740a24a 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/138] 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 71c740a24a..c25c28d914 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/138] 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 0a4ac5b7af..ece65849db 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/138] 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 a0c794be19..dceb3b871f 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 12a7f02987..5a5070a1b7 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/138] 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 dceb3b871f..ba24b14aeb 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 f3ac234fe2..727ed3a1c5 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 0000000000..e0b8832571 --- /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 dfe015db18..037147c341 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/138] 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 c25c28d914..e585b2bdf7 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 ba24b14aeb..2e74b64d21 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 5a5070a1b7..1b28621454 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 73cc58c49b..bf96523c4f 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 774b0c186c..b6ed5eb195 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 727ed3a1c5..0605470cb9 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 e0b8832571..c50398b314 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 d4a81bb9f2..820b57fac3 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 037147c341..dfe015db18 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 b560747811..26af5217c8 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 734381a886..791b99883d 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/138] 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 e585b2bdf7..03f7197a3c 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 2e74b64d21..d892336ffd 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 1b28621454..49884b55af 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 bf96523c4f..73cc58c49b 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 0605470cb9..5ef52a46e2 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 c50398b314..f1177dd970 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 820b57fac3..d4a81bb9f2 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/138] 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 2278e6959f..3599c317d8 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/138] 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 3599c317d8..b9280962b2 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/138] 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 9b2e56539d..5c846b8a6c 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 95181065d3..a3b78906a6 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/138] 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 b0ece6216d..58b7bb9b05 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/138] 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 f2803616ed..0e558437be 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 db9d4952e6..cac996383e 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 a2c8d6ffca..f689dbd63b 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 0000000000..aff1ab77c1 --- /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 78fa718e08..e488181071 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 cda45affa2..79cc90b1b5 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 23dbfc931b..737314bf33 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 6956b562ae..f4fe002fa9 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/138] 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 5c846b8a6c..0c97c48d08 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 377ce324cf..33a9211ddb 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/138] 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 aff1ab77c1..e1530772dc 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 426e391073..1bb98c03fa 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/138] 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 1bb98c03fa..9b2356b171 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:50:53 +0200 Subject: [PATCH 074/138] 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 5c846b8a6c..0c97c48d08 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 377ce324cf..33a9211ddb 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 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 075/138] 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 b9280962b2..70f87829d0 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 658ed1621d..c654f87ad3 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 076/138] 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 fc0dcc5919..dc8b444cb4 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 70f87829d0..a328150637 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 0000000000..70f87829d0 --- /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 077/138] 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 a328150637..2f0fcd4565 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 70f87829d0..2f0fcd4565 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 078/138] 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 2f0fcd4565..a87727971e 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 079/138] 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 a87727971e..eaa9281410 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 2f0fcd4565..b75959c96a 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 080/138] 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 392f325257..bf0e40e13c 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 7901a43c21..839b7110b4 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 0c97c48d08..28aaf019a7 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 a3b78906a6..343b4d7b6e 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 33a9211ddb..dc2dbe0461 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 d83849d90a..aed8e0166f 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 e807a3e58c..9a6987fa8d 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 eaa9281410..0000000000 --- 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 b75959c96a..0000000000 --- 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 c654f87ad3..0000000000 --- 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 0000000000..4d9e319410 --- /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 03daf16761..2b8e87a561 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 1f2fc2d026..e3b3cb4ea1 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 f87e518d27..ab8075efeb 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 081/138] 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 dc8b444cb4..fc0dcc5919 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 bf0e40e13c..392f325257 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 839b7110b4..7901a43c21 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 343b4d7b6e..5516ac23f9 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 aed8e0166f..d83849d90a 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 082/138] 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 4d9e319410..e2784ce07a 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 083/138] 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 7901a43c21..27c15d0b99 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 e2784ce07a..c5cacecf3f 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 084/138] 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 c5cacecf3f..30a5ee9fb0 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 085/138] 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 9b2356b171..3c06e52064 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 086/138] 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 0000000000..83f45c9605 --- /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 30a5ee9fb0..821715760e 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 a0496013ac..0d46da27ae 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 68bdf3e5f65d2c3fd9dcedd149476e44ae5249e7 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 8 Sep 2022 09:19:01 +0200 Subject: [PATCH 087/138] delete payment-processor batch --- .../src/payment/batch-conversion-proxy.ts | 409 ------------------ 1 file changed, 409 deletions(-) delete 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 deleted file mode 100644 index 83f45c9605..0000000000 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ /dev/null @@ -1,409 +0,0 @@ -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; -} From 8bb84350c943308713176f4820b6c76227d998a4 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 9 Sep 2022 09:13:16 +0200 Subject: [PATCH 088/138] clean payment processor --- .../src/payment/any-to-erc20-proxy.ts | 79 +- .../src/payment/batch-proxy.ts | 7 +- .../payment-processor/src/payment/index.ts | 15 - .../payment-processor/src/payment/utils.ts | 1 + .../payment/any-to-erc20-batch-proxy.test.ts | 719 ------------------ .../payment-processor/test/payment/shared.ts | 9 +- .../test/payment/swap-erc20-fee-proxy.test.ts | 8 - 7 files changed, 16 insertions(+), 822 deletions(-) delete mode 100644 packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts 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 03f7197a3c..a557f826d7 100644 --- a/packages/payment-processor/src/payment/any-to-erc20-proxy.ts +++ b/packages/payment-processor/src/payment/any-to-erc20-proxy.ts @@ -1,10 +1,6 @@ import { constants, ContractTransaction, Signer, providers, BigNumberish, BigNumber } from 'ethers'; -import { - CurrencyDefinition, - CurrencyManager, - UnsupportedCurrencyError, -} from '@requestnetwork/currency'; +import { 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'; @@ -24,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 Conversion fee proxy contract. + * The payment is made by the ERC20 fee proxy contract. * @param request the request to pay * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings payment settings @@ -51,8 +47,7 @@ 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. + * 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. * @param request request to pay * @param signerOrProvider the Web3 provider, or signer. Defaults to window.ethereum. * @param paymentSettings payment settings @@ -65,37 +60,6 @@ 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, - ]); -} - -/** - * 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[]; requestCurrency: CurrencyDefinition } { if (!paymentSettings.currency) { throw new Error('currency must be provided in the paymentSettings'); } @@ -130,43 +94,24 @@ export function checkRequestAndGetPathAndCurrency( // 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 amountToPay = padAmountForChainlink(getAmountToPay(request, amount), requestCurrency); const feeToPay = padAmountForChainlink(feeAmountOverride || feeAmount || 0, requestCurrency); - return { - path, - paymentReference, + + const proxyContract = Erc20ConversionProxy__factory.createInterface(); + return proxyContract.encodeFunctionData('transferFromWithReferenceAndFee', [ 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.ts b/packages/payment-processor/src/payment/batch-proxy.ts index 850261d863..8872c8eea1 100644 --- a/packages/payment-processor/src/payment/batch-proxy.ts +++ b/packages/payment-processor/src/payment/batch-proxy.ts @@ -158,10 +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 */ -export function getBatchArgs( - requests: ClientTypes.IRequestData[], - forcedPaymentType?: 'ETH' | 'ERC20', -): { +function getBatchArgs(requests: ClientTypes.IRequestData[]): { tokenAddresses: Array; paymentAddresses: Array; amountsToPay: Array; @@ -176,7 +173,7 @@ export function getBatchArgs( const feesToPay: Array = []; let feeAddressUsed = constants.AddressZero; - const paymentType = forcedPaymentType ?? requests[0].currencyInfo.type; + const paymentType = requests[0].currencyInfo.type; for (let i = 0; i < requests.length; i++) { if (paymentType === 'ETH') { validateEthFeeProxyRequest(requests[i]); diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index b6ed5eb195..774b0c186c 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -336,18 +336,3 @@ 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 7d870a5ff2..cd570bbafc 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -273,6 +273,7 @@ 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 deleted file mode 100644 index f1177dd970..0000000000 --- a/packages/payment-processor/test/payment/any-to-erc20-batch-proxy.test.ts +++ /dev/null @@ -1,719 +0,0 @@ -import { Wallet, providers, BigNumber } from 'ethers'; - -import { - ClientTypes, - ExtensionTypes, - IdentityTypes, - PaymentTypes, - RequestLogicTypes, -} from '@requestnetwork/types'; -import { getErc20Balance } from '../../src/payment/erc20'; -import Utils from '@requestnetwork/utils'; -import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; -import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; -import { currencyManager } from './shared'; -import { - approveErc20BatchConversionIfNeeded, - getBatchConversionProxyAddress, - payBatchConversionProxyRequest, - 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(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 -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 = { - 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: EURExpectedAmount, - 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: EURFeeAmount, - 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', -}; - -const expectedAmount = 100000; -const feeAmount = 100; -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: expectedAmount, - 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: feeAmount, - 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, -}; - -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); -}; - -/** - * 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`, () => { - 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'; - }); - - 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( - [ - { - paymentNetworkId: 0, - request: request1, - 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 () => { - 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; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError(`wrong request currencyInfo type`); - }); - 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 symbol '' 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'); - }); - }); - - 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'); - }); - - 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); - wallet.sendTransaction = spy; - await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, { - gasPrice: '20000000000', - }); - expect(spy).toHaveBeenCalledWith({ - 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')); - }); - }); - }); -}; - -testERC20Batch('Same tokens', validRequest, validRequest); -testERC20Batch('Different tokens', validRequest, fauValidRequest); diff --git a/packages/payment-processor/test/payment/shared.ts b/packages/payment-processor/test/payment/shared.ts index 791b99883d..734381a886 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; -}; 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 d41dfcae9c..4d67d8d63c 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 @@ -79,14 +79,6 @@ 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', () => { beforeAll(async () => { // revoke erc20SwapToPay approval From aa18befc10d75b22351214bfe0c40b8cd51e63f1 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 12:03:08 +0200 Subject: [PATCH 089/138] add logs --- packages/smart-contracts/hardhat.config.ts | 2 +- .../contract-setup/adminTasks.ts | 23 ++++++++------- .../setupBatchConversionPayments.ts | 19 ++++++++----- .../BatchConversionPayments/index.ts | 28 +++++++++++++++++++ 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index fc0dcc5919..a7bba3fe79 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -164,7 +164,7 @@ export default { signer: process.env.ADMIN_PRIVATE_KEY, networks: process.env.NETWORK ? [process.env.NETWORK] - : ['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], + : [['matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'][3]], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index f689dbd63b..1918a174f7 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -5,14 +5,15 @@ 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) + * Batch Fees: .3% */ export const BATCH_FEE_DEPRECATED = 3; -export const BATCH_FEE = 30; -// Batch Fees: .3% +// Batch conversion and no conversion fees: .3% +export const BATCH_FEE = 30; export const BATCH_CONVERSION_FEE = 30; export const updateChainlinkConversionPath = async ( @@ -58,9 +59,8 @@ 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 + * Updates batch or batchConversion batchFee depends of the proxy selected + * @param batchConversionProxy batchConversionProxy impacts the batch fees used */ export const updateBatchPaymentFees = async ( contract: any, @@ -144,24 +144,27 @@ export const updateBatchConversionPaymentProxy = async ( let currentAddress: string; if (proxyName === 'eth') { proxyAddress = artifacts.ethereumFeeProxyArtifact.getAddress(network); - batchSetProxy = contract.setPaymentEthProxy; + batchSetProxy = await contract.setPaymentEthProxy; currentAddress = await contract.paymentEthProxy(); } else if (proxyName === 'ethConversion') { proxyAddress = artifacts.ethConversionArtifact.getAddress(network); - batchSetProxy = contract.setPaymentEthConversionProxy; + batchSetProxy = await contract.setPaymentEthConversionProxy; currentAddress = await contract.paymentEthConversionProxy(); } else if (proxyName === 'erc20') { proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); - batchSetProxy = contract.setPaymentErc20Proxy; + batchSetProxy = await contract.setPaymentErc20Proxy; currentAddress = await contract.paymentErc20Proxy(); } else { // "erc20Conversion" proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); - batchSetProxy = contract.setPaymentErc20ConversionProxy; + batchSetProxy = await contract.setPaymentErc20ConversionProxy; currentAddress = await contract.paymentErc20ConversionProxy(); } if (currentAddress !== proxyAddress) { + console.log( + `${proxyName}: previous address ${currentAddress} has been replaced by: ${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 index e1530772dc..0e26795ee4 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -23,6 +23,7 @@ export const setupBatchConversionPayments = async ( ); await Promise.all( hre.config.xdeploy.networks.map(async (network) => { + console.log(`Setup BatchConversionPayments on ${network}`); let provider; if (network === 'celo') { provider = utils.getCeloProvider(); @@ -31,41 +32,45 @@ export const setupBatchConversionPayments = async ( } const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); const signer = wallet.connect(provider); - const batchConversionPaymentConnected = await batchConversionPaymentContract.connect(signer); + const batchConversionPaymentConnected = 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( + await updateBatchPaymentFees( + batchConversionPaymentConnected, + adminNonce, + gasPrice.mul(gasCoef), + ), + await updateBatchConversionPaymentFees( batchConversionPaymentConnected, adminNonce + 1, gasPrice.mul(gasCoef), ), - updateBatchConversionPaymentProxy( + await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, adminNonce + 2, gasPrice.mul(gasCoef), 'erc20', ), - updateBatchConversionPaymentProxy( + await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, adminNonce + 3, gasPrice.mul(gasCoef), 'eth', ), - updateBatchConversionPaymentProxy( + await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, adminNonce + 4, gasPrice.mul(gasCoef), 'erc20Conversion', ), - updateBatchConversionPaymentProxy( + await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, adminNonce + 5, diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 3c06e52064..5a7b11ef6c 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -25,6 +25,34 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Thu, 15 Sep 2022 15:39:17 +0200 Subject: [PATCH 090/138] add batch conversion proxy address --- packages/smart-contracts/hardhat.config.ts | 2 +- .../contract-setup/adminTasks.ts | 8 ++++--- .../smart-contracts/scripts-create2/utils.ts | 15 +++++++------ .../BatchConversionPayments/index.ts | 21 ++++++++++++------- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index a7bba3fe79..fc0dcc5919 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -164,7 +164,7 @@ export default { signer: process.env.ADMIN_PRIVATE_KEY, networks: process.env.NETWORK ? [process.env.NETWORK] - : [['matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'][3]], + : ['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 1918a174f7..7072b729bb 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -72,7 +72,9 @@ export const updateBatchPaymentFees = async ( const currentFees = await contract.batchFee(); if (currentFees !== batchFee) { // Log is useful to have a direct view on was is being updated - console.log(`BatchFees, currentFees: ${currentFees.toString()}, new fees: ${batchFee}`); + console.log( + `BatchFees: the current fees: ${currentFees.toString()}, have been replaced by: ${batchFee}`, + ); await contract.setBatchFee(batchFee, { nonce: nonce, gasPrice: gasPrice }); } }; @@ -86,7 +88,7 @@ export const updateBatchConversionPaymentFees = async ( 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}`, + `BatchConversionFees: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_CONVERSION_FEE}`, ); await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { nonce: nonce, @@ -163,7 +165,7 @@ export const updateBatchConversionPaymentProxy = async ( if (currentAddress !== proxyAddress) { console.log( - `${proxyName}: previous address ${currentAddress} has been replaced by: ${proxyAddress}`, + `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, ); await batchSetProxy(proxyAddress, { nonce: nonce, diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index f4fe002fa9..71e5cd01d4 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -7,14 +7,13 @@ 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', 'BatchConversionPayments', ]; diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 5a7b11ef6c..784e4eca25 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -21,6 +21,10 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Thu, 15 Sep 2022 16:15:33 +0200 Subject: [PATCH 091/138] remove batch payment deprecated - should not be deployed anymore --- .../contract-setup/adminTasks.ts | 50 +++---------------- .../setupBatchConversionPayments.ts | 10 ++-- .../contract-setup/setupBatchPayments.ts | 47 ----------------- .../scripts-create2/contract-setup/setups.ts | 5 -- .../smart-contracts/scripts-create2/deploy.ts | 8 --- .../smart-contracts/scripts-create2/verify.ts | 7 --- 6 files changed, 11 insertions(+), 116 deletions(-) delete mode 100644 packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 7072b729bb..5998c92f52 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -59,41 +59,22 @@ export const updateRequestSwapFees = async ( }; /** - * Updates batch or batchConversion batchFee depends of the proxy selected - * @param batchConversionProxy batchConversionProxy impacts the batch fees used + * Updates batch fees with/out conversion */ export const updateBatchPaymentFees = async ( contract: any, nonce: number, gasPrice: BigNumber, - batchConversionProxy = true, + feesName: 'BatchFee' | 'BatchConversionFee', ): Promise => { - const batchFee = batchConversionProxy ? BATCH_FEE : BATCH_FEE_DEPRECATED; + const feesApplied = feesName === 'BatchFee' ? BATCH_FEE : BATCH_CONVERSION_FEE; const currentFees = await contract.batchFee(); - if (currentFees !== batchFee) { + if (currentFees !== feesApplied) { // Log is useful to have a direct view on was is being updated console.log( - `BatchFees: the current fees: ${currentFees.toString()}, have been replaced by: ${batchFee}`, + `${feesName}: the current fees: ${currentFees.toString()}, have been replaced by: ${feesApplied}`, ); - 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: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_CONVERSION_FEE}`, - ); - await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { - nonce: nonce, - gasPrice: gasPrice, - }); + await contract.setBatchFee(feesApplied, { nonce: nonce, gasPrice: gasPrice }); } }; @@ -114,23 +95,6 @@ export const updatePaymentErc20FeeProxy = async ( } }; -export const updatePaymentEthFeeProxy = async ( - contract: any, - network: string, - nonce: number, - gasPrice: BigNumber, -): Promise => { - const ethereumFeeProxy = artifacts.ethereumFeeProxyArtifact; - const ethereumFeeProxyAddress = ethereumFeeProxy.getAddress(network); - const currentAddress = await contract.paymentEthFeeProxy(); - if (currentAddress !== ethereumFeeProxyAddress) { - await contract.setPaymentEthFeeProxy(ethereumFeeProxyAddress, { - nonce: nonce, - gasPrice: gasPrice, - }); - } -}; - /** * Update the address of a payment proxy used by batch conversion contract */ @@ -157,7 +121,7 @@ export const updateBatchConversionPaymentProxy = async ( batchSetProxy = await contract.setPaymentErc20Proxy; currentAddress = await contract.paymentErc20Proxy(); } else { - // "erc20Conversion" + // proxyName === "erc20Conversion" proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); batchSetProxy = await contract.setPaymentErc20ConversionProxy; currentAddress = await contract.paymentErc20ConversionProxy(); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index 0e26795ee4..136aa70b70 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -1,11 +1,7 @@ import { batchConversionPaymentsArtifact } from '../../src/lib'; import { HardhatRuntimeEnvironmentExtended } from '../types'; import utils from '@requestnetwork/utils'; -import { - updateBatchPaymentFees, - updateBatchConversionPaymentFees, - updateBatchConversionPaymentProxy, -} from './adminTasks'; +import { updateBatchPaymentFees, updateBatchConversionPaymentProxy } from './adminTasks'; /** * Updates the values of the batch fees of the BatchConversionPayments contract, if needed @@ -43,11 +39,13 @@ export const setupBatchConversionPayments = async ( batchConversionPaymentConnected, adminNonce, gasPrice.mul(gasCoef), + 'BatchFee', ), - await updateBatchConversionPaymentFees( + await updateBatchPaymentFees( batchConversionPaymentConnected, adminNonce + 1, gasPrice.mul(gasCoef), + 'BatchConversionFee', ), await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts deleted file mode 100644 index e488181071..0000000000 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchPayments.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { batchPaymentsArtifact } from '../../src/lib'; -import { HardhatRuntimeEnvironmentExtended } from '../types'; -import utils from '@requestnetwork/utils'; -import { - updateBatchPaymentFees, - updatePaymentErc20FeeProxy, - updatePaymentEthFeeProxy, -} from './adminTasks'; - -/** - * 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 - */ -export 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), false), - updatePaymentErc20FeeProxy(batchPaymentConnected, network, adminNonce + 1, gasPrice.mul(2)), - updatePaymentEthFeeProxy(batchPaymentConnected, network, adminNonce + 2, gasPrice.mul(2)), - ]); - }), - ); - console.log('Setup for setupBatchPayment successfull'); -}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index 79cc90b1b5..5f0247aa57 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -1,6 +1,5 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; import { setupETHConversionProxy } from './setupETHConversionProxy'; -import { setupBatchPayments } from './setupBatchPayments'; import { setupBatchConversionPayments } from './setupBatchConversionPayments'; import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; @@ -24,10 +23,6 @@ export const setupContract = async ( await setupERC20SwapToConversion(contractAddress, hre); break; } - case 'BatchPayments': { - await setupBatchPayments(contractAddress, hre); - break; - } case 'BatchConversionPayments': { await setupBatchConversionPayments(contractAddress, hre); break; diff --git a/packages/smart-contracts/scripts-create2/deploy.ts b/packages/smart-contracts/scripts-create2/deploy.ts index 737314bf33..edb4fdfc42 100644 --- a/packages/smart-contracts/scripts-create2/deploy.ts +++ b/packages/smart-contracts/scripts-create2/deploy.ts @@ -4,7 +4,6 @@ import { HardhatRuntimeEnvironmentExtended } from './types'; 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 @@ -73,13 +72,6 @@ export const deployWithCreate2FromList = async ( await deployOneWithCreate2({ contract, constructorArgs }, hre); break; } - case 'BatchPayments': { - const network = hre.config.xdeploy.networks[0]; - const constructorArgs = getConstructorArgs(contract, network); - const address = await deployOneWithCreate2({ contract, constructorArgs }, hre); - await setupBatchPayments(address, hre); - break; - } case 'BatchConversionPayments': { const network = hre.config.xdeploy.networks[0]; const constructorArgs = getConstructorArgs(contract, network); diff --git a/packages/smart-contracts/scripts-create2/verify.ts b/packages/smart-contracts/scripts-create2/verify.ts index f27c97d3e4..f1b2034f18 100644 --- a/packages/smart-contracts/scripts-create2/verify.ts +++ b/packages/smart-contracts/scripts-create2/verify.ts @@ -43,13 +43,6 @@ export async function VerifyCreate2FromList(hre: HardhatRuntimeEnvironmentExtend await verifyOne(address, { contract, constructorArgs }, hre); break; } - case 'BatchPayments': { - const network = hre.config.xdeploy.networks[0]; - const constructorArgs = getConstructorArgs(contract, network); - address = await computeCreate2DeploymentAddress({ contract, constructorArgs }, hre); - await verifyOne(address, { contract, constructorArgs }, hre); - break; - } case 'BatchConversionPayments': { const network = hre.config.xdeploy.networks[0]; const constructorArgs = getConstructorArgs(contract, network); From d92341c568c989d38f107a6f95436ef30b33eb11 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 17:50:28 +0200 Subject: [PATCH 092/138] add comments to batch fees --- .../contract-setup/adminTasks.ts | 17 +++++++---------- .../setupBatchConversionPayments.ts | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 5998c92f52..43b3651d22 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -6,15 +6,9 @@ import { BigNumber } from 'ethers'; // Fees: 0.5% export const REQUEST_SWAP_FEES = 5; -/** - * BATCH_FEE_DEPRECATED is only used with batchProxy (NOT with batchConversionProxy) - * Batch Fees: .3% - */ -export const BATCH_FEE_DEPRECATED = 3; - // Batch conversion and no conversion fees: .3% -export const BATCH_FEE = 30; -export const BATCH_CONVERSION_FEE = 30; +const BATCH_NO_CONVERSION_FEE = 30; +const BATCH_CONVERSION_FEE = 30; export const updateChainlinkConversionPath = async ( contract: any, @@ -60,14 +54,17 @@ export const updateRequestSwapFees = async ( /** * Updates batch fees with/out conversion + * BATCH_NO_CONVERSION_FEE e.g: payment DAI - DAI + * BATCH_CONVERSION_FEE e.g: payment EUR - DAI */ export const updateBatchPaymentFees = async ( contract: any, nonce: number, gasPrice: BigNumber, - feesName: 'BatchFee' | 'BatchConversionFee', + feesName: 'BatchNoConversionFee' | 'BatchConversionFee', ): Promise => { - const feesApplied = feesName === 'BatchFee' ? BATCH_FEE : BATCH_CONVERSION_FEE; + const feesApplied = + feesName === 'BatchNoConversionFee' ? BATCH_NO_CONVERSION_FEE : BATCH_CONVERSION_FEE; const currentFees = await contract.batchFee(); if (currentFees !== feesApplied) { // Log is useful to have a direct view on was is being updated diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index 136aa70b70..b3aad4f908 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -39,7 +39,7 @@ export const setupBatchConversionPayments = async ( batchConversionPaymentConnected, adminNonce, gasPrice.mul(gasCoef), - 'BatchFee', + 'BatchNoConversionFee', ), await updateBatchPaymentFees( batchConversionPaymentConnected, From 9738e7dcbf81277b5bc2d3e5bad7453aa4f30a16 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 15 Sep 2022 18:15:43 +0200 Subject: [PATCH 093/138] erase xdeployer batchpayments legacy --- .../scripts-create2/compute-one-address.ts | 1 - .../scripts-create2/constructor-args.ts | 12 ------------ .../scripts-create2/contract-setup/setups.ts | 2 +- packages/smart-contracts/scripts-create2/utils.ts | 2 -- 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index 0e558437be..e3f8ed19fd 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -56,7 +56,6 @@ export const computeCreate2DeploymentAddressesFromList = async ( case 'ERC20FeeProxy': case 'Erc20ConversionProxy': case 'ERC20EscrowToPay': - case 'BatchPayments': case 'BatchConversionPayments': case 'ERC20SwapToConversion': { try { diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index cac996383e..d48d331127 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -37,18 +37,6 @@ export const getConstructorArgs = (contract: string, network?: string): string[] const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); return [erc20FeeProxyAddress, getAdminWalletAddress(contract)]; } - case 'BatchPayments': { - if (!network) { - throw new Error( - 'Batch contract requires network parameter to get correct address of erc20FeeProxy and ethereumFeeProxy', - ); - } - return [ - '0x0000000000000000000000000000000000000000', - '0x0000000000000000000000000000000000000000', - getAdminWalletAddress(contract), - ]; - } case 'BatchConversionPayments': { if (!network) { throw new Error( diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts index 5f0247aa57..4e7749772e 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setups.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setups.ts @@ -4,7 +4,7 @@ import { setupBatchConversionPayments } from './setupBatchConversionPayments'; import { setupERC20SwapToConversion } from './setupERC20SwapToConversion'; /** - * Updates the values of either BatchPayments, ETHConversionProxy, or ERC20SwapToConversion contract, if needed + * Updates the values of either BatchConversionPayments, ETHConversionProxy, or ERC20SwapToConversion contract, if needed * @param contractAddress address of the proxy * @param hre Hardhat runtime environment * @param contractName name of the contract diff --git a/packages/smart-contracts/scripts-create2/utils.ts b/packages/smart-contracts/scripts-create2/utils.ts index 71e5cd01d4..9f4e5a24cc 100644 --- a/packages/smart-contracts/scripts-create2/utils.ts +++ b/packages/smart-contracts/scripts-create2/utils.ts @@ -47,8 +47,6 @@ export const getArtifact = (contract: string): artifacts.ContractArtifact Date: Thu, 22 Sep 2022 10:55:57 +0200 Subject: [PATCH 094/138] proxy addresses updated --- .../contract-setup/adminTasks.ts | 97 ++++++++++--------- .../setupBatchConversionPayments.ts | 26 ++--- .../BatchConversionPayments/index.ts | 19 ++-- 3 files changed, 69 insertions(+), 73 deletions(-) diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 51e27e432a..6e8d4a8b0c 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -52,33 +52,36 @@ export const updateRequestSwapFees = async ( } }; -/** - * Updates batch fees with/out conversion - * BATCH_NO_CONVERSION_FEE e.g: payment DAI - DAI - * BATCH_CONVERSION_FEE e.g: payment EUR - DAI - */ -export const updateBatchPaymentFees = async ( +export const updateBatchPaymentFees = async (contract: any, gasPrice: BigNumber): Promise => { + const currentFees = await contract.batchFee(); + if (currentFees.toNumber() !== BATCH_NO_CONVERSION_FEE) { + // Log is useful to have a direct view on was is being updated + console.log( + `Batch: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_NO_CONVERSION_FEE}`, + ); + await contract.setBatchFee(BATCH_NO_CONVERSION_FEE, { gasPrice: gasPrice }); + } +}; + +export const updateBatchConversionPaymentFees = async ( contract: any, - nonce: number, gasPrice: BigNumber, - feesName: 'BatchNoConversionFee' | 'BatchConversionFee', ): Promise => { - const feesApplied = - feesName === 'BatchNoConversionFee' ? BATCH_NO_CONVERSION_FEE : BATCH_CONVERSION_FEE; - const currentFees = await contract.batchFee(); - if (currentFees !== feesApplied) { + const currentFees = await contract.batchConversionFee(); + if (currentFees.toNumber() !== BATCH_CONVERSION_FEE) { // Log is useful to have a direct view on was is being updated console.log( - `${feesName}: the current fees: ${currentFees.toString()}, have been replaced by: ${feesApplied}`, + `$Batch conversion: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_CONVERSION_FEE}`, ); - await contract.setBatchFee(feesApplied, { nonce: nonce, gasPrice: gasPrice }); + await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { + gasPrice: gasPrice, + }); } }; export const updatePaymentErc20FeeProxy = async ( contract: any, network: string, - nonce: number, gasPrice: BigNumber, ): Promise => { const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; @@ -86,7 +89,6 @@ export const updatePaymentErc20FeeProxy = async ( const currentAddress = await contract.paymentErc20FeeProxy(); if (currentAddress !== erc20FeeProxyAddress) { await contract.setPaymentErc20FeeProxy(erc20FeeProxyAddress, { - nonce: nonce, gasPrice: gasPrice, }); } @@ -98,39 +100,42 @@ export const updatePaymentErc20FeeProxy = async ( 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 = await contract.setPaymentEthProxy; - currentAddress = await contract.paymentEthProxy(); - } else if (proxyName === 'ethConversion') { - proxyAddress = artifacts.ethConversionArtifact.getAddress(network); - batchSetProxy = await contract.setPaymentEthConversionProxy; - currentAddress = await contract.paymentEthConversionProxy(); - } else if (proxyName === 'erc20') { - proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); - batchSetProxy = await contract.setPaymentErc20Proxy; - currentAddress = await contract.paymentErc20Proxy(); - } else { - // proxyName === "erc20Conversion" - proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); - batchSetProxy = await contract.setPaymentErc20ConversionProxy; - currentAddress = await contract.paymentErc20ConversionProxy(); - } + try { + let proxyAddress: string; + let batchSetProxy: any; + let currentAddress: string; + if (proxyName === 'eth') { + proxyAddress = artifacts.ethereumFeeProxyArtifact.getAddress(network); + batchSetProxy = await contract.setPaymentEthProxy; + currentAddress = await contract.paymentEthProxy(); + } else if (proxyName === 'ethConversion') { + proxyAddress = artifacts.ethConversionArtifact.getAddress(network); + batchSetProxy = await contract.setPaymentEthConversionProxy; + currentAddress = await contract.paymentEthConversionProxy(); + } else if (proxyName === 'erc20') { + proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); + batchSetProxy = await contract.setPaymentErc20Proxy; + currentAddress = await contract.paymentErc20Proxy(); + } else { + // proxyName === "erc20Conversion" + proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); + batchSetProxy = await contract.setPaymentErc20ConversionProxy; + currentAddress = await contract.paymentErc20ConversionProxy(); + } - if (currentAddress !== proxyAddress) { - console.log( - `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, - ); - await batchSetProxy(proxyAddress, { - nonce: nonce, - gasPrice: gasPrice, - }); + if (currentAddress !== proxyAddress) { + console.log( + `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, + ); + await batchSetProxy(proxyAddress, { + gasPrice: gasPrice, + }); + } + } catch (e) { + console.log(`Cannot update ${proxyName} proxy, it might not exist on this network`); + console.log(e); } }; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index b3aad4f908..7fbe1356fb 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -1,7 +1,11 @@ import { batchConversionPaymentsArtifact } from '../../src/lib'; import { HardhatRuntimeEnvironmentExtended } from '../types'; import utils from '@requestnetwork/utils'; -import { updateBatchPaymentFees, updateBatchConversionPaymentProxy } from './adminTasks'; +import { + updateBatchPaymentFees, + updateBatchConversionPaymentProxy, + updateBatchConversionPaymentFees, +} from './adminTasks'; /** * Updates the values of the batch fees of the BatchConversionPayments contract, if needed @@ -29,53 +33,39 @@ export const setupBatchConversionPayments = async ( const wallet = new hre.ethers.Wallet(hre.config.xdeploy.signer, provider); const signer = wallet.connect(provider); const batchConversionPaymentConnected = 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([ - await updateBatchPaymentFees( + await updateBatchPaymentFees(batchConversionPaymentConnected, gasPrice.mul(gasCoef)), + await updateBatchConversionPaymentFees( batchConversionPaymentConnected, - adminNonce, gasPrice.mul(gasCoef), - 'BatchNoConversionFee', - ), - await updateBatchPaymentFees( - batchConversionPaymentConnected, - adminNonce + 1, - gasPrice.mul(gasCoef), - 'BatchConversionFee', ), await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, - adminNonce + 2, gasPrice.mul(gasCoef), 'erc20', ), await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, - adminNonce + 3, gasPrice.mul(gasCoef), 'eth', ), await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, - adminNonce + 4, gasPrice.mul(gasCoef), 'erc20Conversion', ), await updateBatchConversionPaymentProxy( batchConversionPaymentConnected, network, - adminNonce + 5, gasPrice.mul(gasCoef), 'ethConversion', - ), - ]); + ); }), ); console.log('Setup for setupBatchConversionPayment successfull'); diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 784e4eca25..838656e4d3 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -49,15 +49,16 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Fri, 7 Oct 2022 15:47:05 +0200 Subject: [PATCH 095/138] add fee batch amount limit to batchMultiERC20ConversionPayments --- ...test-deploy-batch-conversion-deployment.ts | 4 + .../src/contracts/BatchConversionPayments.sol | 92 ++++++++++++++++-- .../BatchConversionPayments/0.1.0.json | 93 +++++++++++++++++++ .../contracts/BatchConversionPayments.test.ts | 84 ++++++++++++++--- 4 files changed, 252 insertions(+), 21 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 dc23d6b9a6..b057bb65a2 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -28,6 +28,7 @@ export async function deployBatchConversionPayment( const _EthereumFeeProxyAddress = ethereumFeeProxyArtifact.getAddress('private'); const _paymentErc20ConversionFeeProxy = erc20ConversionProxy.getAddress('private'); const _paymentEthConversionFeeProxy = ethConversionArtifact.getAddress('private'); + const _chainlinkConversionPath = '0x4e71920b7330515faf5EA0c690f1aD06a85fB60c'; // Deploy BatchConversionPayments contract const { address: BatchConversionPaymentsAddress } = await deployOne( @@ -40,6 +41,7 @@ export async function deployBatchConversionPayment( _EthereumFeeProxyAddress, _paymentErc20ConversionFeeProxy, _paymentEthConversionFeeProxy, + _chainlinkConversionPath, await (await hre.ethers.getSigners())[0].getAddress(), ], }, @@ -75,6 +77,8 @@ export async function deployBatchConversionPayment( const batchConversion = batchConversionPaymentsArtifact.connect(hre.network.name, owner); await batchConversion.connect(owner).setBatchFee(30); await batchConversion.connect(owner).setBatchConversionFee(30); + await batchConversion.connect(owner).setUSDAddress(currencyManager.fromSymbol('USD')!.hash); + await batchConversion.connect(owner).setBatchFeeAmountUSDLimit(300); // * 1_000_000_000_000_000_000); // ---------------------------------- console.log('Contracts deployed'); diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 28aaf019a7..a869f975cb 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.4; import './interfaces/IERC20ConversionProxy.sol'; import './interfaces/IEthConversionProxy.sol'; +import './ChainlinkConversionPath.sol'; import './BatchNoConversionPayments.sol'; /** @@ -24,9 +25,13 @@ contract BatchConversionPayments is BatchNoConversionPayments { IERC20ConversionProxy public paymentErc20ConversionProxy; IEthConversionProxy public paymentEthConversionProxy; + ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchConversionFee; + uint256 public batchFeeAmountUSDLimit; // maybe not necessary + address public USDAddress; + /** * @dev All the information of a request, except the feeAddress * _recipient Recipient address of the payment @@ -57,6 +62,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { uint256[] amounts; bytes[] paymentReferences; uint256[] feeAmounts; + //address[][] pathsToUSD; // TODO is there another solution ? } /** @@ -76,6 +82,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 address of the conversion path contract * @param _owner Owner of the contract. */ constructor( @@ -83,10 +90,12 @@ contract BatchConversionPayments is BatchNoConversionPayments { address _paymentEthProxy, address _paymentErc20ConversionProxy, address _paymentEthConversionFeeProxy, + address _chainlinkConversionPathAddress, address _owner ) BatchNoConversionPayments(_paymentErc20Proxy, _paymentEthProxy, _owner) { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); batchConversionFee = 0; } @@ -99,16 +108,29 @@ contract BatchConversionPayments is BatchNoConversionPayments { * - 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 pathsToUSD The list of paths into USD for every token, used to limit the batch fees, caution, + Caution, the calculation of batchFeeAmountUSD which allows to limit the batch fees takes only + into consideration these paths. Without paths, there is not limitation. * @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 { + function batchRouter( + MetaDetail[] calldata metaDetails, + address[][] calldata pathsToUSD, + address _feeAddress + ) external payable { require(metaDetails.length < 6, 'more than 5 metaDetails'); + uint256 batchFeeAmountUSD = 0; for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; if (metaConversionDetail.paymentNetworkId == 0) { - batchMultiERC20ConversionPayments(metaConversionDetail.conversionDetails, _feeAddress); + batchFeeAmountUSD = batchMultiERC20ConversionPayments( + metaConversionDetail.conversionDetails, + batchFeeAmountUSD, + pathsToUSD, + _feeAddress + ); } else if (metaConversionDetail.paymentNetworkId == 1) { batchERC20Payments( metaConversionDetail.cryptoDetails.tokenAddresses[0], @@ -154,12 +176,15 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 pathsToUSD The list of paths into USD for every token * @param _feeAddress The fee recipient */ function batchMultiERC20ConversionPayments( ConversionDetail[] calldata conversionDetails, + uint256 batchFeeAmountUSD, + address[][] calldata pathsToUSD, address _feeAddress - ) public { + ) public returns (uint256) { // a list of unique tokens, with the sum of maxToSpend by token Token[] memory uTokens = new Token[](conversionDetails.length); for (uint256 i = 0; i < conversionDetails.length; i++) { @@ -237,16 +262,23 @@ contract BatchConversionPayments is BatchNoConversionPayments { requestedToken.safeTransfer(msg.sender, excessAmount); } + uint256 batchFeeToPay = ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / + tenThousand; + + (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeToPay, + uTokens[k].tokenAddress, + batchFeeAmountUSD, + pathsToUSD + ); + // Payer pays the exact batch fees amount require( - safeTransferFrom( - uTokens[k].tokenAddress, - _feeAddress, - ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / tenThousand - ), + safeTransferFrom(uTokens[k].tokenAddress, _feeAddress, batchFeeToPay), 'batch fee transferFrom() failed' ); } + return batchFeeAmountUSD; } /** @@ -295,6 +327,34 @@ contract BatchConversionPayments is BatchNoConversionPayments { payerAuthorized = false; } + function calculateBatchFeeToPay( + uint256 batchFeeToPay, + address tokenAddress, + uint256 batchFeeAmountUSD, + address[][] calldata pathsToUSD + ) internal view returns (uint256, uint256) { + if (pathsToUSD.length > 0) { + for (uint256 i = 0; i < pathsToUSD.length; i++) { + if ( + pathsToUSD[i][0] == tokenAddress && pathsToUSD[i][pathsToUSD[i].length - 1] == USDAddress + ) { + uint256 conversionUSD = 0; + (conversionUSD, ) = chainlinkConversionPath.getConversion(batchFeeToPay, pathsToUSD[i]); + // calculate the batch fee to pay, taking care of the batchFeeAmountUSDLimit + uint256 conversionToPayUSD = conversionUSD; + if (batchFeeAmountUSD + conversionToPayUSD > batchFeeAmountUSDLimit) { + conversionToPayUSD = batchFeeAmountUSDLimit - batchFeeAmountUSD; + batchFeeToPay = (batchFeeToPay * conversionToPayUSD) / conversionUSD; + } + batchFeeAmountUSD += conversionToPayUSD; + // add only once the fees + break; + } + } + } + return (batchFeeToPay, batchFeeAmountUSD); + } + /* * Admin functions to edit the conversion proxies address and fees */ @@ -322,4 +382,20 @@ contract BatchConversionPayments is BatchNoConversionPayments { 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); + } + + function setUSDAddress(address _USDAddress) external onlyOwner { + USDAddress = _USDAddress; + } + + function setBatchFeeAmountUSDLimit(uint256 _batchFeeAmountUSDLimit) external onlyOwner { + batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; + } } 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 dc2dbe0461..4854ef2de5 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,6 +22,11 @@ "name": "_paymentEthConversionFeeProxy", "type": "address" }, + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + }, { "internalType": "address", "name": "_owner", @@ -50,6 +55,19 @@ "name": "OwnershipTransferred", "type": "event" }, + { + "inputs": [], + "name": "USDAddress", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "batchConversionFee", @@ -202,6 +220,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "batchFeeAmountUSDLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -246,6 +277,11 @@ "name": "conversionDetails", "type": "tuple[]" }, + { + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" + }, { "internalType": "address", "name": "_feeAddress", @@ -383,6 +419,11 @@ "name": "metaDetails", "type": "tuple[]" }, + { + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" + }, { "internalType": "address", "name": "_feeAddress", @@ -394,6 +435,19 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [], + "name": "chainlinkConversionPath", + "outputs": [ + { + "internalType": "contract ChainlinkConversionPath", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "owner", @@ -492,6 +546,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_batchFeeAmountUSDLimit", + "type": "uint256" + } + ], + "name": "setBatchFeeAmountUSDLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + } + ], + "name": "setConversionPathAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -544,6 +624,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_USDAddress", + "type": "address" + } + ], + "name": "setUSDAddress", + "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 1bb70ab17e..53f82c3792 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -128,6 +128,7 @@ describe('contract: BatchConversionPayments', async () => { ethFeeProxy.address, erc20ConversionProxy.address, ethConversionProxy.address, + chainlinkPath.address, await adminSigner.getAddress(), ); @@ -138,6 +139,15 @@ describe('contract: BatchConversionPayments', async () => { // set batch proxy fees and connect fromSigner await batchConversionProxy.setBatchFee(BATCH_FEE); await batchConversionProxy.setBatchConversionFee(BATCH_CONV_FEE); + await batchConversionProxy.setUSDAddress(currencyManager.fromSymbol('USD')!.hash); + + // set a batchFeeAmountUSDLimit equal to 1001$ + 2*1001€ + await batchConversionProxy.setBatchFeeAmountUSDLimit( + BigNumber.from(1001 * 100 + 2 * 12 * (10000 + 12)) + .mul(1e8) + .div(100), + ); + batchConversionProxy = batchConversionProxy.connect(fromSigner); // set ERC20 tokens and transfer token to "from" (fromSigner) @@ -335,6 +345,7 @@ describe('contract: BatchConversionPayments', async () => { }, }, ], + [[FAU_address, USD_hash]], feeAddress, ); @@ -362,6 +373,10 @@ describe('contract: BatchConversionPayments', async () => { cryptoDetails: emptyCryptoDetails, }, ], + [ + [FAU_address, USD_hash], + [DAI_address, USD_hash], + ], feeAddress, ); }; @@ -388,6 +403,7 @@ describe('contract: BatchConversionPayments', async () => { }, }, ], + [], feeAddress, { value: 1000 + 1 + 11 }, // + 11 to pay batch fees ); @@ -415,6 +431,7 @@ describe('contract: BatchConversionPayments', async () => { cryptoDetails: emptyCryptoDetails, }, ], + [], feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees @@ -478,6 +495,7 @@ describe('contract: BatchConversionPayments', async () => { cryptoDetails: emptyCryptoDetails, }, ], + [[FAU_address, USD_hash]], feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees ); @@ -551,6 +569,7 @@ describe('contract: BatchConversionPayments', async () => { conversionDetails: [], cryptoDetails: emptyCryptoDetails, }), + [[FAU_address, USD_hash]], feeAddress, ), ).to.be.revertedWith('more than 5 metaDetails'); @@ -565,6 +584,7 @@ describe('contract: BatchConversionPayments', async () => { cryptoDetails: emptyCryptoDetails, }, ], + [[FAU_address, USD_hash]], feeAddress, ), ).to.be.revertedWith('wrong paymentNetworkId'); @@ -577,7 +597,12 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments([fauConvDetail], feeAddress); + .batchMultiERC20ConversionPayments( + [fauConvDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ); const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); @@ -598,7 +623,12 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments([daiConvDetail], feeAddress); + .batchMultiERC20ConversionPayments( + [daiConvDetail], + 0, + [[DAI_address, USD_hash]], + feeAddress, + ); const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); @@ -615,12 +645,15 @@ describe('contract: BatchConversionPayments', async () => { }); it('make 3 payments with different tokens and conversion length', async () => { const batchPayment = async () => { - return await batchConversionProxy - .connect(fromSigner) - .batchMultiERC20ConversionPayments( - [fauConvDetail, daiConvDetail, daiConvDetail], - feeAddress, - ); + return await batchConversionProxy.connect(fromSigner).batchMultiERC20ConversionPayments( + [fauConvDetail, daiConvDetail, daiConvDetail], + 0, + [ + [FAU_address, USD_hash], + [DAI_address, USD_hash], + ], + feeAddress, + ); }; await manyPaymentsBatchConv(batchPayment); }); @@ -630,7 +663,12 @@ describe('contract: BatchConversionPayments', async () => { const convDetail = Utils.deepCopy(fauConvDetail); convDetail.path = [EUR_hash, ETH_hash, DAI_address]; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments( + [convDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ), ).to.be.revertedWith('revert No aggregator found'); }); @@ -638,7 +676,12 @@ describe('contract: BatchConversionPayments', async () => { const convDetail = Utils.deepCopy(fauConvDetail); convDetail.maxToSpend = '1000000'; // not enough await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments( + [convDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ), ).to.be.revertedWith('Amount to pay is over the user limit'); }); @@ -646,7 +689,12 @@ describe('contract: BatchConversionPayments', async () => { const convDetail = Utils.deepCopy(fauConvDetail); convDetail.maxRateTimespan = '10'; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments( + [convDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ), ).to.be.revertedWith('aggregator rate is outdated'); }); @@ -661,7 +709,12 @@ describe('contract: BatchConversionPayments', async () => { }, ); await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convDetail], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments( + [convDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ), ).to.be.revertedWith('Insufficient allowance for batch to pay'); }); @@ -680,7 +733,12 @@ describe('contract: BatchConversionPayments', async () => { await expect( batchConversionProxy .connect(signer4) - .batchMultiERC20ConversionPayments([convDetail, convDetail, convDetail], feeAddress), + .batchMultiERC20ConversionPayments( + [convDetail, convDetail, convDetail], + 0, + [[FAU_address, USD_hash]], + feeAddress, + ), ).to.be.revertedWith('not enough funds, including fees'); // signer4 transfer token to fromSigner From 151014247a85fe9548e8592234a40bf9a0c4ee59 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Sun, 9 Oct 2022 21:37:51 +0200 Subject: [PATCH 096/138] batchFees amount limited for batchNoConversion erc20 functions --- .../src/contracts/BatchConversionPayments.sol | 126 +--- .../contracts/BatchNoConversionPayments.sol | 257 +++++--- .../BatchConversionPayments/0.1.0.json | 94 ++- .../contracts/BatchConversionPayments.test.ts | 81 ++- .../BatchNoConversionErc20Payments.test.ts | 597 ++++++++++-------- .../BatchNoConversionEthPayments.test.ts | 5 + 6 files changed, 653 insertions(+), 507 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index a869f975cb..562b51c07f 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 './BatchNoConversionPayments.sol'; /** @@ -25,46 +24,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { IERC20ConversionProxy public paymentErc20ConversionProxy; IEthConversionProxy public paymentEthConversionProxy; - ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchConversionFee; - uint256 public batchFeeAmountUSDLimit; // maybe not necessary - address public USDAddress; - - /** - * @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 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 acceptable times span for conversion rates, ignored if zero - */ - struct ConversionDetail { - address recipient; - uint256 requestAmount; - address[] path; - bytes paymentReference; - uint256 feeAmount; - uint256 maxToSpend; - uint256 maxRateTimespan; - } - - /** - * @dev BatchNoConversionPayments contract input structure. - */ - struct CryptoDetails { - address[] tokenAddresses; - address[] recipients; - uint256[] amounts; - bytes[] paymentReferences; - uint256[] feeAmounts; - //address[][] pathsToUSD; // TODO is there another solution ? - } - /** * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. @@ -92,10 +54,16 @@ contract BatchConversionPayments is BatchNoConversionPayments { address _paymentEthConversionFeeProxy, address _chainlinkConversionPathAddress, address _owner - ) BatchNoConversionPayments(_paymentErc20Proxy, _paymentEthProxy, _owner) { + ) + BatchNoConversionPayments( + _paymentErc20Proxy, + _paymentEthProxy, + _chainlinkConversionPathAddress, + _owner + ) + { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); batchConversionFee = 0; } @@ -125,7 +93,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; if (metaConversionDetail.paymentNetworkId == 0) { - batchFeeAmountUSD = batchMultiERC20ConversionPayments( + batchFeeAmountUSD += batchMultiERC20ConversionPayments( metaConversionDetail.conversionDetails, batchFeeAmountUSD, pathsToUSD, @@ -133,20 +101,16 @@ contract BatchConversionPayments is BatchNoConversionPayments { ); } else if (metaConversionDetail.paymentNetworkId == 1) { batchERC20Payments( - metaConversionDetail.cryptoDetails.tokenAddresses[0], - metaConversionDetail.cryptoDetails.recipients, - metaConversionDetail.cryptoDetails.amounts, - metaConversionDetail.cryptoDetails.paymentReferences, - metaConversionDetail.cryptoDetails.feeAmounts, + metaConversionDetail.conversionDetails, + pathsToUSD, + batchFeeAmountUSD, _feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 2) { - batchMultiERC20Payments( - metaConversionDetail.cryptoDetails.tokenAddresses, - metaConversionDetail.cryptoDetails.recipients, - metaConversionDetail.cryptoDetails.amounts, - metaConversionDetail.cryptoDetails.paymentReferences, - metaConversionDetail.cryptoDetails.feeAmounts, + batchFeeAmountUSD += batchMultiERC20Payments( + metaConversionDetail.conversionDetails, + pathsToUSD, + batchFeeAmountUSD, _feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 3) { @@ -176,6 +140,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 batchFeeAmountUSD The batchFeeAmountUSD already paid * @param pathsToUSD The list of paths into USD for every token * @param _feeAddress The fee recipient */ @@ -185,26 +150,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { address[][] calldata pathsToUSD, address _feeAddress ) public returns (uint256) { - // a list of unique tokens, with the sum of maxToSpend by token - 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 == 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 && (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 = conversionDetails[i].maxToSpend; - break; - } - } - } + Token[] memory uTokens = getUTokens(conversionDetails); IERC20 requestedToken; // For each token: check allowance, transfer funds on the contract and approve the paymentProxy to spend if needed @@ -327,34 +273,6 @@ contract BatchConversionPayments is BatchNoConversionPayments { payerAuthorized = false; } - function calculateBatchFeeToPay( - uint256 batchFeeToPay, - address tokenAddress, - uint256 batchFeeAmountUSD, - address[][] calldata pathsToUSD - ) internal view returns (uint256, uint256) { - if (pathsToUSD.length > 0) { - for (uint256 i = 0; i < pathsToUSD.length; i++) { - if ( - pathsToUSD[i][0] == tokenAddress && pathsToUSD[i][pathsToUSD[i].length - 1] == USDAddress - ) { - uint256 conversionUSD = 0; - (conversionUSD, ) = chainlinkConversionPath.getConversion(batchFeeToPay, pathsToUSD[i]); - // calculate the batch fee to pay, taking care of the batchFeeAmountUSDLimit - uint256 conversionToPayUSD = conversionUSD; - if (batchFeeAmountUSD + conversionToPayUSD > batchFeeAmountUSDLimit) { - conversionToPayUSD = batchFeeAmountUSDLimit - batchFeeAmountUSD; - batchFeeToPay = (batchFeeToPay * conversionToPayUSD) / conversionUSD; - } - batchFeeAmountUSD += conversionToPayUSD; - // add only once the fees - break; - } - } - } - return (batchFeeToPay, batchFeeAmountUSD); - } - /* * Admin functions to edit the conversion proxies address and fees */ @@ -390,12 +308,4 @@ contract BatchConversionPayments is BatchNoConversionPayments { function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); } - - function setUSDAddress(address _USDAddress) external onlyOwner { - USDAddress = _USDAddress; - } - - function setBatchFeeAmountUSDLimit(uint256 _batchFeeAmountUSDLimit) external onlyOwner { - batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; - } } diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 5516ac23f9..b408ea12b1 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -6,6 +6,7 @@ import './lib/SafeERC20.sol'; import '@openzeppelin/contracts/access/Ownable.sol'; import './interfaces/ERC20FeeProxy.sol'; import './interfaces/EthereumFeeProxy.sol'; +import './ChainlinkConversionPath.sol'; /** * @title BatchNoConversionPayments @@ -25,6 +26,7 @@ contract BatchNoConversionPayments is Ownable { IERC20FeeProxy public paymentErc20Proxy; IEthereumFeeProxy public paymentEthProxy; + ChainlinkConversionPath public chainlinkConversionPath; uint256 public batchFee; /** Used to to calculate batch fees */ @@ -37,24 +39,62 @@ contract BatchNoConversionPayments is Ownable { // and call both batchEthPayments and batchConversionEthPaymentsWithReference bool internal transferBackRemainingEth = true; + uint256 public batchFeeAmountUSDLimit; + address public USDAddress; + struct Token { address tokenAddress; uint256 amountAndFee; uint256 batchFeeAmount; } + /** + * @dev BatchNoConversionPayments contract input structure. + */ + struct CryptoDetails { + address[] tokenAddresses; + address[] recipients; + uint256[] amounts; + bytes[] paymentReferences; + uint256[] feeAmounts; + } + + /** + * @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 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 acceptable times span for conversion rates, ignored if zero + */ + struct ConversionDetail { + address recipient; + uint256 requestAmount; + address[] path; + bytes paymentReference; + uint256 feeAmount; + uint256 maxToSpend; + uint256 maxRateTimespan; + } + /** * @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 _chainlinkConversionPathAddress The address of the conversion path contract * @param _owner Owner of the contract. */ constructor( address _paymentErc20Proxy, address _paymentEthProxy, + address _chainlinkConversionPathAddress, address _owner ) { paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); batchFee = 0; } @@ -124,46 +164,35 @@ contract BatchNoConversionPayments is Ownable { /** * @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 conversionDetails TODO + * @param pathsToUSD The list containing the path of the token into USD + * @param batchFeeAmountUSD The batchFeeAmountUSD already paid * @param _feeAddress The fee recipient. * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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 batchERC20Payments( - address _tokenAddress, - address[] calldata _recipients, - uint256[] calldata _amounts, - bytes[] calldata _paymentReferences, - uint256[] calldata _feeAmounts, + ConversionDetail[] calldata conversionDetails, + address[][] calldata pathsToUSD, + uint256 batchFeeAmountUSD, address _feeAddress - ) public { - require( - _recipients.length == _amounts.length && - _recipients.length == _paymentReferences.length && - _recipients.length == _feeAmounts.length, - 'the input arrays must have the same length' - ); - + ) public returns (uint256) { // 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]; + for (uint256 i = 0; i < conversionDetails.length; i++) { + amount += conversionDetails[i].requestAmount + conversionDetails[i].feeAmount; } // Transfer the amount and fee from the payer to the batch contract - IERC20 requestedToken = IERC20(_tokenAddress); + IERC20 requestedToken = IERC20(conversionDetails[0].path[0]); require( requestedToken.allowance(msg.sender, address(this)) >= amount, 'Insufficient allowance for batch to pay' ); require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); require( - safeTransferFrom(_tokenAddress, address(this), amount), + safeTransferFrom(conversionDetails[0].path[0], address(this), amount), 'payment transferFrom() failed' ); @@ -173,15 +202,16 @@ contract BatchNoConversionPayments is Ownable { } // Batch contract pays the requests using Erc20FeeProxy - for (uint256 i = 0; i < _recipients.length; i++) { + for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory cD = conversionDetails[i]; // amount is updated to become the sum of amounts, to calculate batch fee amount - amount -= _feeAmounts[i]; + amount -= cD.feeAmount; paymentErc20Proxy.transferFromWithReferenceAndFee( - _tokenAddress, - _recipients[i], - _amounts[i], - _paymentReferences[i], - _feeAmounts[i], + cD.path[0], + cD.recipient, + cD.requestAmount, + cD.paymentReference, + cD.feeAmount, _feeAddress ); } @@ -192,61 +222,42 @@ contract BatchNoConversionPayments is Ownable { require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); // Payer pays batch fee amount + uint256 batchFeeToPay = amount; + + (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeToPay, + conversionDetails[0].path[0], + batchFeeAmountUSD, + pathsToUSD + ); require( - safeTransferFrom(_tokenAddress, _feeAddress, amount), + safeTransferFrom(conversionDetails[0].path[0], _feeAddress, amount), 'batch fee transferFrom() failed' ); + + return batchFeeAmountUSD; } /** * @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 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 conversionDetails It contains payments information: + * @param pathsToUSD The list of paths into USD for every token + * @param batchFeeAmountUSD The batchFeeAmountUSD already paid * @param _feeAddress The fee recipient. * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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 batchMultiERC20Payments( - address[] calldata _tokenAddresses, - address[] calldata _recipients, - uint256[] calldata _amounts, - bytes[] calldata _paymentReferences, - uint256[] calldata _feeAmounts, + ConversionDetail[] calldata conversionDetails, + address[][] calldata pathsToUSD, + uint256 batchFeeAmountUSD, 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' - ); - + ) public returns (uint256) { // 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 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 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 (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; - } - } - } + Token[] memory uTokens = getUTokens(conversionDetails); // The payer transfers tokens to the batch contract and pays batch fee for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { @@ -279,23 +290,113 @@ contract BatchNoConversionPayments is Ownable { } // Payer pays batch fee amount + + uint256 batchFeeToPay = uTokens[i].batchFeeAmount; + + (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeToPay, + uTokens[i].tokenAddress, + batchFeeAmountUSD, + pathsToUSD + ); + require( - safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, uTokens[i].batchFeeAmount), + safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, batchFeeToPay), 'batch fee transferFrom() failed' ); } // Batch contract pays the requests using Erc20FeeProxy - for (uint256 i = 0; i < _recipients.length; i++) { + for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory cD = conversionDetails[i]; paymentErc20Proxy.transferFromWithReferenceAndFee( - _tokenAddresses[i], - _recipients[i], - _amounts[i], - _paymentReferences[i], - _feeAmounts[i], + cD.path[0], + cD.recipient, + cD.requestAmount, + cD.paymentReference, + cD.feeAmount, _feeAddress ); } + return batchFeeAmountUSD; + } + + function getUTokens(ConversionDetail[] calldata conversionDetails) + internal + pure + returns (Token[] memory uTokens) + { + // a list of unique tokens, with the sum of maxToSpend by token + 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 == conversionDetails[i].path[conversionDetails[i].path.length - 1] + ) { + if (conversionDetails[i].path.length > 1) { + uTokens[k].amountAndFee += conversionDetails[i].maxToSpend; + } else { + // it is not a conversion payment + uTokens[k].amountAndFee += + conversionDetails[i].requestAmount + + conversionDetails[i].feeAmount; + uTokens[k].batchFeeAmount += conversionDetails[i].requestAmount; + } + break; + } + // If the token is not in the list (amountAndFee = 0) + else if ( + uTokens[k].amountAndFee == 0 && + (conversionDetails[i].maxToSpend > 0 || + conversionDetails[i].requestAmount + conversionDetails[i].feeAmount > 0) + ) { + uTokens[k].tokenAddress = conversionDetails[i].path[conversionDetails[i].path.length - 1]; + + if (conversionDetails[i].path.length > 1) { + // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract + uTokens[k].amountAndFee = conversionDetails[i].maxToSpend; + } else { + // it is not a conversion payment + uTokens[k].amountAndFee = + conversionDetails[i].requestAmount + + conversionDetails[i].feeAmount; + uTokens[k].batchFeeAmount = conversionDetails[i].requestAmount; + } + break; + } + } + } + } + + function calculateBatchFeeToPay( + uint256 batchFeeToPay, + address tokenAddress, + uint256 batchFeeAmountUSD, + address[][] calldata pathsToUSD + ) internal view returns (uint256, uint256) { + if (pathsToUSD.length > 0) { + for (uint256 i = 0; i < pathsToUSD.length; i++) { + if ( + pathsToUSD[i][0] == tokenAddress && pathsToUSD[i][pathsToUSD[i].length - 1] == USDAddress + ) { + (uint256 conversionUSD, ) = chainlinkConversionPath.getConversion( + batchFeeToPay, + pathsToUSD[i] + ); + // calculate the batch fee to pay, taking care of the batchFeeAmountUSDLimit + uint256 conversionToPayUSD = conversionUSD; + if (batchFeeAmountUSD + conversionToPayUSD > batchFeeAmountUSDLimit) { + conversionToPayUSD = batchFeeAmountUSDLimit - batchFeeAmountUSD; + batchFeeToPay = (batchFeeToPay * conversionToPayUSD) / conversionUSD; + } + batchFeeAmountUSD += conversionToPayUSD; + // add only once the fees + break; + } + } + } + return (batchFeeToPay, batchFeeAmountUSD); } /* @@ -384,4 +485,12 @@ contract BatchNoConversionPayments is Ownable { function setPaymentEthProxy(address _paymentEthProxy) external onlyOwner { paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); } + + function setUSDAddress(address _USDAddress) external onlyOwner { + USDAddress = _USDAddress; + } + + function setBatchFeeAmountUSDLimit(uint256 _batchFeeAmountUSDLimit) external onlyOwner { + batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; + } } 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 4854ef2de5..365cfa0f1a 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 @@ -159,7 +159,7 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", "name": "conversionDetails", "type": "tuple[]" }, @@ -273,10 +273,15 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", "name": "conversionDetails", "type": "tuple[]" }, + { + "internalType": "uint256", + "name": "batchFeeAmountUSD", + "type": "uint256" + }, { "internalType": "address[][]", "name": "pathsToUSD", @@ -289,36 +294,69 @@ } ], "name": "batchMultiERC20ConversionPayments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { - "internalType": "address[]", - "name": "_tokenAddresses", - "type": "address[]" - }, - { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_amounts", - "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 BatchNoConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" }, { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" + "internalType": "uint256", + "name": "batchFeeAmountUSD", + "type": "uint256" }, { "internalType": "address", @@ -327,7 +365,13 @@ } ], "name": "batchMultiERC20Payments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -378,7 +422,7 @@ "type": "uint256" } ], - "internalType": "struct BatchConversionPayments.ConversionDetail[]", + "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", "name": "conversionDetails", "type": "tuple[]" }, @@ -410,7 +454,7 @@ "type": "uint256[]" } ], - "internalType": "struct BatchConversionPayments.CryptoDetails", + "internalType": "struct BatchNoConversionPayments.CryptoDetails", "name": "cryptoDetails", "type": "tuple" } diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 53f82c3792..b183f708b1 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -243,9 +243,13 @@ describe('contract: BatchConversionPayments', async () => { const toBalanceDiff = BigNumber.from(toBalance).sub(initialToBalance); const feeBalanceDiff = BigNumber.from(feeBalance).sub(initialFeeBalance); + console.log('check balance to'); expect(toBalanceDiff).to.equals(expectedToBalanceDiff, `toBalanceDiff in ${token}`); + console.log('check balance fee'); expect(feeBalanceDiff).to.equals(expectedFeeBalanceDiff, `feeBalanceDiff in ${token}`); + console.log('check balance from'); expect(fromBalanceDiff).to.equals(expectedFromBalanceDiff, `fromBalanceDiff in ${token}`); + console.log('here 1'); expect(batchBalance).to.equals('0', `batchBalance in ${token}`); }; @@ -335,14 +339,18 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [FAU_address], - recipients: [to], - amounts: ['100000'], - paymentReferences: [referenceExample], - feeAmounts: ['100'], - }, + conversionDetails: [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], @@ -457,13 +465,6 @@ describe('contract: BatchConversionPayments', async () => { const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const cryptoDetails: PaymentTypes.CryptoDetails = { - tokenAddresses: [FAU_address], - recipients: [to], - amounts: ['100000'], - paymentReferences: [referenceExample], - feeAmounts: ['100'], - }; const ethCryptoDetails: PaymentTypes.CryptoDetails = { tokenAddresses: [], recipients: [to], @@ -481,8 +482,18 @@ describe('contract: BatchConversionPayments', async () => { }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [], - cryptoDetails: cryptoDetails, + conversionDetails: [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, @@ -830,11 +841,19 @@ describe('contract: BatchConversionPayments', async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); await batchConversionProxy.batchERC20Payments( - FAU_address, - [to], - ['100000'], - [referenceExample], - ['100'], + [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [[FAU_address, USD_hash]], + 0, feeAddress, ); @@ -856,11 +875,19 @@ describe('contract: BatchConversionPayments', async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); await batchConversionProxy.batchMultiERC20Payments( - [FAU_address], - [to], - ['100000'], - [referenceExample], - ['100'], + [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [[FAU_address, USD_hash]], + 0, feeAddress, ); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 220704ff2b..6558078cb0 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -1,4 +1,4 @@ -import { ethers } from 'hardhat'; +import { ethers, network } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; import { @@ -9,7 +9,11 @@ import { BatchNoConversionPayments, ERC20FeeProxy__factory, BatchNoConversionPayments__factory, + ChainlinkConversionPath, } from '../../src/types'; +import { chainlinkConversionPath } from '../../src/lib'; +import { CurrencyManager } from '@requestnetwork/currency'; +import { ConversionDetail } from 'types/dist/payment-types'; const logGasInfos = false; @@ -28,6 +32,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { let token2: TestERC20; let token3: TestERC20; let batch: BatchNoConversionPayments; + let chainlinkPath: ChainlinkConversionPath; let erc20FeeProxy: ERC20FeeProxy; let token1Address: string; @@ -53,15 +58,20 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const erc20Decimal = BigNumber.from('1000000000000000000'); + const currencyManager = CurrencyManager.getDefault(); + const USD_hash = currencyManager.fromSymbol('USD')!.hash; + before(async () => { [, payee1, payee2, payee3, feeAddress] = (await ethers.getSigners()).map((s) => s.address); [owner, spender1, spender2, spender3] = await ethers.getSigners(); erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); const ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + chainlinkPath = chainlinkConversionPath.connect(network.name, owner); batch = await new BatchNoConversionPayments__factory(owner).deploy( erc20FeeProxy.address, ethFeeProxy.address, + chainlinkPath.address, await owner.getAddress(), ); token1 = await new TestERC20__factory(owner).deploy(erc20Decimal.mul(10000)); @@ -78,6 +88,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batchAddress = batch.address; await batch.connect(owner).setBatchFee(1000); + await batch.connect(owner).setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); }); beforeEach(async () => { @@ -108,16 +119,40 @@ describe('contract: batchNoConversionPayments: ERC20', () => { beforeERC20Balance3 = await token1.balanceOf(spender3Address); await expect( - batch - .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee2], - [200, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [20, 2, 3], - feeAddress, - ), + batch.connect(spender3).batchERC20Payments( + [ + { + recipient: payee1, + requestAmount: 200, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: 20, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 30, + path: [token1Address], + paymentReference: referenceExample2, + feeAmount: 2, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 40, + path: [token1Address], + paymentReference: referenceExample3, + feeAmount: 3, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [[token1Address, USD_hash]], + 0, + feeAddress, + ), ) .to.emit(token1, 'Transfer') .withArgs(spender3Address, batchAddress, 200 + 30 + 40 + 20 + 2 + 3) @@ -187,16 +222,40 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const beforeFeeAddress_token3 = await token3.balanceOf(feeAddress); await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token2Address, token3Address], - [payee1, payee2, payee2], - [500, 300, 400], - [referenceExample1, referenceExample2, referenceExample3], - [60, 20, 30], - feeAddress, - ), + batch.connect(spender3).batchMultiERC20Payments( + [ + { + recipient: payee1, + requestAmount: 500, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: 60, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 300, + path: [token2Address], + paymentReference: referenceExample2, + feeAmount: 20, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 400, + path: [token3Address], + paymentReference: referenceExample3, + feeAmount: 30, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [], + 0, + feeAddress, + ), ) // Transfer event of each token from the spender to the batch proxy .to.emit(token1, 'Transfer') @@ -278,16 +337,44 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const beforeFeeAddress_token2 = await token2.balanceOf(feeAddress); const beforeFeeAddress_token3 = await token3.balanceOf(feeAddress); - const tx = await batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token2Address, token3Address], - [payee1, payee2, payee2], - [500, 0, 400], - [referenceExample1, referenceExample2, referenceExample3], - [60, 0, 30], - feeAddress, - ); + const tx = await batch.connect(spender3).batchMultiERC20Payments( + [ + { + recipient: payee1, + requestAmount: 500, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: 60, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 0, + path: [token2Address], + paymentReference: referenceExample2, + feeAmount: 0, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 400, + path: [token3Address], + paymentReference: referenceExample3, + feeAmount: 30, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [ + [token1Address, USD_hash], + [token2Address, USD_hash], + [token3Address, USD_hash], + ], + 0, + feeAddress, + ); await tx.wait(); @@ -305,6 +392,83 @@ describe('contract: batchNoConversionPayments: ERC20', () => { ); }); + it.only('Should pay 3 ERC20 payments Multi tokens, with one payment of 0 token', async () => { + await token1.connect(owner).transfer(spender3Address, 1000_000_000_000_000); + await token2.connect(owner).transfer(spender3Address, 1000_000_000_000_000); + await token3.connect(owner).transfer(spender3Address, 1000_000_000_000_000); + + await token1.connect(spender3).approve(batchAddress, 1000_000_000_000_000); + await token2.connect(spender3).approve(batchAddress, 1000_000_000_000_000); + await token3.connect(spender3).approve(batchAddress, 1000_000_000_000_000); + + beforeERC20Balance1 = await token1.balanceOf(payee1); + const beforeERC20Balance2_token2 = await token2.balanceOf(payee2); + const beforeERC20Balance2_token3 = await token3.balanceOf(payee2); + beforeERC20Balance3 = await token1.balanceOf(spender3Address); + + const beforeFeeAddress_token1 = await token1.balanceOf(feeAddress); + const beforeFeeAddress_token2 = await token2.balanceOf(feeAddress); + const beforeFeeAddress_token3 = await token3.balanceOf(feeAddress); + + const tx = await batch.connect(spender3).batchMultiERC20Payments( + [ + { + recipient: payee1, + requestAmount: 500_000_000_000_000, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: 60, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 0, + path: [token2Address], + paymentReference: referenceExample2, + feeAmount: 0, + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: 400_000_000_000_000, + path: [token3Address], + paymentReference: referenceExample3, + feeAmount: 30, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + [ + [token1Address, USD_hash], + [token2Address, USD_hash], + [token3Address, USD_hash], + ], + 0, + feeAddress, + ); + + await tx.wait(); + + expect(await token1.balanceOf(payee1)).to.be.equal( + beforeERC20Balance1.add(500_000_000_000_000), + ); + expect(await token2.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token2.add(0)); + expect(await token3.balanceOf(payee2)).to.be.equal( + beforeERC20Balance2_token3.add(400_000_000_000_000), + ); + expect(beforeERC20Balance3).to.be.equal( + (await token1.balanceOf(spender3Address)).add(500_000_000_000_000 + 60 + 50), + ); + + expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(50 + 60)); + expect(await token2.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token2.add(0)); + expect(await token3.balanceOf(feeAddress)).to.be.equal( + beforeFeeAddress_token3.add((30 + 40) * 1), + ); + }); + it('Should pay 4 ERC20 payments on 2 tokens', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); @@ -319,23 +483,35 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const amount = 20; const feeAmount = 1; - const nbTxs = 4; - const [tokenAddresses, recipients, amounts, paymentReferences, feeAmounts] = - getBatchPaymentsInputs(nbTxs, token1Address, payee2, amount, referenceExample1, feeAmount); - - tokenAddresses[2] = token2Address; - tokenAddresses[3] = token2Address; - - const tx = await batch - .connect(spender3) - .batchMultiERC20Payments( - tokenAddresses, - recipients, - amounts, - paymentReferences, - feeAmounts, - feeAddress, - ); + + const tx = await batch.connect(spender3).batchMultiERC20Payments( + [ + ...Array(2).fill({ + recipient: payee2, + requestAmount: amount, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: feeAmount, + maxToSpend: '0', + maxRateTimespan: '0', + }), + ...Array(2).fill({ + recipient: payee2, + requestAmount: amount, + path: [token2Address], + paymentReference: referenceExample1, + feeAmount: feeAmount, + maxToSpend: '0', + maxRateTimespan: '0', + }), + ], + [ + [token1Address, USD_hash], + [token2Address, USD_hash], + ], + 0, + feeAddress, + ); await tx.wait(); afterERC20Balance1 = await token1.balanceOf(payee2); @@ -351,8 +527,8 @@ describe('contract: batchNoConversionPayments: ERC20', () => { expect(beforeERC20Balance3Token2).to.be.equal(afterERC20Balance3Token2.add((20 + 1 + 2) * 2)); }); - it('Should pay 10 ERC20 payments', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); + it.only('Should pay 10 ERC20 payments', async () => { + await token1.connect(owner).transfer(spender3Address, 100); await token1.connect(spender3).approve(batchAddress, 1000); beforeERC20Balance1 = await token1.balanceOf(payee1); @@ -362,19 +538,22 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const feeAmount = 10; const nbTxs = 10; - const [token1Addresses, recipients, amounts, paymentReferences, feeAmounts] = - getBatchPaymentsInputs(nbTxs, token1Address, payee1, amount, referenceExample1, feeAmount); - - const tx = await batch - .connect(spender3) - .batchERC20Payments( - token1Addresses[0], - recipients, - amounts, - paymentReferences, - feeAmounts, - feeAddress, - ); + const tx = await batch.connect(spender3).batchERC20Payments( + [ + ...Array(nbTxs).fill({ + recipient: payee1, + requestAmount: amount, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: feeAmount, + maxToSpend: '0', + maxRateTimespan: '0', + }), + ], + [[token1Address, USD_hash]], + 0, + feeAddress, + ); await tx.wait(); const receipt = await tx.wait(); @@ -402,29 +581,39 @@ describe('contract: batchNoConversionPayments: ERC20', () => { const amount = 20; const feeAmount = 10; - const nbTxs = 10; - - const [tokenAddresses, recipients, amounts, paymentReferences, feeAmounts] = - getBatchPaymentsInputs(nbTxs, token1Address, payee1, amount, referenceExample1, feeAmount); - - for (let i = 0; i < 5; i++) { - tokenAddresses[i] = token2Address; - } - const tx = await batch - .connect(spender3) - .batchMultiERC20Payments( - tokenAddresses, - recipients, - amounts, - paymentReferences, - feeAmounts, - feeAddress, - ); + const tx = await batch.connect(spender3).batchMultiERC20Payments( + [ + ...Array(5).fill({ + recipient: payee1, + requestAmount: amount, + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: feeAmount, + maxToSpend: '0', + maxRateTimespan: '0', + }), + ...Array(5).fill({ + recipient: payee1, + requestAmount: amount, + path: [token2Address], + paymentReference: referenceExample1, + feeAmount: feeAmount, + maxToSpend: '0', + maxRateTimespan: '0', + }), + ], + [ + [token1Address, USD_hash], + [token2Address, USD_hash], + ], + 0, + feeAddress, + ); const receipt = await tx.wait(); if (logGasInfos) { - console.log(`nbTxs= ${nbTxs}, gas consumption: `, receipt.gasUsed.toString()); + console.log(`nbTxs=10, gas consumption: `, receipt.gasUsed.toString()); } afterERC20Balance1 = await token1.balanceOf(payee1); @@ -435,21 +624,48 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }); describe('Batch revert, issues with: args, or funds, or approval', () => { + let conversionDetails: ConversionDetail[] = []; + beforeEach(async () => { + conversionDetails = [ + { + recipient: payee1, + requestAmount: '5', + path: [token1Address], + paymentReference: referenceExample1, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee2, + requestAmount: '30', + path: [token1Address], + paymentReference: referenceExample2, + feeAmount: '2', + maxToSpend: '0', + maxRateTimespan: '0', + }, + { + recipient: payee3, + requestAmount: '40', + path: [token1Address], + paymentReference: referenceExample3, + feeAmount: '3', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ]; + }); + it('Should revert batch if not enough funds to pay the request', async () => { await token1.connect(owner).transfer(spender3Address, 100); await token1.connect(spender3).approve(batchAddress, 1000); + conversionDetails[2].requestAmount = '400'; await expect( batch .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee3], - [5, 30, 400], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), + .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('not enough funds'); }); @@ -457,15 +673,15 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 1000); + conversionDetails[0].requestAmount = '100'; + conversionDetails[1].requestAmount = '200'; await expect( batch .connect(spender3) .batchERC20Payments( - token1Address, - [payee1, payee2], - [100, 200], - [referenceExample1, referenceExample2], - [1, 2], + conversionDetails.slice(0, 2), + [[token1Address, USD_hash]], + 0, feeAddress, ), ).revertedWith('not enough funds for the batch fee'); @@ -474,17 +690,11 @@ describe('contract: batchNoConversionPayments: ERC20', () => { it('Should revert batch without approval', async () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 10); + conversionDetails[0].requestAmount = '20'; await expect( batch .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee3], - [20, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), + .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); @@ -492,17 +702,11 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 400); await token1.connect(spender3).approve(batchAddress, 1000); + conversionDetails[2].requestAmount = '400'; await expect( batch .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee3], - [5, 30, 400], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), + .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('not enough funds'); }); @@ -510,17 +714,14 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 607); await token1.connect(spender3).approve(batchAddress, 1000); + conversionDetails[0].requestAmount = '100'; + conversionDetails[1].requestAmount = '200'; + conversionDetails[2].requestAmount = '300'; + conversionDetails[2].recipient = payee2; await expect( batch .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee2], - [100, 200, 300], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), + .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('not enough funds'); }); @@ -528,164 +729,14 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 10); + conversionDetails[0].requestAmount = '100'; + conversionDetails[1].requestAmount = '200'; + conversionDetails[2].requestAmount = '300'; await expect( batch .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee3], - [100, 200, 300], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), + .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); - - it('Should revert batch multi tokens if input s arrays do not have same size', async () => { - await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address], - [payee1, payee2, payee3], - [5, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2], - [5, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee3], - [5, 30], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee3], - [5, 30, 40], - [referenceExample1, referenceExample2], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchMultiERC20Payments( - [token1Address, token1Address, token1Address], - [payee1, payee2, payee3], - [5, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - }); - - it('Should revert batch if input s arrays do not have same size', async () => { - await expect( - batch - .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee3], - [5, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2], - [5, 30, 40], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee3], - [5, 30], - [referenceExample1, referenceExample2, referenceExample3], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(spender3) - .batchERC20Payments( - token1Address, - [payee1, payee2, payee3], - [5, 30, 40], - [referenceExample1, referenceExample2], - [1, 2, 3], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - }); }); }); - -// Allow to create easly batchNoConversionPayments input, especially for gas optimization -const getBatchPaymentsInputs = function ( - nbTxs: number, - tokenAddress: string, - recipient: string, - amount: number, - referenceExample1: string, - feeAmount: number, -): [Array, Array, Array, Array, Array] { - let tokenAddresses = []; - let recipients = []; - let amounts = []; - let paymentReferences = []; - let feeAmounts = []; - - for (let i = 0; i < nbTxs; i++) { - tokenAddresses.push(tokenAddress); - recipients.push(recipient); - amounts.push(amount); - paymentReferences.push(referenceExample1); - feeAmounts.push(feeAmount); - } - return [tokenAddresses, recipients, amounts, paymentReferences, feeAmounts]; -}; diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index dd550678af..508e0ea383 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -5,8 +5,10 @@ import { EthereumFeeProxy__factory, BatchNoConversionPayments__factory, ERC20FeeProxy__factory, + ChainlinkConversionPath, } from '../../src/types'; import { EthereumFeeProxy, BatchNoConversionPayments } from '../../src/types'; +import { chainlinkConversionPath } from '../../src/lib'; import { HttpNetworkConfig } from 'hardhat/types'; const logGasInfos = false; @@ -29,6 +31,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const referenceExample2 = '0xbbbb'; let ethFeeProxy: EthereumFeeProxy; + let chainlinkPath: ChainlinkConversionPath; let batch: BatchNoConversionPayments; const networkConfig = network.config as HttpNetworkConfig; const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); @@ -39,9 +42,11 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const erc20FeeProxy = await new ERC20FeeProxy__factory(owner).deploy(); ethFeeProxy = await new EthereumFeeProxy__factory(owner).deploy(); + chainlinkPath = chainlinkConversionPath.connect(network.name, owner); batch = await new BatchNoConversionPayments__factory(owner).deploy( erc20FeeProxy.address, ethFeeProxy.address, + chainlinkPath.address, await owner.getAddress(), ); batchAddress = batch.address; From 59a14856f716888034b0fe7013a9ed08e74083f9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 10 Oct 2022 18:35:57 +0200 Subject: [PATCH 097/138] batch erc20 functions tested --- .../contracts/BatchNoConversionPayments.sol | 7 +- .../contracts/BatchConversionPayments.test.ts | 685 ++++++++++++------ .../BatchNoConversionErc20Payments.test.ts | 82 +-- 3 files changed, 452 insertions(+), 322 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index b408ea12b1..ea9bd21e9c 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -222,10 +222,9 @@ contract BatchNoConversionPayments is Ownable { require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); // Payer pays batch fee amount - uint256 batchFeeToPay = amount; - - (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( - batchFeeToPay, + // amount that represents batchFeeToPay updated if needed + (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( + amount, conversionDetails[0].path[0], batchFeeAmountUSD, pathsToUSD diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index b183f708b1..9485a0ed3a 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -141,12 +141,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy.setBatchConversionFee(BATCH_CONV_FEE); await batchConversionProxy.setUSDAddress(currencyManager.fromSymbol('USD')!.hash); - // set a batchFeeAmountUSDLimit equal to 1001$ + 2*1001€ - await batchConversionProxy.setBatchFeeAmountUSDLimit( - BigNumber.from(1001 * 100 + 2 * 12 * (10000 + 12)) - .mul(1e8) - .div(100), - ); + await batchConversionProxy.setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); batchConversionProxy = batchConversionProxy.connect(fromSigner); @@ -180,7 +175,7 @@ describe('contract: BatchConversionPayments', async () => { amount: number, fee: number, nPayment: number, - path: string, + path: 'EUR_DAI' | 'USD_FAU', ) => { // Temporary decimal offset to have a precise conversion with floating rates const precision = 1_000_000; @@ -188,41 +183,37 @@ describe('contract: BatchConversionPayments', async () => { path === 'EUR_DAI' ? BigNumber.from(daiDecimals).mul(precision).mul(EUR_USD_RATE).div(DAI_USD_RATE) : BigNumber.from(daiDecimals).mul(precision).mul(PRECISION_RATE).div(FAU_USD_RATE); - const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); - const expectedFeeDAIBalanceDiff = + const expectedToBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); + const expectedFeeBalanceDiff = // fee added by the batch - expectedToDAIBalanceDiff + expectedToBalanceDiff .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(expectedFeeDAIBalanceDiff) - .mul(-1); + const expectedFromBalanceDiff = expectedToBalanceDiff.add(expectedFeeBalanceDiff).mul(-1); return [ - expectedFromDAIBalanceDiff.div(precision), - expectedToDAIBalanceDiff.div(precision), - expectedFeeDAIBalanceDiff.div(precision), + expectedFromBalanceDiff.div(precision), + expectedToBalanceDiff.div(precision), + expectedFeeBalanceDiff.div(precision), ]; }; /** No conversion */ - const getExpectedERC20Balances = (amount: number, fee: number, nPayment: number) => { - const expectedToDAIBalanceDiff = BigNumber.from(amount).mul(nPayment); - const expectedFeeDAIBalanceDiff = + const getExpectedERC20Balances = (amount: string, fee: string, nPayment: number) => { + const expectedToBalanceDiff = BigNumber.from(amount).mul(nPayment); + const expectedFeeBalanceDiff = // fee added by the batch - expectedToDAIBalanceDiff + expectedToBalanceDiff .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(expectedFeeDAIBalanceDiff) - .mul(-1); - return [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff]; + const expectedFromBalanceDiff = expectedToBalanceDiff.add(expectedFeeBalanceDiff).mul(-1); + return [expectedFromBalanceDiff, expectedToBalanceDiff, expectedFeeBalanceDiff]; }; /** Compares the expected delta-balances with the one it computes for from, to and fee addresses. */ @@ -242,7 +233,6 @@ describe('contract: BatchConversionPayments', async () => { const fromBalanceDiff = BigNumber.from(fromBalance).sub(initialFromBalance); const toBalanceDiff = BigNumber.from(toBalance).sub(initialToBalance); const feeBalanceDiff = BigNumber.from(feeBalance).sub(initialFeeBalance); - console.log('check balance to'); expect(toBalanceDiff).to.equals(expectedToBalanceDiff, `toBalanceDiff in ${token}`); console.log('check balance fee'); @@ -332,243 +322,460 @@ describe('contract: BatchConversionPayments', async () => { }; describe('batchRouter', async () => { - it(`make 1 ERC20 payment with no conversion`, async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - await batchConversionProxy.batchRouter( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + describe('payment under the fee limit', async () => { + it(`make 1 ERC20 payment with no conversion`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, + }, + ], + [[FAU_address, USD_hash]], + feeAddress, + ); + + // check the balance fauERC20 token + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('100000', '100', 1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { + const batchPayment = async () => { + return await batchConversionProxy.batchRouter( + [ { - recipient: to, - requestAmount: '100000', - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '100', - maxToSpend: '0', - maxRateTimespan: '0', + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], + cryptoDetails: emptyCryptoDetails, }, ], - cryptoDetails: emptyCryptoDetails, - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - ); + [ + [FAU_address, USD_hash], + [DAI_address, USD_hash], + ], + feeAddress, + ); + }; + await manyPaymentsBatchConv(batchPayment); + }); - // check the balance fauERC20 token - const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances(100000, 100, 1); + it('make 1 ETH payment without conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - }); - it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { - const batchPayment = async () => { - return await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchRouter( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], - cryptoDetails: emptyCryptoDetails, + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [], + cryptoDetails: { + tokenAddresses: [], + recipients: [to], + amounts: ['1000'], + paymentReferences: [referenceExample], + feeAmounts: ['1'], + }, }, ], - [ - [FAU_address, USD_hash], - [DAI_address, USD_hash], - ], + [], feeAddress, + { value: 1000 + 1 + 11 }, // + 11 to pay batch fees ); - }; - await manyPaymentsBatchConv(batchPayment); - }); - it('make 1 ETH payment without conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + await expectETHBalanceDiffs( + BigNumber.from(1000), + BigNumber.from(1), + BATCH_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); - tx = await batchConversionProxy.batchRouter( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [], - recipients: [to], - amounts: ['1000'], - paymentReferences: [referenceExample], - feeAmounts: ['1'], + it('make 1 ETH payment with 1-step conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, }, - }, - ], - [], - feeAddress, - { value: 1000 + 1 + 11 }, // + 11 to pay batch fees - ); - - await expectETHBalanceDiffs( - BigNumber.from(1000), - BigNumber.from(1), - BATCH_FEE, - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - ); - }); - - it('make 1 ETH payment with 1-step conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchRouter( - [ + ], + [], + feeAddress, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [ethConvDetail], - cryptoDetails: emptyCryptoDetails, + value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }, - ], - [], - feeAddress, - { - value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees - }, - ); - - await expectETHBalanceDiffs( - BigNumber.from(1000 * USD_ETH_RATE), - BigNumber.from(1 * USD_ETH_RATE), - BATCH_CONV_FEE, - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - ); - }); - - it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { - // get balances - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + ); - // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const ethCryptoDetails: PaymentTypes.CryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: ['1000'], - paymentReferences: [referenceExample], - feeAmounts: ['1'], - }; + await expectETHBalanceDiffs( + BigNumber.from(1000 * USD_ETH_RATE), + BigNumber.from(1 * USD_ETH_RATE), + BATCH_CONV_FEE, + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + ); + }); - tx = await batchConversionProxy.batchRouter( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail], - cryptoDetails: emptyCryptoDetails, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ - { - recipient: to, - requestAmount: '100000', - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '100', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], - cryptoDetails: emptyCryptoDetails, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [], - cryptoDetails: ethCryptoDetails, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [ethConvDetail], - cryptoDetails: emptyCryptoDetails, - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees - ); + it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { + // get balances + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + // set inputs: ERC20 cryptoDetails & ethCryptoDetails + const ethCryptoDetails: PaymentTypes.CryptoDetails = { + tokenAddresses: [], + recipients: [to], + amounts: ['1000'], + paymentReferences: [referenceExample], + feeAmounts: ['1'], + }; + + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [], + cryptoDetails: ethCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [ethConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + [[FAU_address, USD_hash]], + feeAddress, + { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees + ); - // Chech FAU Balances // - const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + // Chech FAU Balances // + const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + const [ + noConvExpectedFromFAUBalanceDiff, + noConvExpectedToFAUBalanceDiff, + noConvExpectedFeeFAUBalanceDiff, + ] = getExpectedERC20Balances('100000', '100', 1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff.add(noConvExpectedFromFAUBalanceDiff), + expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), + expectedFeeFAUBalanceDiff.add(noConvExpectedFeeFAUBalanceDiff), + ); - const [ - noConvExpectedFromFAUBalanceDiff, - noConvExpectedToFAUBalanceDiff, - noConvExpectedFeeFAUBalanceDiff, - ] = getExpectedERC20Balances(100000, 100, 1); + // Check ETH balances // + const receipt = await tx.wait(); + const gasAmount = receipt.gasUsed.mul(gasPrice); + + const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const toETHBalance = await provider.getBalance(to); + const feeETHBalance = await provider.getBalance(feeAddress); + const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - initial + const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); + const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); + const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + + // expectedFeeETHBalanceDiff includes batch conversion fees now + 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 = gasAmount + .add(1000 * USD_ETH_RATE + 1000) + .add(expectedFeeETHBalanceDiff) + .mul(-1); + + // Check balance changes + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(toETHBalanceDiff).to.equals( + BigNumber.from(1000 * USD_ETH_RATE + 1000), + 'toETHBalanceDiff', + ); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + }); + }); + describe('payment above the fee limit', async () => { + before(async () => { + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); // 1_000 $ + }); + after(async () => { + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); // 100_000 $ + }); + it.only(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '20100' + daiDecimals, + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '0' + daiDecimals, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, + }, + ], + [[FAU_address, USD_hash]], + feeAddress, + ); - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff.add(noConvExpectedFromFAUBalanceDiff), - expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), - expectedFeeFAUBalanceDiff.add(noConvExpectedFeeFAUBalanceDiff), - ); + // check the balance fauERC20 token + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('20100' + daiDecimals, '0' + daiDecimals, 1); + + // 1000$ of batch fees in FAU + expectedFeeFAUBalanceDiff = BigNumber.from('498512437810945273631'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it(`make 1 ERC20 payment with no conversion`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '10100' + daiDecimals, + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '0' + daiDecimals, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, + }, + ], + [[FAU_address, USD_hash]], + feeAddress, + ); - // Check ETH balances // - const receipt = await tx.wait(); - const gasAmount = receipt.gasUsed.mul(gasPrice); + // check the balance fauERC20 token + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('10100' + daiDecimals, '0' + daiDecimals, 1); + + // 1000$ of batch fees in FAU + expectedFeeFAUBalanceDiff = BigNumber.from('498512437810945273631'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it(`make 1 ERC20 payment with no conversion and wrong paths to USD`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '10100' + daiDecimals, + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '0' + daiDecimals, + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + cryptoDetails: emptyCryptoDetails, + }, + ], + [[DAI_address, USD_hash]], + feeAddress, + ); - const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const toETHBalance = await provider.getBalance(to); - const feeETHBalance = await provider.getBalance(feeAddress); - const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + // check the balance fauERC20 token + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('10100' + daiDecimals, '0' + daiDecimals, 1); + + // around 1001$ of batch fees in FAU - the fee limit is not applied because of the wrong path + expectedFeeFAUBalanceDiff = BigNumber.from('506005000000000000000'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it('make 2 ERC20 payments with conversion with limited fees', async () => { + // get balances + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = + await getERC20Balances(daiERC20); + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [daiConvDetail], + cryptoDetails: emptyCryptoDetails, + }, + ], + [ + [FAU_address, USD_hash], + [DAI_address, USD_hash], + ], + feeAddress, + ); - // Calculate the difference of the balance : now - initial - const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); - const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); - const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + // Check FAU Balances // + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + // 1000$ of batch fees converted in FAU, and 100$ of fees converted in FAU. + // instead of (1000$ + 1$) + 100$ of fees + expectedFeeFAUBalanceDiff = BigNumber.from('547263681597009955218'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); - // expectedFeeETHBalanceDiff includes batch conversion fees now - 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 = gasAmount - .add(1000 * USD_ETH_RATE + 1000) - .add(expectedFeeETHBalanceDiff) - .mul(-1); - - // Check balance changes - expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); - expect(toETHBalanceDiff).to.equals( - BigNumber.from(1000 * USD_ETH_RATE + 1000), - 'toETHBalanceDiff', - ); - expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); - expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + // Check DAI Balances // + let [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); + console.log('JJJ'); + // 0€ of batch fees converted in DAI, and 100€ of fees converted in DAI. + // instead of (1000€ + 1€) + 100€ of fees without the limit + expectedFeeDAIBalanceDiff = BigNumber.from('118811881188118811881'); + expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff + .add(expectedFeeDAIBalanceDiff) + .mul(-1); + + await expectERC20BalanceDiffs( + 'DAI', + initialFromDAIBalance, + initialToDAIBalance, + initialFeeDAIBalance, + expectedFromDAIBalanceDiff, + expectedToDAIBalanceDiff, + expectedFeeDAIBalanceDiff, + ); + }); }); }); describe('batchRouter errors', async () => { @@ -858,7 +1065,7 @@ describe('contract: BatchConversionPayments', async () => { ); const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances(100000, 100, 1); + getExpectedERC20Balances('100000', '100', 1); await expectERC20BalanceDiffs( 'FAU', @@ -892,7 +1099,7 @@ describe('contract: BatchConversionPayments', async () => { ); const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances(100000, 100, 1); + getExpectedERC20Balances('100000', '100', 1); await expectERC20BalanceDiffs( 'FAU', initialFromFAUBalance, diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 6558078cb0..c9e35355b1 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -88,6 +88,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batchAddress = batch.address; await batch.connect(owner).setBatchFee(1000); + // batch fee amount USD limited to 1$ await batch.connect(owner).setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); }); @@ -392,83 +393,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { ); }); - it.only('Should pay 3 ERC20 payments Multi tokens, with one payment of 0 token', async () => { - await token1.connect(owner).transfer(spender3Address, 1000_000_000_000_000); - await token2.connect(owner).transfer(spender3Address, 1000_000_000_000_000); - await token3.connect(owner).transfer(spender3Address, 1000_000_000_000_000); - - await token1.connect(spender3).approve(batchAddress, 1000_000_000_000_000); - await token2.connect(spender3).approve(batchAddress, 1000_000_000_000_000); - await token3.connect(spender3).approve(batchAddress, 1000_000_000_000_000); - - beforeERC20Balance1 = await token1.balanceOf(payee1); - const beforeERC20Balance2_token2 = await token2.balanceOf(payee2); - const beforeERC20Balance2_token3 = await token3.balanceOf(payee2); - beforeERC20Balance3 = await token1.balanceOf(spender3Address); - - const beforeFeeAddress_token1 = await token1.balanceOf(feeAddress); - const beforeFeeAddress_token2 = await token2.balanceOf(feeAddress); - const beforeFeeAddress_token3 = await token3.balanceOf(feeAddress); - - const tx = await batch.connect(spender3).batchMultiERC20Payments( - [ - { - recipient: payee1, - requestAmount: 500_000_000_000_000, - path: [token1Address], - paymentReference: referenceExample1, - feeAmount: 60, - maxToSpend: '0', - maxRateTimespan: '0', - }, - { - recipient: payee2, - requestAmount: 0, - path: [token2Address], - paymentReference: referenceExample2, - feeAmount: 0, - maxToSpend: '0', - maxRateTimespan: '0', - }, - { - recipient: payee2, - requestAmount: 400_000_000_000_000, - path: [token3Address], - paymentReference: referenceExample3, - feeAmount: 30, - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], - [ - [token1Address, USD_hash], - [token2Address, USD_hash], - [token3Address, USD_hash], - ], - 0, - feeAddress, - ); - - await tx.wait(); - - expect(await token1.balanceOf(payee1)).to.be.equal( - beforeERC20Balance1.add(500_000_000_000_000), - ); - expect(await token2.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token2.add(0)); - expect(await token3.balanceOf(payee2)).to.be.equal( - beforeERC20Balance2_token3.add(400_000_000_000_000), - ); - expect(beforeERC20Balance3).to.be.equal( - (await token1.balanceOf(spender3Address)).add(500_000_000_000_000 + 60 + 50), - ); - - expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(50 + 60)); - expect(await token2.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token2.add(0)); - expect(await token3.balanceOf(feeAddress)).to.be.equal( - beforeFeeAddress_token3.add((30 + 40) * 1), - ); - }); - it('Should pay 4 ERC20 payments on 2 tokens', async () => { await token1.connect(owner).transfer(spender3Address, 1000); await token2.connect(owner).transfer(spender3Address, 1000); @@ -527,8 +451,8 @@ describe('contract: batchNoConversionPayments: ERC20', () => { expect(beforeERC20Balance3Token2).to.be.equal(afterERC20Balance3Token2.add((20 + 1 + 2) * 2)); }); - it.only('Should pay 10 ERC20 payments', async () => { - await token1.connect(owner).transfer(spender3Address, 100); + it('Should pay 10 ERC20 payments', async () => { + await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 1000); beforeERC20Balance1 = await token1.balanceOf(payee1); From 53e75c89086f80f47d05cdad868a53402d6b0f0b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 11 Oct 2022 23:25:09 +0200 Subject: [PATCH 098/138] functions and test working --- ...test-deploy-batch-conversion-deployment.ts | 8 +- .../src/contracts/BatchConversionPayments.sol | 149 ++++---- .../contracts/BatchNoConversionPayments.sol | 268 ++++++++------ .../src/contracts/BatchPayments.sol | 4 +- .../BatchConversionPayments/0.1.0.json | 267 ++++++++------ .../contracts/BatchConversionPayments.test.ts | 341 ++++++++++++++---- .../BatchNoConversionErc20Payments.test.ts | 8 +- .../BatchNoConversionEthPayments.test.ts | 197 +++------- 8 files changed, 734 insertions(+), 508 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 b057bb65a2..9fbc0b2ffe 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -76,8 +76,12 @@ export async function deployBatchConversionPayment( // 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); - await batchConversion.connect(owner).setUSDAddress(currencyManager.fromSymbol('USD')!.hash); + await batchConversion + .connect(owner) + .setETHAndUSDAddress( + currencyManager.fromSymbol('ETH')!.hash, + currencyManager.fromSymbol('USD')!.hash, + ); await batchConversion.connect(owner).setBatchFeeAmountUSDLimit(300); // * 1_000_000_000_000_000_000); // ---------------------------------- diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 562b51c07f..6520898f06 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -15,8 +15,7 @@ import './BatchNoConversionPayments.sol'; * - fees: conversion proxy fees and additional batch conversion fees are paid to the same address. * 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 + * @dev batchRouter is the main function, but other batch payment functions are "public" in order to do * gas optimization in some cases. */ contract BatchConversionPayments is BatchNoConversionPayments { @@ -25,18 +24,14 @@ contract BatchConversionPayments is BatchNoConversionPayments { IERC20ConversionProxy public paymentErc20ConversionProxy; IEthConversionProxy public paymentEthConversionProxy; - uint256 public batchConversionFee; - /** * @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 4 - * - cryptoDetails all the data required to pay requests without conversion, for paymentNetworkId = 1, 2, or 3 + * - conversionDetails all the data required for conversion and no conversion requests to be paid */ struct MetaDetail { uint256 paymentNetworkId; ConversionDetail[] conversionDetails; - CryptoDetails cryptoDetails; } /** @@ -64,31 +59,33 @@ contract BatchConversionPayments is BatchNoConversionPayments { { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); - batchConversionFee = 0; } /** * @notice Batch payments on different payment networks at once. - * @param metaDetails contains paymentNetworkId, conversionDetails, and cryptoDetails + * @param metaDetails contains paymentNetworkId and conversionDetails * - 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 pathsToUSD The list of paths into USD for every token, used to limit the batch fees, caution, - Caution, the calculation of batchFeeAmountUSD which allows to limit the batch fees takes only - into consideration these paths. Without paths, there is not limitation. - * @param _feeAddress The address where fees should be paid + * If metaDetails use paymentNetworkId = 4, it must be at the end of the list, or the transaction can be reverted. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not limitation. + * @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[][] calldata pathsToUSD, - address _feeAddress + address feeAddress ) external payable { require(metaDetails.length < 6, 'more than 5 metaDetails'); + if (pathsToUSD.length > 0) { + // Set to true to limit the batch fee to pay + batchPaymentOrigin = true; + } uint256 batchFeeAmountUSD = 0; for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; @@ -97,66 +94,76 @@ contract BatchConversionPayments is BatchNoConversionPayments { metaConversionDetail.conversionDetails, batchFeeAmountUSD, pathsToUSD, - _feeAddress + feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 1) { - batchERC20Payments( + batchFeeAmountUSD += batchERC20Payments( metaConversionDetail.conversionDetails, pathsToUSD, batchFeeAmountUSD, - _feeAddress + feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 2) { batchFeeAmountUSD += batchMultiERC20Payments( metaConversionDetail.conversionDetails, pathsToUSD, batchFeeAmountUSD, - _feeAddress + feeAddress ); } else if (metaConversionDetail.paymentNetworkId == 3) { 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) + batchFeeAmountUSD += batchEthPayments( + metaConversionDetail.conversionDetails, + batchFeeAmountUSD, + payable(feeAddress) ); if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { transferBackRemainingEth = true; } } else if (metaConversionDetail.paymentNetworkId == 4) { - batchEthConversionPayments(metaConversionDetail.conversionDetails, payable(_feeAddress)); + batchFeeAmountUSD += batchEthConversionPayments( + metaConversionDetail.conversionDetails, + batchFeeAmountUSD, + payable(feeAddress) + ); } else { - revert('wrong paymentNetworkId'); + revert('Wrong paymentNetworkId'); } } + if (pathsToUSD.length > 0) { + batchPaymentOrigin = false; + } } /** * @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 batchFeeAmountUSD The batchFeeAmountUSD already paid - * @param pathsToUSD The list of paths into USD for every token - * @param _feeAddress The fee recipient + * @param conversionDetails List of ERC20 requests denominated in fiat to pay. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not limitation. + * @param feeAddress The fee recipient */ function batchMultiERC20ConversionPayments( ConversionDetail[] calldata conversionDetails, uint256 batchFeeAmountUSD, address[][] calldata pathsToUSD, - address _feeAddress + address feeAddress ) public returns (uint256) { + // Avoid the possibility to manually put high value to batchFeeAmountUSD + if (batchPaymentOrigin != true) { + batchFeeAmountUSD = 0; + } Token[] memory uTokens = getUTokens(conversionDetails); 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++) { requestedToken = IERC20(uTokens[k].tokenAddress); - uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchConversionFee) / tenThousand; + uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchFee) / feeDenominator; // Check proxy's allowance from user, and user's funds to pay approximated amounts. require( requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, @@ -164,7 +171,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { ); require( requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, - 'not enough funds, including fees' + 'Not enough funds, including fees' ); // Transfer the amount and fee required for the token on the batch conversion contract @@ -191,7 +198,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { rI.path, rI.paymentReference, rI.feeAmount, - _feeAddress, + feeAddress, rI.maxToSpend, rI.maxRateTimespan ); @@ -208,8 +215,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { requestedToken.safeTransfer(msg.sender, excessAmount); } - uint256 batchFeeToPay = ((uTokens[k].amountAndFee - excessAmount) * batchConversionFee) / - tenThousand; + // Calculate batch fee to pay + uint256 batchFeeToPay = ((uTokens[k].amountAndFee - excessAmount) * batchFee) / + feeDenominator; (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( batchFeeToPay, @@ -220,8 +228,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { // Payer pays the exact batch fees amount require( - safeTransferFrom(uTokens[k].tokenAddress, _feeAddress, batchFeeToPay), - 'batch fee transferFrom() failed' + safeTransferFrom(uTokens[k].tokenAddress, feeAddress, batchFeeToPay), + 'Batch fee transferFrom() failed' ); } return batchFeeAmountUSD; @@ -230,9 +238,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @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 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. + * @param conversionDetails List of ETH requests denominated in fiat to pay. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. * Please: * Note that if there is not enough ether attached to the function call, @@ -241,50 +249,55 @@ contract BatchConversionPayments is BatchNoConversionPayments { */ function batchEthConversionPayments( ConversionDetail[] calldata conversionDetails, - address payable _feeAddress - ) public payable { + uint256 batchFeeAmountUSD, + address payable feeAddress + ) public payable returns (uint256) { + // Avoid the possibility to manually put high value to batchFeeAmountUSD + if (batchPaymentOrigin != true) { + batchFeeAmountUSD = 0; + } uint256 contractBalance = address(this).balance; payerAuthorized = true; // Batch contract pays the requests through EthConversionProxy for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory cD = conversionDetails[i]; paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( - payable(conversionDetails[i].recipient), - conversionDetails[i].requestAmount, - conversionDetails[i].path, - conversionDetails[i].paymentReference, - conversionDetails[i].feeAmount, - _feeAddress, - conversionDetails[i].maxRateTimespan + payable(cD.recipient), + cD.requestAmount, + cD.path, + cD.paymentReference, + cD.feeAmount, + feeAddress, + cD.maxRateTimespan ); } - // Check that batch contract has enough funds to pay batch conversion fees - uint256 amountBatchFees = (((contractBalance - address(this).balance)) * batchConversionFee) / - tenThousand; - require(address(this).balance >= amountBatchFees, 'not enough funds for batch conversion fees'); - // Batch contract pays batch fee - _feeAddress.transfer(amountBatchFees); + uint256 batchFeeToPay = (((contractBalance - address(this).balance)) * batchFee) / + feeDenominator; + + (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeToPay, + pathsEthToUSD[0][0], + batchFeeAmountUSD, + pathsEthToUSD + ); + require(address(this).balance >= batchFeeToPay, 'Not enough funds for batch conversion fees'); + feeAddress.transfer(batchFeeToPay); // 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; + + return batchFeeAmountUSD; } /* - * Admin functions to edit the conversion proxies address and fees + * Admin functions to edit the conversion proxies address and fees. */ - /** - * @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; - } - /** * @param _paymentErc20ConversionProxy The address of the ERC20 Conversion payment proxy to use. * Update cautiously, the proxy has to match the invoice proxy. @@ -302,8 +315,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { } /** - * @notice Update the conversion path contract used to fetch conversions - * @param _chainlinkConversionPathAddress address of the conversion path contract + * @notice Update the conversion path contract used to fetch conversions. + * @param _chainlinkConversionPathAddress The address of the conversion path contract. */ function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index ea9bd21e9c..abf65ba7e9 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -13,8 +13,8 @@ import './ChainlinkConversionPath.sol'; * @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. + * - 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 three main modifications: * - function "receive" has one other condition: payerAuthorized @@ -30,45 +30,42 @@ contract BatchNoConversionPayments is Ownable { uint256 public batchFee; /** Used to to calculate batch fees */ - uint256 internal tenThousand = 10000; + uint256 internal feeDenominator = 10000; - // payerAuthorized is set to true only when needed for batch Eth conversion + /** payerAuthorized is set to true only when needed for batch Eth conversion */ bool internal payerAuthorized; + /** batchPayment function is the caller */ + bool internal batchPaymentOrigin; - // 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; + /** The amount of the batch fee cannot exceed a predefined amount in USD */ uint256 public batchFeeAmountUSDLimit; address public USDAddress; + address public ETHAddress; + address[][] public pathsEthToUSD; + /** Contain the address of a token, the sum of the amount and fees paid with it, and the batch fee amount */ struct Token { address tokenAddress; uint256 amountAndFee; uint256 batchFeeAmount; } - /** - * @dev BatchNoConversionPayments contract input structure. - */ - struct CryptoDetails { - address[] tokenAddresses; - address[] recipients; - uint256[] amounts; - bytes[] paymentReferences; - uint256[] feeAmounts; - } - /** * @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 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 acceptable times span for conversion rates, ignored if zero + * recipient: Recipient address of the payment + * requestAmount: Request amount, in fiat for conversion payment + * path: Only for conversion payment: the conversion path + * paymentReference: Unique reference of the payment + * feeAmount: The fee amount, denominated in the first currency of `path` for conversion payment + * maxToSpend: Only for conversion payment: + * Maximum amount the payer wants to spend, denominated in the last currency of `path`: + * it includes fee proxy but NOT the batchFee + * maxRateTimespan: Only for conversion payment: + * Max acceptable times span for conversion rates, ignored if zero */ struct ConversionDetail { address recipient; @@ -110,64 +107,66 @@ contract BatchNoConversionPayments is Ownable { /** * @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. + * @param conversionDetails List of ETH requests to pay. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @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 batchEthPayments( - address[] calldata _recipients, - uint256[] calldata _amounts, - bytes[] calldata _paymentReferences, - uint256[] calldata _feeAmounts, - address payable _feeAddress - ) public payable { - require( - _recipients.length == _amounts.length && - _recipients.length == _paymentReferences.length && - _recipients.length == _feeAmounts.length, - 'the input arrays must have the same length' - ); - + ConversionDetail[] calldata conversionDetails, + uint256 batchFeeAmountUSD, + address payable feeAddress + ) public payable returns (uint256) { + // Avoid the possibility to manually put high value to batchFeeAmountUSD + if (batchPaymentOrigin != true) { + batchFeeAmountUSD = 0; + } // 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]; - - paymentEthProxy.transferWithReferenceAndFee{value: _amounts[i] + _feeAmounts[i]}( - payable(_recipients[i]), - _paymentReferences[i], - _feeAmounts[i], - payable(_feeAddress) + for (uint256 i = 0; i < conversionDetails.length; i++) { + ConversionDetail memory cD = conversionDetails[i]; + require(address(this).balance >= cD.requestAmount + cD.feeAmount, 'Not enough funds'); + amount += cD.requestAmount; + + paymentEthProxy.transferWithReferenceAndFee{value: cD.requestAmount + cD.feeAmount}( + payable(cD.recipient), + cD.paymentReference, + cD.feeAmount, + payable(feeAddress) ); } // amount is updated into batch fee amount - amount = (amount * batchFee) / tenThousand; + amount = (amount * batchFee) / feeDenominator; + (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( + amount, + pathsEthToUSD[0][0], + batchFeeAmountUSD, + pathsEthToUSD + ); // Check that batch contract has enough funds to pay batch fee - require(address(this).balance >= amount, 'not enough funds for batch fee'); + require(address(this).balance >= amount, 'Not enough funds for batch fee'); // Batch pays batch fee - _feeAddress.transfer(amount); + feeAddress.transfer(amount); // Batch contract transfers the remaining ethers to the payer 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'); } + return batchFeeAmountUSD; } /** * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. - * @param conversionDetails TODO - * @param pathsToUSD The list containing the path of the token into USD - * @param batchFeeAmountUSD The batchFeeAmountUSD already paid - * @param _feeAddress The fee recipient. + * @param conversionDetails List of ERC20 requests to pay, with only one ERC20 token. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not limitation. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param feeAddress The fee recipient. * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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. @@ -176,50 +175,54 @@ contract BatchNoConversionPayments is Ownable { ConversionDetail[] calldata conversionDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, - address _feeAddress + address feeAddress ) public returns (uint256) { - // amount is used to get the total amount and fee, and then used as batch fee amount + // Avoid the possibility to manually put high value to batchFeeAmountUSD + if (batchPaymentOrigin != true) { + batchFeeAmountUSD = 0; + } + // amount is used to get the total amount, and then used as batch fee amount uint256 amount = 0; + uint256 amountAndFee = 0; for (uint256 i = 0; i < conversionDetails.length; i++) { - amount += conversionDetails[i].requestAmount + conversionDetails[i].feeAmount; + amount += conversionDetails[i].requestAmount; + amountAndFee += conversionDetails[i].requestAmount + conversionDetails[i].feeAmount; } // Transfer the amount and fee from the payer to the batch contract IERC20 requestedToken = IERC20(conversionDetails[0].path[0]); require( - requestedToken.allowance(msg.sender, address(this)) >= amount, + requestedToken.allowance(msg.sender, address(this)) >= amountAndFee, 'Insufficient allowance for batch to pay' ); - require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); + require(requestedToken.balanceOf(msg.sender) >= amountAndFee, 'Not enough funds'); require( - safeTransferFrom(conversionDetails[0].path[0], address(this), amount), + safeTransferFrom(conversionDetails[0].path[0], address(this), amountAndFee), 'payment transferFrom() failed' ); // Batch contract approve Erc20FeeProxy to spend the token - if (requestedToken.allowance(address(this), address(paymentErc20Proxy)) < amount) { + if (requestedToken.allowance(address(this), address(paymentErc20Proxy)) < amountAndFee) { approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); } // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < conversionDetails.length; i++) { ConversionDetail memory cD = conversionDetails[i]; - // amount is updated to become the sum of amounts, to calculate batch fee amount - amount -= cD.feeAmount; paymentErc20Proxy.transferFromWithReferenceAndFee( cD.path[0], cD.recipient, cD.requestAmount, cD.paymentReference, cD.feeAmount, - _feeAddress + feeAddress ); } // amount is updated into batch fee amount - amount = (amount * batchFee) / tenThousand; + amount = (amount * batchFee) / feeDenominator; // Check if the payer has enough funds to pay batch fee - require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds for the batch fee'); + require(requestedToken.balanceOf(msg.sender) >= amount, 'Not enough funds for the batch fee'); // Payer pays batch fee amount // amount that represents batchFeeToPay updated if needed @@ -230,8 +233,8 @@ contract BatchNoConversionPayments is Ownable { pathsToUSD ); require( - safeTransferFrom(conversionDetails[0].path[0], _feeAddress, amount), - 'batch fee transferFrom() failed' + safeTransferFrom(conversionDetails[0].path[0], feeAddress, amount), + 'Batch fee transferFrom() failed' ); return batchFeeAmountUSD; @@ -239,10 +242,11 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. - * @param conversionDetails It contains payments information: - * @param pathsToUSD The list of paths into USD for every token - * @param batchFeeAmountUSD The batchFeeAmountUSD already paid - * @param _feeAddress The fee recipient. + * @param conversionDetails List of ERC20 requests to pay. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not limitation. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param feeAddress The fee recipient. * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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. @@ -251,27 +255,28 @@ contract BatchNoConversionPayments is Ownable { ConversionDetail[] calldata conversionDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, - address _feeAddress + address feeAddress ) public returns (uint256) { - // 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 + // Avoid the possibility to manually put high value to batchFeeAmountUSD + if (batchPaymentOrigin != true) { + batchFeeAmountUSD = 0; + } Token[] memory uTokens = getUTokens(conversionDetails); // The payer transfers tokens to the batch contract and pays batch fee 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); + uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / feeDenominator; require( requestedToken.allowance(msg.sender, address(this)) >= 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 + // Check if the payer can pay the amount, the fee, and the batchFee require( requestedToken.balanceOf(msg.sender) >= uTokens[i].amountAndFee + uTokens[i].batchFeeAmount, - 'not enough funds' + 'Not enough funds' ); // Transfer only the amount and fee required for the token on the batch contract @@ -300,8 +305,8 @@ contract BatchNoConversionPayments is Ownable { ); require( - safeTransferFrom(uTokens[i].tokenAddress, _feeAddress, batchFeeToPay), - 'batch fee transferFrom() failed' + safeTransferFrom(uTokens[i].tokenAddress, feeAddress, batchFeeToPay), + 'Batch fee transferFrom() failed' ); } @@ -314,53 +319,58 @@ contract BatchNoConversionPayments is Ownable { cD.requestAmount, cD.paymentReference, cD.feeAmount, - _feeAddress + feeAddress ); } return batchFeeAmountUSD; } + /* + * Helper functions + */ + + /** + * It create a list of unique tokens used and the amounts associated. + * It only considers tokens having: requestAmount + feeAmount > 0. + * Regarding ERC20 no conversion payments: + * batchFeeAmount is the sum of requestAmount and feeAmount. + * Out of the function, batch fee rate is applied + * @param conversionDetails List of requests to pay. + */ function getUTokens(ConversionDetail[] calldata conversionDetails) internal pure returns (Token[] memory uTokens) { - // a list of unique tokens, with the sum of maxToSpend by token + // A list of unique tokens, with the sum of maxToSpend by token uTokens = new Token[](conversionDetails.length); for (uint256 i = 0; i < conversionDetails.length; i++) { for (uint256 k = 0; k < conversionDetails.length; k++) { + ConversionDetail memory cD = conversionDetails[i]; // If the token is already in the existing uTokens list - if ( - uTokens[k].tokenAddress == conversionDetails[i].path[conversionDetails[i].path.length - 1] - ) { - if (conversionDetails[i].path.length > 1) { - uTokens[k].amountAndFee += conversionDetails[i].maxToSpend; + if (uTokens[k].tokenAddress == cD.path[cD.path.length - 1]) { + if (cD.path.length > 1) { + uTokens[k].amountAndFee += cD.maxToSpend; } else { - // it is not a conversion payment - uTokens[k].amountAndFee += - conversionDetails[i].requestAmount + - conversionDetails[i].feeAmount; - uTokens[k].batchFeeAmount += conversionDetails[i].requestAmount; + // It is not a conversion payment + uTokens[k].amountAndFee += cD.requestAmount + cD.feeAmount; + uTokens[k].batchFeeAmount += cD.requestAmount; } break; } // If the token is not in the list (amountAndFee = 0) else if ( - uTokens[k].amountAndFee == 0 && - (conversionDetails[i].maxToSpend > 0 || - conversionDetails[i].requestAmount + conversionDetails[i].feeAmount > 0) + uTokens[k].amountAndFee == 0 && (cD.maxToSpend > 0 || cD.requestAmount + cD.feeAmount > 0) ) { - uTokens[k].tokenAddress = conversionDetails[i].path[conversionDetails[i].path.length - 1]; + uTokens[k].tokenAddress = cD.path[cD.path.length - 1]; - if (conversionDetails[i].path.length > 1) { + if (cD.path.length > 1) { // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract - uTokens[k].amountAndFee = conversionDetails[i].maxToSpend; + uTokens[k].amountAndFee = cD.maxToSpend; } else { - // it is not a conversion payment - uTokens[k].amountAndFee = - conversionDetails[i].requestAmount + - conversionDetails[i].feeAmount; - uTokens[k].batchFeeAmount = conversionDetails[i].requestAmount; + // It is not a conversion payment + uTokens[k].amountAndFee = cD.requestAmount + cD.feeAmount; + uTokens[k].batchFeeAmount = cD.requestAmount; } break; } @@ -368,14 +378,31 @@ contract BatchNoConversionPayments is Ownable { } } + /** + * Calculate the batch fee amount to pay, using the USD fee limitation. + * Without pathsToUSD or a wrong one, the fee limitation is not applied. + * @param batchFeeToPay The amount of batch fee to pay + * @param tokenAddress The address of the token + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not limitation. + */ function calculateBatchFeeToPay( uint256 batchFeeToPay, address tokenAddress, uint256 batchFeeAmountUSD, - address[][] calldata pathsToUSD + address[][] memory pathsToUSD ) internal view returns (uint256, uint256) { - if (pathsToUSD.length > 0) { + // Fees are not limited if there is no pathsToUSD + // Excepted if batchFeeAmountUSD is already >= batchFeeAmountUSDLimit + if (pathsToUSD.length == 0 && batchFeeAmountUSD < batchFeeAmountUSDLimit) { + return (batchFeeToPay, batchFeeAmountUSD); + } + + // Apply the fee limit and calculate if needed batchFeeToPay + if (batchFeeAmountUSD < batchFeeAmountUSDLimit) { for (uint256 i = 0; i < pathsToUSD.length; i++) { + // Check if the pathToUSD is right if ( pathsToUSD[i][0] == tokenAddress && pathsToUSD[i][pathsToUSD[i].length - 1] == USDAddress ) { @@ -394,14 +421,12 @@ contract BatchNoConversionPayments is Ownable { break; } } + } else { + batchFeeToPay = 0; } return (batchFeeToPay, batchFeeAmountUSD); } - /* - * Helper functions - */ - /** * @notice Authorizes the proxy to spend a new request currency (ERC20). * @param _erc20Address Address of an ERC20 used as the request currency. @@ -485,10 +510,21 @@ contract BatchNoConversionPayments is Ownable { paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); } - function setUSDAddress(address _USDAddress) external onlyOwner { + /** + * This function define variables allowing to limit the fees: + * ETHAddress, USDAddress, and pathsEthToUSD + * @param _ETHAddress The address representing the Ethereum currency. + * @param _USDAddress The address representing the USD currency. + */ + function setETHAndUSDAddress(address _ETHAddress, address _USDAddress) external onlyOwner { + ETHAddress = _ETHAddress; USDAddress = _USDAddress; + pathsEthToUSD = [[ETHAddress, USDAddress]]; } + /** + * @param _batchFeeAmountUSDLimit The limitation of the batch fee amount in USD. + */ function setBatchFeeAmountUSDLimit(uint256 _batchFeeAmountUSDLimit) external onlyOwner { batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; } diff --git a/packages/smart-contracts/src/contracts/BatchPayments.sol b/packages/smart-contracts/src/contracts/BatchPayments.sol index 0947637f5f..df303d115d 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -97,7 +97,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { // amount is updated into batch fee amount amount = (amount * batchFee) / 1000; // Check that batch contract has enough funds to pay batch fee - require(address(this).balance >= amount, 'not enough funds for batch fee'); + require(address(this).balance >= amount, 'Not enough funds for batch fee'); // Batch pays batch fee _feeAddress.transfer(amount); @@ -147,7 +147,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { requestedToken.allowance(msg.sender, address(this)) >= amount, 'Not sufficient allowance for batch to pay' ); - require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); + require(requestedToken.balanceOf(msg.sender) >= amount, 'Not enough funds'); require( safeTransferFrom(_tokenAddress, address(this), amount), 'payment transferFrom() failed' 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 365cfa0f1a..9679b3e0a5 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 @@ -57,7 +57,7 @@ }, { "inputs": [], - "name": "USDAddress", + "name": "ETHAddress", "outputs": [ { "internalType": "address", @@ -70,12 +70,12 @@ }, { "inputs": [], - "name": "batchConversionFee", + "name": "USDAddress", "outputs": [ { - "internalType": "uint256", + "internalType": "address", "name": "", - "type": "uint256" + "type": "address" } ], "stateMutability": "view", @@ -84,38 +84,71 @@ { "inputs": [ { - "internalType": "address", - "name": "_tokenAddress", - "type": "address" - }, - { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_amounts", - "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 BatchNoConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" }, { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" + "internalType": "uint256", + "name": "batchFeeAmountUSD", + "type": "uint256" }, { "internalType": "address", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], "name": "batchERC20Payments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, @@ -163,47 +196,91 @@ "name": "conversionDetails", "type": "tuple[]" }, + { + "internalType": "uint256", + "name": "batchFeeAmountUSD", + "type": "uint256" + }, { "internalType": "address payable", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], "name": "batchEthConversionPayments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "payable", "type": "function" }, { "inputs": [ { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_amounts", - "type": "uint256[]" - }, - { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" + "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 BatchNoConversionPayments.ConversionDetail[]", + "name": "conversionDetails", + "type": "tuple[]" }, { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" + "internalType": "uint256", + "name": "batchFeeAmountUSD", + "type": "uint256" }, { "internalType": "address payable", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], "name": "batchEthPayments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "payable", "type": "function" }, @@ -289,7 +366,7 @@ }, { "internalType": "address", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], @@ -360,7 +437,7 @@ }, { "internalType": "address", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], @@ -425,38 +502,6 @@ "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", "name": "conversionDetails", "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 BatchNoConversionPayments.CryptoDetails", - "name": "cryptoDetails", - "type": "tuple" } ], "internalType": "struct BatchConversionPayments.MetaDetail[]", @@ -470,7 +515,7 @@ }, { "internalType": "address", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], @@ -505,6 +550,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "pathsEthToUSD", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "paymentErc20ConversionProxy", @@ -564,19 +633,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_batchConversionFee", - "type": "uint256" - } - ], - "name": "setBatchConversionFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -620,11 +676,16 @@ "inputs": [ { "internalType": "address", - "name": "_paymentErc20ConversionProxy", + "name": "_ETHAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_USDAddress", "type": "address" } ], - "name": "setPaymentErc20ConversionProxy", + "name": "setETHAndUSDAddress", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -633,11 +694,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentErc20Proxy", + "name": "_paymentErc20ConversionProxy", "type": "address" } ], - "name": "setPaymentErc20Proxy", + "name": "setPaymentErc20ConversionProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -646,11 +707,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthConversionProxy", + "name": "_paymentErc20Proxy", "type": "address" } ], - "name": "setPaymentEthConversionProxy", + "name": "setPaymentErc20Proxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -659,11 +720,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthProxy", + "name": "_paymentEthConversionProxy", "type": "address" } ], - "name": "setPaymentEthProxy", + "name": "setPaymentEthConversionProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -672,11 +733,11 @@ "inputs": [ { "internalType": "address", - "name": "_USDAddress", + "name": "_paymentEthProxy", "type": "address" } ], - "name": "setUSDAddress", + "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 9485a0ed3a..272bebdaae 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -40,8 +40,7 @@ describe('contract: BatchConversionPayments', async () => { let tx: ContractTransaction; // constants used to set up batch conversion proxy, and also requests payment - const BATCH_FEE = 50; // .5% - const BATCH_CONV_FEE = 100; // 1% + const BATCH_FEE = 100; // 1% const BATCH_DENOMINATOR = 10000; const daiDecimals = '1000000000000000000'; // 10^18 const fiatDecimals = '00000000'; @@ -65,15 +64,7 @@ describe('contract: BatchConversionPayments', async () => { let fauERC20: TestERC20; let chainlinkPath: ChainlinkConversionPath; - // constants inputs for batch functions, both conversion and no-conversion - const emptyCryptoDetails: PaymentTypes.CryptoDetails = { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }; - + // constants inputs for batch conversion functions const fauConvDetail: PaymentTypes.ConversionDetail = { recipient: '', requestAmount: '100000' + fiatDecimals, @@ -138,8 +129,7 @@ describe('contract: BatchConversionPayments', async () => { // set batch proxy fees and connect fromSigner await batchConversionProxy.setBatchFee(BATCH_FEE); - await batchConversionProxy.setBatchConversionFee(BATCH_CONV_FEE); - await batchConversionProxy.setUSDAddress(currencyManager.fromSymbol('USD')!.hash); + await batchConversionProxy.setETHAndUSDAddress(ETH_hash, USD_hash); await batchConversionProxy.setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); @@ -188,7 +178,7 @@ describe('contract: BatchConversionPayments', async () => { // fee added by the batch expectedToBalanceDiff .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)) - .mul(BATCH_CONV_FEE) + .mul(BATCH_FEE) .div(BATCH_DENOMINATOR) // fee within the invoice: .1% of the amount, .add(BigNumber.from(fee).mul(conversionRate).mul(nPayment)); @@ -247,10 +237,11 @@ describe('contract: BatchConversionPayments', async () => { const expectETHBalanceDiffs = async ( ethAmount: BigNumber, ethFeeAmount: BigNumber, - feeApplied = BATCH_CONV_FEE, initialFromETHBalance: BigNumber, initialToETHBalance: BigNumber, initialFeeETHBalance: BigNumber, + isConversion = true, + forceExpectedFeeETHBalanceDiff?: BigNumber, ) => { const receipt = await tx.wait(); const gasAmount = receipt.gasUsed.mul(gasPrice); @@ -266,19 +257,26 @@ describe('contract: BatchConversionPayments', async () => { const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); const expectedToETHBalanceDiff = ethAmount; - const expectedFeeETHBalanceDiff = expectedToETHBalanceDiff - .add(ethFeeAmount) - .mul(feeApplied) - .div(BATCH_DENOMINATOR) - .add(ethFeeAmount); + + const expectedFeeETHBalanceDiff = + forceExpectedFeeETHBalanceDiff ?? + expectedToETHBalanceDiff + .add(isConversion ? ethFeeAmount : 0) + .mul(BATCH_FEE) + .div(BATCH_DENOMINATOR) + .add(ethFeeAmount); const expectedFromETHBalanceDiff = gasAmount .add(expectedToETHBalanceDiff) .add(expectedFeeETHBalanceDiff); // Check balance changes - expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + console.log('to'); expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); + console.log('fee:', feeETHBalanceDiff.toString()); expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + console.log('from'); + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + console.log('contract'); expect(batchETHBalance).to.equals('0', 'batchETHBalance'); }; @@ -341,7 +339,6 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], @@ -369,7 +366,6 @@ describe('contract: BatchConversionPayments', async () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], - cryptoDetails: emptyCryptoDetails, }, ], [ @@ -392,14 +388,17 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: [], - recipients: [to], - amounts: ['1000'], - paymentReferences: [referenceExample], - feeAmounts: ['1'], - }, + conversionDetails: [ + { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], }, ], [], @@ -410,10 +409,10 @@ describe('contract: BatchConversionPayments', async () => { await expectETHBalanceDiffs( BigNumber.from(1000), BigNumber.from(1), - BATCH_FEE, initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, + false, ); }); @@ -427,7 +426,6 @@ describe('contract: BatchConversionPayments', async () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, conversionDetails: [ethConvDetail], - cryptoDetails: emptyCryptoDetails, }, ], [], @@ -440,7 +438,6 @@ describe('contract: BatchConversionPayments', async () => { await expectETHBalanceDiffs( BigNumber.from(1000 * USD_ETH_RATE), BigNumber.from(1 * USD_ETH_RATE), - BATCH_CONV_FEE, initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -456,12 +453,14 @@ describe('contract: BatchConversionPayments', async () => { const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const ethCryptoDetails: PaymentTypes.CryptoDetails = { - tokenAddresses: [], - recipients: [to], - amounts: ['1000'], - paymentReferences: [referenceExample], - feeAmounts: ['1'], + const ethCryptoDetail: PaymentTypes.ConversionDetail = { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', }; tx = await batchConversionProxy.batchRouter( @@ -469,7 +468,6 @@ describe('contract: BatchConversionPayments', async () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, conversionDetails: [fauConvDetail], - cryptoDetails: emptyCryptoDetails, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, @@ -484,17 +482,14 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - cryptoDetails: emptyCryptoDetails, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [], - cryptoDetails: ethCryptoDetails, + conversionDetails: [ethCryptoDetail], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, conversionDetails: [ethConvDetail], - cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], @@ -541,7 +536,7 @@ describe('contract: BatchConversionPayments', async () => { // Batch conversion BigNumber.from(1000 * USD_ETH_RATE) .add(1 * USD_ETH_RATE) - .mul(BATCH_CONV_FEE) + .mul(BATCH_FEE) .div(BATCH_DENOMINATOR) // Batch no-conversion .add(1 * USD_ETH_RATE) @@ -573,7 +568,7 @@ describe('contract: BatchConversionPayments', async () => { .connect(adminSigner) .setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); // 100_000 $ }); - it.only(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { + it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); await batchConversionProxy.batchRouter( @@ -591,7 +586,6 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], @@ -636,7 +630,6 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], @@ -681,19 +674,17 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - cryptoDetails: emptyCryptoDetails, }, ], - [[DAI_address, USD_hash]], + [[DAI_address, USD_hash]], // it should be FAU_address feeAddress, ); - // check the balance fauERC20 token let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedERC20Balances('10100' + daiDecimals, '0' + daiDecimals, 1); // around 1001$ of batch fees in FAU - the fee limit is not applied because of the wrong path - expectedFeeFAUBalanceDiff = BigNumber.from('506005000000000000000'); + expectedFeeFAUBalanceDiff = BigNumber.from('1011010000000000000000'); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) .mul(-1); @@ -708,23 +699,18 @@ describe('contract: BatchConversionPayments', async () => { expectedFeeFAUBalanceDiff, ); }); - it('make 2 ERC20 payments with conversion with limited fees', async () => { + it('make 2 ERC20 payments on two tokens with conversion and only FAU token pays batch fee', async () => { // get balances const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = await getERC20Balances(daiERC20); + tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail], - cryptoDetails: emptyCryptoDetails, - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [daiConvDetail], - cryptoDetails: emptyCryptoDetails, + conversionDetails: [fauConvDetail, daiConvDetail], }, ], [ @@ -758,7 +744,7 @@ describe('contract: BatchConversionPayments', async () => { // Check DAI Balances // let [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); - console.log('JJJ'); + // 0€ of batch fees converted in DAI, and 100€ of fees converted in DAI. // instead of (1000€ + 1€) + 100€ of fees without the limit expectedFeeDAIBalanceDiff = BigNumber.from('118811881188118811881'); @@ -776,6 +762,204 @@ describe('contract: BatchConversionPayments', async () => { expectedFeeDAIBalanceDiff, ); }); + it('make 1 ETH payment without conversion', async () => { + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 1 $ + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const USDReqAmount = 200000000000; + const reqAmount = BigNumber.from(USDReqAmount).mul(USD_ETH_RATE).toString(); + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: reqAmount, + path: [], + paymentReference: referenceExample, + feeAmount: '2000', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + }, + ], + [], + feeAddress, + { value: BigNumber.from(reqAmount).mul(2) }, + ); + + await expectETHBalanceDiffs( + BigNumber.from(reqAmount), + BigNumber.from('2000'), + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + false, + BigNumber.from('20000000000000000').add('2000'), // batch fees limited + invoice fees + ); + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); + }); + it('make 1 ETH payment with 1-step conversion', async () => { + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10 $ + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const reqAmount = 200000000000; + const increasedEthConvDetail = Utils.deepCopy(ethConvDetail); + increasedEthConvDetail.requestAmount = reqAmount.toString(); + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [increasedEthConvDetail], + }, + ], + [], + feeAddress, + { + value: BigNumber.from(2 * reqAmount + 1) + .mul(USD_ETH_RATE) + .toString(), + }, + ); + + await expectETHBalanceDiffs( + BigNumber.from(reqAmount).mul(USD_ETH_RATE), + BigNumber.from(1).mul(USD_ETH_RATE), + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + true, + BigNumber.from('20000000020100000'), // equal the sum of the batch fee (10$) and the invoice fee + ); + await batchConversionProxy + .connect(adminSigner) + .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); // 1000 $ + }); + it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + // set inputs: ERC20 cryptoDetails & ethCryptoDetails + const ethCryptoDetail: PaymentTypes.ConversionDetail = { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }; + + tx = await batchConversionProxy.batchRouter( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + conversionDetails: [fauConvDetail], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + conversionDetails: [ + { + recipient: to, + requestAmount: '100000', + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + conversionDetails: [ethCryptoDetail], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + conversionDetails: [ethConvDetail], + }, + ], + [[FAU_address, USD_hash]], + feeAddress, + { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees + ); + ethCryptoDetail; + ethConvDetail; + // Chech FAU Balances // + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + const [ + _noConvExpectedFromFAUBalanceDiff, + noConvExpectedToFAUBalanceDiff, + _noConvExpectedFeeFAUBalanceDiff, + ] = getExpectedERC20Balances('100000', '100', 1); + + expectedFeeFAUBalanceDiff = BigNumber.from('547263681597009955318'); // 1000$ of batch fee and the invoice fees + expectedToFAUBalanceDiff = expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + + // Check ETH balances // + const receipt = await tx.wait(); + const gasAmount = receipt.gasUsed.mul(gasPrice); + + const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const toETHBalance = await provider.getBalance(to); + const feeETHBalance = await provider.getBalance(feeAddress); + const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - initial + const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); + const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); + const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + + // expectedFeeETHBalanceDiff => batch conversion fees = 0 + const expectedFeeETHBalanceDiff = BigNumber.from(1 * USD_ETH_RATE).add(1); + + const expectedFromETHBalanceDiff = gasAmount + .add(1000 * USD_ETH_RATE + 1000) + .add(expectedFeeETHBalanceDiff) + .mul(-1); + + // Check balance changes + console.log('ETH'); + console.log('to'); + expect(toETHBalanceDiff).to.equals( + BigNumber.from(1000 * USD_ETH_RATE + 1000), + 'toETHBalanceDiff', + ); + console.log('fee'); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + console.log('from'); + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + }); }); }); describe('batchRouter errors', async () => { @@ -785,7 +969,6 @@ describe('contract: BatchConversionPayments', async () => { Array(6).fill({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, conversionDetails: [], - cryptoDetails: emptyCryptoDetails, }), [[FAU_address, USD_hash]], feeAddress, @@ -799,13 +982,12 @@ describe('contract: BatchConversionPayments', async () => { { paymentNetworkId: 6, conversionDetails: [], - cryptoDetails: emptyCryptoDetails, }, ], [[FAU_address, USD_hash]], feeAddress, ), - ).to.be.revertedWith('wrong paymentNetworkId'); + ).to.be.revertedWith('Wrong paymentNetworkId'); }); }); describe('batchMultiERC20ConversionPayments', async () => { @@ -957,7 +1139,7 @@ describe('contract: BatchConversionPayments', async () => { [[FAU_address, USD_hash]], feeAddress, ), - ).to.be.revertedWith('not enough funds, including fees'); + ).to.be.revertedWith('Not enough funds, including fees'); // signer4 transfer token to fromSigner await fauERC20 @@ -971,13 +1153,12 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], feeAddress, { + tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], 0, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }); await expectETHBalanceDiffs( BigNumber.from(1000 * USD_ETH_RATE), BigNumber.from(1 * USD_ETH_RATE), - BATCH_CONV_FEE, initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -994,6 +1175,7 @@ describe('contract: BatchConversionPayments', async () => { tx = await batchConversionProxy.batchEthConversionPayments( [ethConvDetail, EurConvDetail, ethConvDetail], + 0, feeAddress, { value: BigNumber.from('100000000000000000'), @@ -1004,7 +1186,6 @@ describe('contract: BatchConversionPayments', async () => { .mul(2) .add(1000 * 24000000), // 24000000 is EUR_ETH_RATE BigNumber.from(USD_ETH_RATE).mul(2).add(24000000), - BATCH_CONV_FEE, initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -1016,7 +1197,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvDetail = Utils.deepCopy(ethConvDetail); wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], 0, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }), ).to.be.revertedWith('No aggregator found'); @@ -1025,6 +1206,7 @@ describe('contract: BatchConversionPayments', async () => { await expect( batchConversionProxy.batchEthConversionPayments( [ethConvDetail, ethConvDetail], + 0, feeAddress, { value: (2000 + 1) * USD_ETH_RATE, // no enough to pay the amount AND the fees @@ -1037,7 +1219,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvDetail = Utils.deepCopy(ethConvDetail); wrongConvDetail.maxRateTimespan = '1'; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvDetail], feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvDetail], 0, feeAddress, { value: 1000 + 1 + 11, // + 11 to pay batch fees }), ).to.be.revertedWith('aggregator rate is outdated'); @@ -1117,17 +1299,24 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchEthPayments( - [to], - ['1000'], - [referenceExample], - ['1'], + [ + { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], + 0, feeAddress, { value: 1000 + 1 + 11 }, // + 11 to pay batch fees ); await expectETHBalanceDiffs( BigNumber.from(1000), BigNumber.from(1), - BATCH_FEE, initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index c9e35355b1..7cb8b7934f 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -590,7 +590,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batch .connect(spender3) .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), - ).revertedWith('not enough funds'); + ).revertedWith('Not enough funds'); }); it('Should revert batch if not enough funds to pay the batch fee', async () => { @@ -608,7 +608,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { 0, feeAddress, ), - ).revertedWith('not enough funds for the batch fee'); + ).revertedWith('Not enough funds for the batch fee'); }); it('Should revert batch without approval', async () => { @@ -631,7 +631,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batch .connect(spender3) .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), - ).revertedWith('not enough funds'); + ).revertedWith('Not enough funds'); }); it('Should revert batch multi tokens if not enough funds to pay the batch fee', async () => { @@ -646,7 +646,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batch .connect(spender3) .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), - ).revertedWith('not enough funds'); + ).revertedWith('Not enough funds'); }); it('Should revert batch multi tokens without approval', async () => { diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 508e0ea383..b781b84b3a 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -1,6 +1,7 @@ import { ethers, network } from 'hardhat'; import { BigNumber, Signer } from 'ethers'; import { expect } from 'chai'; +import Utils from '@requestnetwork/utils'; import { EthereumFeeProxy__factory, BatchNoConversionPayments__factory, @@ -10,6 +11,8 @@ import { import { EthereumFeeProxy, BatchNoConversionPayments } from '../../src/types'; import { chainlinkConversionPath } from '../../src/lib'; import { HttpNetworkConfig } from 'hardhat/types'; +import { PaymentTypes } from 'types/dist'; +import { CurrencyManager } from '@requestnetwork/currency'; const logGasInfos = false; @@ -35,6 +38,26 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { let batch: BatchNoConversionPayments; const networkConfig = network.config as HttpNetworkConfig; const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); + const currencyManager = CurrencyManager.getDefault(); + + const ethConvDetail1: PaymentTypes.ConversionDetail = { + recipient: '', + requestAmount: '200', + path: [], + paymentReference: referenceExample1, + feeAmount: '10', + maxToSpend: '0', + maxRateTimespan: '0', + }; + const ethConvDetail2: PaymentTypes.ConversionDetail = { + recipient: '', + requestAmount: '300', + path: [], + paymentReference: referenceExample2, + feeAmount: '20', + maxToSpend: '0', + maxRateTimespan: '0', + }; before(async () => { [, payee1, payee2, feeAddress] = (await ethers.getSigners()).map((s) => s.address); @@ -51,6 +74,13 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { ); batchAddress = batch.address; await batch.connect(owner).setBatchFee(100); + await batch.setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); // 1$ + await batch.setETHAndUSDAddress( + currencyManager.fromSymbol('ETH')!.hash, + currencyManager.fromSymbol('USD')!.hash, + ); + ethConvDetail1.recipient = payee1; + ethConvDetail2.recipient = payee2; }); describe('Batch Eth normal flow', () => { @@ -59,19 +89,19 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); + const copyEthConvDetail1 = Utils.deepCopy(ethConvDetail1); + copyEthConvDetail1.requestAmount = '2000'; + copyEthConvDetail1.feeAmount = '100'; + + const copyEthConvDetail2 = Utils.deepCopy(ethConvDetail2); + copyEthConvDetail2.requestAmount = '3000'; + copyEthConvDetail2.feeAmount = '200'; await expect( batch .connect(owner) - .batchEthPayments( - [payee1, payee2], - [2000, 3000], - [referenceExample1, referenceExample2], - [100, 200], - feeAddress, - { - value: BigNumber.from('6000'), - }, - ), + .batchEthPayments([copyEthConvDetail1, copyEthConvDetail2], 0, feeAddress, { + value: BigNumber.from('6000'), + }), ) .to.emit(ethFeeProxy, 'TransferWithReferenceAndFee') .withArgs(payee1, '2000', ethers.utils.keccak256(referenceExample1), '100', feeAddress) @@ -94,20 +124,13 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); - const totalAmount = BigNumber.from('535'); // amount: 500, fee: 10+20, batchFee: 2+3 + const totalAmount = BigNumber.from('535'); // amount: 200+300, fee: 10+20, batchFee: 2+3 const tx = await batch .connect(owner) - .batchEthPayments( - [payee1, payee2], - [200, 300], - [referenceExample1, referenceExample2], - [10, 20], - feeAddress, - { - value: totalAmount, - }, - ); + .batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + value: totalAmount, + }); await tx.wait(); afterEthBalance1 = await provider.getBalance(payee1); @@ -125,19 +148,15 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const amount = 2; const feeAmount = 1; const nbTxs = 10; // to compare gas optim, go to 100. - const [_, recipients, amounts, paymentReferences, feeAmounts] = getBatchPaymentsInputs( - nbTxs, - '_noTokenAddress', - payee2, - amount, - referenceExample1, - feeAmount, - ); + + const copyEthConvDetail = Utils.deepCopy(ethConvDetail2); + copyEthConvDetail.requestAmount = amount.toString(); + copyEthConvDetail.feeAmount = feeAmount.toString(); const totalAmount = BigNumber.from(((amount + feeAmount) * nbTxs).toString()); const tx = await batch .connect(owner) - .batchEthPayments(recipients, amounts, paymentReferences, feeAmounts, feeAddress, { + .batchEthPayments(Array(nbTxs).fill(copyEthConvDetail), 0, feeAddress, { value: totalAmount, }); @@ -161,19 +180,10 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const totalAmount = BigNumber.from('400'); await expect( - batch - .connect(owner) - .batchEthPayments( - [payee1, payee2], - [200, 300], - [referenceExample1, referenceExample2], - [10, 20], - feeAddress, - { - value: totalAmount, - }, - ), - ).revertedWith('not enough funds'); + batch.connect(owner).batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + value: totalAmount, + }), + ).revertedWith('Not enough funds'); afterEthBalance1 = await provider.getBalance(payee1); expect(afterEthBalance1).to.be.equal(beforeEthBalance1); @@ -191,19 +201,10 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const totalAmount = BigNumber.from('530'); // missing 5 (= (200+300) * 1%) await expect( - batch - .connect(owner) - .batchEthPayments( - [payee1, payee2], - [200, 300], - [referenceExample1, referenceExample2], - [10, 20], - feeAddress, - { - value: totalAmount, - }, - ), - ).revertedWith('not enough funds for batch fee'); + batch.connect(owner).batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + value: totalAmount, + }), + ).revertedWith('Not enough funds for batch fee'); afterEthBalance1 = await provider.getBalance(payee1); expect(afterEthBalance1).to.be.equal(beforeEthBalance1); @@ -213,52 +214,6 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { expect(await provider.getBalance(batchAddress)).to.be.equal(0); }); - - it('Should revert batch if input s arrays do not have same size', async () => { - await expect( - batch - .connect(owner) - .batchEthPayments( - [payee1, payee2], - [5, 30], - [referenceExample1, referenceExample2], - [1], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(owner) - .batchEthPayments( - [payee1], - [5, 30], - [referenceExample1, referenceExample2], - [1, 2], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(owner) - .batchEthPayments( - [payee1, payee2], - [5], - [referenceExample1, referenceExample2], - [1, 2], - feeAddress, - ), - ).revertedWith('the input arrays must have the same length'); - - await expect( - batch - .connect(owner) - .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); - }); }); describe('Function allowed only to the owner', () => { @@ -276,16 +231,9 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments( - [payee1, payee2], - [200, 300], - [referenceExample1, referenceExample2], - [10, 20], - feeAddress, - { - value: BigNumber.from('1000'), - }, - ); + .batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + value: BigNumber.from('1000'), + }); await tx.wait(); const afterFeeAddress = await provider.getBalance(feeAddress); @@ -299,28 +247,3 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { }); }); }); - -// Allow to create easly batchNoConversionPayments input, especially for gas optimization. -const getBatchPaymentsInputs = function ( - nbTxs: number, - tokenAddress: string, - recipient: string, - amount: number, - referenceExample1: string, - feeAmount: number, -): [Array, Array, Array, Array, Array] { - let tokenAddresses = []; - let recipients = []; - let amounts = []; - let paymentReferences = []; - let feeAmounts = []; - - for (let i = 0; i < nbTxs; i++) { - tokenAddresses.push(tokenAddress); - recipients.push(recipient); - amounts.push(amount); - paymentReferences.push(referenceExample1); - feeAmounts.push(feeAmount); - } - return [tokenAddresses, recipients, amounts, paymentReferences, feeAmounts]; -}; From 69d76a7de6050338931fe64499609a046dd119b4 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 10:34:58 +0200 Subject: [PATCH 099/138] refacto smart contract and optimize gas --- .../src/contracts/BatchConversionPayments.sol | 36 ++--- .../contracts/BatchNoConversionPayments.sol | 131 ++++++++++-------- .../BatchNoConversionErc20Payments.test.ts | 4 +- 3 files changed, 83 insertions(+), 88 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 6520898f06..e7ca519c35 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -82,10 +82,13 @@ contract BatchConversionPayments is BatchNoConversionPayments { address feeAddress ) external payable { require(metaDetails.length < 6, 'more than 5 metaDetails'); - if (pathsToUSD.length > 0) { + + // Check that there are paths to USD, and more than one paymentNetworkId + if (pathsToUSD.length > 0 && metaDetails.length > 1) { // Set to true to limit the batch fee to pay batchPaymentOrigin = true; } + uint256 batchFeeAmountUSD = 0; for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaConversionDetail = metaDetails[i]; @@ -133,7 +136,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { revert('Wrong paymentNetworkId'); } } - if (pathsToUSD.length > 0) { + if (pathsToUSD.length > 0 && metaDetails.length > 1) { batchPaymentOrigin = false; } } @@ -162,31 +165,14 @@ contract BatchConversionPayments is BatchNoConversionPayments { 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++) { - requestedToken = IERC20(uTokens[k].tokenAddress); uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchFee) / feeDenominator; - // Check proxy's allowance from user, and user's funds to pay approximated amounts. - require( - requestedToken.allowance(msg.sender, address(this)) >= uTokens[k].amountAndFee, - 'Insufficient allowance for batch to pay' - ); - require( - requestedToken.balanceOf(msg.sender) >= uTokens[k].amountAndFee + uTokens[k].batchFeeAmount, - 'Not enough funds, including fees' - ); - - // 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' + requestedToken = IERC20(uTokens[k].tokenAddress); + contractAllowanceApprovalTransfer( + requestedToken, + uTokens[k].amountAndFee, + uTokens[k].batchFeeAmount, + address(paymentErc20ConversionProxy) ); - - // Batch contract approves Erc20ConversionProxy to spend the token - if ( - requestedToken.allowance(address(this), address(paymentErc20ConversionProxy)) < - uTokens[k].amountAndFee - ) { - approvePaymentProxyToSpend(uTokens[k].tokenAddress, address(paymentErc20ConversionProxy)); - } } // Batch pays the requests using Erc20ConversionFeeProxy diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index abf65ba7e9..769a837033 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -33,9 +33,9 @@ contract BatchNoConversionPayments is Ownable { uint256 internal feeDenominator = 10000; /** payerAuthorized is set to true only when needed for batch Eth conversion */ - bool internal payerAuthorized; + bool internal payerAuthorized = false; /** batchPayment function is the caller */ - bool internal batchPaymentOrigin; + bool internal batchPaymentOrigin = false; /** transferBackRemainingEth is set to false only if the payer use batchRouter and call both batchEthPayments and batchConversionEthPaymentsWithReference */ @@ -63,7 +63,7 @@ contract BatchNoConversionPayments is Ownable { * feeAmount: The fee amount, denominated in the first currency of `path` for conversion payment * maxToSpend: Only for conversion payment: * Maximum amount the payer wants to spend, denominated in the last currency of `path`: - * it includes fee proxy but NOT the batchFee + * it includes fee proxy but NOT the batch fees to pay * maxRateTimespan: Only for conversion payment: * Max acceptable times span for conversion rates, ignored if zero */ @@ -181,31 +181,37 @@ contract BatchNoConversionPayments is Ownable { if (batchPaymentOrigin != true) { batchFeeAmountUSD = 0; } - // amount is used to get the total amount, and then used as batch fee amount - uint256 amount = 0; uint256 amountAndFee = 0; + uint256 batchFeeAmount = 0; for (uint256 i = 0; i < conversionDetails.length; i++) { - amount += conversionDetails[i].requestAmount; amountAndFee += conversionDetails[i].requestAmount + conversionDetails[i].feeAmount; + batchFeeAmount += conversionDetails[i].requestAmount; } + batchFeeAmount = (batchFeeAmount * batchFee) / feeDenominator; + + // batchFeeToPay and batchFeeAmountUSD are updated if needed + (batchFeeAmount, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeAmount, + conversionDetails[0].path[0], + batchFeeAmountUSD, + pathsToUSD + ); - // Transfer the amount and fee from the payer to the batch contract IERC20 requestedToken = IERC20(conversionDetails[0].path[0]); - require( - requestedToken.allowance(msg.sender, address(this)) >= amountAndFee, - 'Insufficient allowance for batch to pay' + + contractAllowanceApprovalTransfer( + requestedToken, + amountAndFee, + batchFeeAmount, + address(paymentErc20Proxy) ); - require(requestedToken.balanceOf(msg.sender) >= amountAndFee, 'Not enough funds'); + + // Payer pays batch fee amount require( - safeTransferFrom(conversionDetails[0].path[0], address(this), amountAndFee), - 'payment transferFrom() failed' + safeTransferFrom(conversionDetails[0].path[0], feeAddress, batchFeeAmount), + 'Batch fee transferFrom() failed' ); - // Batch contract approve Erc20FeeProxy to spend the token - if (requestedToken.allowance(address(this), address(paymentErc20Proxy)) < amountAndFee) { - approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); - } - // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < conversionDetails.length; i++) { ConversionDetail memory cD = conversionDetails[i]; @@ -219,24 +225,6 @@ contract BatchNoConversionPayments is Ownable { ); } - // amount is updated into batch fee amount - amount = (amount * batchFee) / feeDenominator; - // 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 - // amount that represents batchFeeToPay updated if needed - (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( - amount, - conversionDetails[0].path[0], - batchFeeAmountUSD, - pathsToUSD - ); - require( - safeTransferFrom(conversionDetails[0].path[0], feeAddress, amount), - 'Batch fee transferFrom() failed' - ); - return batchFeeAmountUSD; } @@ -265,34 +253,15 @@ contract BatchNoConversionPayments is Ownable { // The payer transfers tokens to the batch contract and pays batch fee for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { - IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / feeDenominator; - - require( - requestedToken.allowance(msg.sender, address(this)) >= - 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) >= 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(uTokens[i].tokenAddress, address(this), uTokens[i].amountAndFee), - 'payment transferFrom() failed' + IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); + contractAllowanceApprovalTransfer( + requestedToken, + uTokens[i].amountAndFee, + uTokens[i].batchFeeAmount, + address(paymentErc20Proxy) ); - // Batch contract approves Erc20FeeProxy to spend the token - if ( - requestedToken.allowance(address(this), address(paymentErc20Proxy)) < - uTokens[i].amountAndFee - ) { - approvePaymentProxyToSpend(address(requestedToken), address(paymentErc20Proxy)); - } - // Payer pays batch fee amount uint256 batchFeeToPay = uTokens[i].batchFeeAmount; @@ -329,6 +298,46 @@ contract BatchNoConversionPayments is Ownable { * Helper functions */ + /** + * It: + * - checks that the batch contract has enough allowance from the payer + * - checks that the payer has enough fund, including batch fees + * - does the transfer of token from the payer to the batch contract + * - increases the allowance of the contract to use the payment proxy if needed + * @param requestedToken The token to pay + * @param amountAndFee The amount and the fee for a token to pay + * @param batchFeeAmount The batch fee amount for a token to pay + * @param paymentProxyAddress The payment proxy address used to pay + */ + function contractAllowanceApprovalTransfer( + IERC20 requestedToken, + uint256 amountAndFee, + uint256 batchFeeAmount, + address paymentProxyAddress + ) internal { + // Check proxy's allowance from user + require( + requestedToken.allowance(msg.sender, address(this)) >= amountAndFee, + 'Insufficient allowance for batch to pay' + ); + // Check user's funds to pay amounts, it is an approximation for conversion payment + require( + requestedToken.balanceOf(msg.sender) >= amountAndFee + batchFeeAmount, + 'Not enough funds, including fees' + ); + + // Transfer the amount and fee required for the token on the batch contract + require( + safeTransferFrom(address(requestedToken), address(this), amountAndFee), + 'payment transferFrom() failed' + ); + + // Batch contract approves Erc20ConversionProxy to spend the token + if (requestedToken.allowance(address(this), paymentProxyAddress) < amountAndFee) { + approvePaymentProxyToSpend(address(requestedToken), paymentProxyAddress); + } + } + /** * It create a list of unique tokens used and the amounts associated. * It only considers tokens having: requestAmount + feeAmount > 0. diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 7cb8b7934f..1f3a7e49ab 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -590,7 +590,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { batch .connect(spender3) .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), - ).revertedWith('Not enough funds'); + ).revertedWith('Not enough funds, including fees'); }); it('Should revert batch if not enough funds to pay the batch fee', async () => { @@ -608,7 +608,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { 0, feeAddress, ), - ).revertedWith('Not enough funds for the batch fee'); + ).revertedWith('Not enough funds, including fees'); }); it('Should revert batch without approval', async () => { From 552ebbb3cf74320d74cdfb5f43d533e6cc67da90 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 12:12:01 +0200 Subject: [PATCH 100/138] rename ConversionDetail into RequestDetail --- .../src/contracts/BatchConversionPayments.sol | 60 +++---- .../contracts/BatchNoConversionPayments.sol | 118 ++++++------- .../BatchConversionPayments/0.1.0.json | 24 +-- .../contracts/BatchConversionPayments.test.ts | 158 +++++++++--------- .../BatchNoConversionErc20Payments.test.ts | 42 ++--- .../BatchNoConversionEthPayments.test.ts | 50 +++--- packages/types/src/payment-types.ts | 19 +-- 7 files changed, 231 insertions(+), 240 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index e7ca519c35..93b6e5061e 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -27,11 +27,11 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @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 and no conversion requests to be paid + * - requestDetails all the data required for conversion and no conversion requests to be paid */ struct MetaDetail { uint256 paymentNetworkId; - ConversionDetail[] conversionDetails; + RequestDetail[] requestDetails; } /** @@ -63,7 +63,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @notice Batch payments on different payment networks at once. - * @param metaDetails contains paymentNetworkId and conversionDetails + * @param metaDetails contains paymentNetworkId and requestDetails * - batchMultiERC20ConversionPayments, paymentNetworkId=0 * - batchERC20Payments, paymentNetworkId=1 * - batchMultiERC20Payments, paymentNetworkId=2 @@ -76,7 +76,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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( + function batchPayment( MetaDetail[] calldata metaDetails, address[][] calldata pathsToUSD, address feeAddress @@ -91,44 +91,44 @@ contract BatchConversionPayments is BatchNoConversionPayments { uint256 batchFeeAmountUSD = 0; for (uint256 i = 0; i < metaDetails.length; i++) { - MetaDetail calldata metaConversionDetail = metaDetails[i]; - if (metaConversionDetail.paymentNetworkId == 0) { + MetaDetail calldata metaDetail = metaDetails[i]; + if (metaDetail.paymentNetworkId == 0) { batchFeeAmountUSD += batchMultiERC20ConversionPayments( - metaConversionDetail.conversionDetails, + metaDetail.requestDetails, batchFeeAmountUSD, pathsToUSD, feeAddress ); - } else if (metaConversionDetail.paymentNetworkId == 1) { + } else if (metaDetail.paymentNetworkId == 1) { batchFeeAmountUSD += batchERC20Payments( - metaConversionDetail.conversionDetails, + metaDetail.requestDetails, pathsToUSD, batchFeeAmountUSD, feeAddress ); - } else if (metaConversionDetail.paymentNetworkId == 2) { + } else if (metaDetail.paymentNetworkId == 2) { batchFeeAmountUSD += batchMultiERC20Payments( - metaConversionDetail.conversionDetails, + metaDetail.requestDetails, pathsToUSD, batchFeeAmountUSD, feeAddress ); - } else if (metaConversionDetail.paymentNetworkId == 3) { + } else if (metaDetail.paymentNetworkId == 3) { if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { // Set to false only if batchEthConversionPayments is called after this function transferBackRemainingEth = false; } batchFeeAmountUSD += batchEthPayments( - metaConversionDetail.conversionDetails, + metaDetail.requestDetails, batchFeeAmountUSD, payable(feeAddress) ); if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { transferBackRemainingEth = true; } - } else if (metaConversionDetail.paymentNetworkId == 4) { + } else if (metaDetail.paymentNetworkId == 4) { batchFeeAmountUSD += batchEthConversionPayments( - metaConversionDetail.conversionDetails, + metaDetail.requestDetails, batchFeeAmountUSD, payable(feeAddress) ); @@ -144,14 +144,14 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @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 ERC20 requests denominated in fiat to pay. + * @param requestDetails List of ERC20 requests denominated in fiat to pay. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. * Without paths, there is not limitation. * @param feeAddress The fee recipient */ function batchMultiERC20ConversionPayments( - ConversionDetail[] calldata conversionDetails, + RequestDetail[] calldata requestDetails, uint256 batchFeeAmountUSD, address[][] calldata pathsToUSD, address feeAddress @@ -160,7 +160,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { if (batchPaymentOrigin != true) { batchFeeAmountUSD = 0; } - Token[] memory uTokens = getUTokens(conversionDetails); + Token[] memory uTokens = getUTokens(requestDetails); IERC20 requestedToken; // For each token: check allowance, transfer funds on the contract and approve the paymentProxy to spend if needed @@ -176,8 +176,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { } // Batch pays the requests using Erc20ConversionFeeProxy - for (uint256 i = 0; i < conversionDetails.length; i++) { - ConversionDetail memory rI = conversionDetails[i]; + for (uint256 i = 0; i < requestDetails.length; i++) { + RequestDetail memory rI = requestDetails[i]; paymentErc20ConversionProxy.transferFromWithReferenceAndFee( rI.recipient, rI.requestAmount, @@ -224,7 +224,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @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 conversionDetails List of ETH requests denominated in fiat to pay. + * @param requestDetails List of ETH requests denominated in fiat to pay. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. @@ -234,7 +234,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * This choice reduces the gas significantly, by delegating the whole conversion to the payment proxy. */ function batchEthConversionPayments( - ConversionDetail[] calldata conversionDetails, + RequestDetail[] calldata requestDetails, uint256 batchFeeAmountUSD, address payable feeAddress ) public payable returns (uint256) { @@ -246,16 +246,16 @@ contract BatchConversionPayments is BatchNoConversionPayments { payerAuthorized = true; // Batch contract pays the requests through EthConversionProxy - for (uint256 i = 0; i < conversionDetails.length; i++) { - ConversionDetail memory cD = conversionDetails[i]; + for (uint256 i = 0; i < requestDetails.length; i++) { + RequestDetail memory rD = requestDetails[i]; paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( - payable(cD.recipient), - cD.requestAmount, - cD.path, - cD.paymentReference, - cD.feeAmount, + payable(rD.recipient), + rD.requestAmount, + rD.path, + rD.paymentReference, + rD.feeAmount, feeAddress, - cD.maxRateTimespan + rD.maxRateTimespan ); } diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 769a837033..afd2287846 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -67,7 +67,7 @@ contract BatchNoConversionPayments is Ownable { * maxRateTimespan: Only for conversion payment: * Max acceptable times span for conversion rates, ignored if zero */ - struct ConversionDetail { + struct RequestDetail { address recipient; uint256 requestAmount; address[] path; @@ -107,14 +107,14 @@ contract BatchNoConversionPayments is Ownable { /** * @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 conversionDetails List of ETH requests to pay. + * @param requestDetails List of ETH requests to pay. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @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 batchEthPayments( - ConversionDetail[] calldata conversionDetails, + RequestDetail[] calldata requestDetails, uint256 batchFeeAmountUSD, address payable feeAddress ) public payable returns (uint256) { @@ -126,15 +126,15 @@ contract BatchNoConversionPayments is Ownable { uint256 amount = 0; // Batch contract pays the requests thourgh EthFeeProxy - for (uint256 i = 0; i < conversionDetails.length; i++) { - ConversionDetail memory cD = conversionDetails[i]; - require(address(this).balance >= cD.requestAmount + cD.feeAmount, 'Not enough funds'); - amount += cD.requestAmount; - - paymentEthProxy.transferWithReferenceAndFee{value: cD.requestAmount + cD.feeAmount}( - payable(cD.recipient), - cD.paymentReference, - cD.feeAmount, + for (uint256 i = 0; i < requestDetails.length; i++) { + RequestDetail memory rD = requestDetails[i]; + require(address(this).balance >= rD.requestAmount + rD.feeAmount, 'Not enough funds'); + amount += rD.requestAmount; + + paymentEthProxy.transferWithReferenceAndFee{value: rD.requestAmount + rD.feeAmount}( + payable(rD.recipient), + rD.paymentReference, + rD.feeAmount, payable(feeAddress) ); } @@ -162,7 +162,7 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. - * @param conversionDetails List of ERC20 requests to pay, with only one ERC20 token. + * @param requestDetails List of ERC20 requests to pay, with only one ERC20 token. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. * Without paths, there is not limitation. * @param batchFeeAmountUSD The batch fee amount in USD already paid. @@ -172,7 +172,7 @@ contract BatchNoConversionPayments is Ownable { * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ function batchERC20Payments( - ConversionDetail[] calldata conversionDetails, + RequestDetail[] calldata requestDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, address feeAddress @@ -183,21 +183,21 @@ contract BatchNoConversionPayments is Ownable { } uint256 amountAndFee = 0; uint256 batchFeeAmount = 0; - for (uint256 i = 0; i < conversionDetails.length; i++) { - amountAndFee += conversionDetails[i].requestAmount + conversionDetails[i].feeAmount; - batchFeeAmount += conversionDetails[i].requestAmount; + for (uint256 i = 0; i < requestDetails.length; i++) { + amountAndFee += requestDetails[i].requestAmount + requestDetails[i].feeAmount; + batchFeeAmount += requestDetails[i].requestAmount; } batchFeeAmount = (batchFeeAmount * batchFee) / feeDenominator; // batchFeeToPay and batchFeeAmountUSD are updated if needed (batchFeeAmount, batchFeeAmountUSD) = calculateBatchFeeToPay( batchFeeAmount, - conversionDetails[0].path[0], + requestDetails[0].path[0], batchFeeAmountUSD, pathsToUSD ); - IERC20 requestedToken = IERC20(conversionDetails[0].path[0]); + IERC20 requestedToken = IERC20(requestDetails[0].path[0]); contractAllowanceApprovalTransfer( requestedToken, @@ -208,19 +208,19 @@ contract BatchNoConversionPayments is Ownable { // Payer pays batch fee amount require( - safeTransferFrom(conversionDetails[0].path[0], feeAddress, batchFeeAmount), + safeTransferFrom(requestDetails[0].path[0], feeAddress, batchFeeAmount), 'Batch fee transferFrom() failed' ); // Batch contract pays the requests using Erc20FeeProxy - for (uint256 i = 0; i < conversionDetails.length; i++) { - ConversionDetail memory cD = conversionDetails[i]; + for (uint256 i = 0; i < requestDetails.length; i++) { + RequestDetail memory rD = requestDetails[i]; paymentErc20Proxy.transferFromWithReferenceAndFee( - cD.path[0], - cD.recipient, - cD.requestAmount, - cD.paymentReference, - cD.feeAmount, + rD.path[0], + rD.recipient, + rD.requestAmount, + rD.paymentReference, + rD.feeAmount, feeAddress ); } @@ -230,7 +230,7 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. - * @param conversionDetails List of ERC20 requests to pay. + * @param requestDetails List of ERC20 requests to pay. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. * Without paths, there is not limitation. * @param batchFeeAmountUSD The batch fee amount in USD already paid. @@ -240,7 +240,7 @@ contract BatchNoConversionPayments is Ownable { * Make sure the payer has enough tokens to pay the amount, the fee, and the batch fee. */ function batchMultiERC20Payments( - ConversionDetail[] calldata conversionDetails, + RequestDetail[] calldata requestDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, address feeAddress @@ -249,7 +249,7 @@ contract BatchNoConversionPayments is Ownable { if (batchPaymentOrigin != true) { batchFeeAmountUSD = 0; } - Token[] memory uTokens = getUTokens(conversionDetails); + Token[] memory uTokens = getUTokens(requestDetails); // The payer transfers tokens to the batch contract and pays batch fee for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { @@ -280,14 +280,14 @@ contract BatchNoConversionPayments is Ownable { } // Batch contract pays the requests using Erc20FeeProxy - for (uint256 i = 0; i < conversionDetails.length; i++) { - ConversionDetail memory cD = conversionDetails[i]; + for (uint256 i = 0; i < requestDetails.length; i++) { + RequestDetail memory rD = requestDetails[i]; paymentErc20Proxy.transferFromWithReferenceAndFee( - cD.path[0], - cD.recipient, - cD.requestAmount, - cD.paymentReference, - cD.feeAmount, + rD.path[0], + rD.recipient, + rD.requestAmount, + rD.paymentReference, + rD.feeAmount, feeAddress ); } @@ -326,7 +326,7 @@ contract BatchNoConversionPayments is Ownable { 'Not enough funds, including fees' ); - // Transfer the amount and fee required for the token on the batch contract + // Transfer the amount and fees (no batch fees) required for the token on the batch contract require( safeTransferFrom(address(requestedToken), address(this), amountAndFee), 'payment transferFrom() failed' @@ -344,42 +344,42 @@ contract BatchNoConversionPayments is Ownable { * Regarding ERC20 no conversion payments: * batchFeeAmount is the sum of requestAmount and feeAmount. * Out of the function, batch fee rate is applied - * @param conversionDetails List of requests to pay. + * @param requestDetails List of requests to pay. */ - function getUTokens(ConversionDetail[] calldata conversionDetails) + function getUTokens(RequestDetail[] calldata requestDetails) internal pure returns (Token[] memory uTokens) { // A list of unique tokens, with the sum of maxToSpend by token - uTokens = new Token[](conversionDetails.length); - for (uint256 i = 0; i < conversionDetails.length; i++) { - for (uint256 k = 0; k < conversionDetails.length; k++) { - ConversionDetail memory cD = conversionDetails[i]; + uTokens = new Token[](requestDetails.length); + for (uint256 i = 0; i < requestDetails.length; i++) { + for (uint256 k = 0; k < requestDetails.length; k++) { + RequestDetail memory rD = requestDetails[i]; // If the token is already in the existing uTokens list - if (uTokens[k].tokenAddress == cD.path[cD.path.length - 1]) { - if (cD.path.length > 1) { - uTokens[k].amountAndFee += cD.maxToSpend; + if (uTokens[k].tokenAddress == rD.path[rD.path.length - 1]) { + if (rD.path.length > 1) { + uTokens[k].amountAndFee += rD.maxToSpend; } else { // It is not a conversion payment - uTokens[k].amountAndFee += cD.requestAmount + cD.feeAmount; - uTokens[k].batchFeeAmount += cD.requestAmount; + uTokens[k].amountAndFee += rD.requestAmount + rD.feeAmount; + uTokens[k].batchFeeAmount += rD.requestAmount; } break; } // If the token is not in the list (amountAndFee = 0) else if ( - uTokens[k].amountAndFee == 0 && (cD.maxToSpend > 0 || cD.requestAmount + cD.feeAmount > 0) + uTokens[k].amountAndFee == 0 && (rD.maxToSpend > 0 || rD.requestAmount + rD.feeAmount > 0) ) { - uTokens[k].tokenAddress = cD.path[cD.path.length - 1]; + uTokens[k].tokenAddress = rD.path[rD.path.length - 1]; - if (cD.path.length > 1) { + if (rD.path.length > 1) { // amountAndFee is used to store _maxToSpend, useful to send enough tokens to this contract - uTokens[k].amountAndFee = cD.maxToSpend; + uTokens[k].amountAndFee = rD.maxToSpend; } else { // It is not a conversion payment - uTokens[k].amountAndFee = cD.requestAmount + cD.feeAmount; - uTokens[k].batchFeeAmount = cD.requestAmount; + uTokens[k].amountAndFee = rD.requestAmount + rD.feeAmount; + uTokens[k].batchFeeAmount = rD.requestAmount; } break; } @@ -419,14 +419,14 @@ contract BatchNoConversionPayments is Ownable { batchFeeToPay, pathsToUSD[i] ); - // calculate the batch fee to pay, taking care of the batchFeeAmountUSDLimit + // Calculate the batch fee to pay, taking care of the batchFeeAmountUSDLimit uint256 conversionToPayUSD = conversionUSD; if (batchFeeAmountUSD + conversionToPayUSD > batchFeeAmountUSDLimit) { conversionToPayUSD = batchFeeAmountUSDLimit - batchFeeAmountUSD; batchFeeToPay = (batchFeeToPay * conversionToPayUSD) / conversionUSD; } batchFeeAmountUSD += conversionToPayUSD; - // add only once the fees + // Add only once the fees break; } } @@ -498,8 +498,8 @@ contract BatchNoConversionPayments is Ownable { */ /** - * @notice fees added when using Erc20/Eth batch functions - * @param _batchFee between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fee + * @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; 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 9679b3e0a5..d6aac7cfa0 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 @@ -121,8 +121,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" }, { @@ -192,8 +192,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" }, { @@ -258,8 +258,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" }, { @@ -350,8 +350,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" }, { @@ -421,8 +421,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" }, { @@ -499,8 +499,8 @@ "type": "uint256" } ], - "internalType": "struct BatchNoConversionPayments.ConversionDetail[]", - "name": "conversionDetails", + "internalType": "struct BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", "type": "tuple[]" } ], diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 272bebdaae..af07ee3951 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -65,7 +65,7 @@ describe('contract: BatchConversionPayments', async () => { let chainlinkPath: ChainlinkConversionPath; // constants inputs for batch conversion functions - const fauConvDetail: PaymentTypes.ConversionDetail = { + const fauConvRequest: PaymentTypes.RequestDetail = { recipient: '', requestAmount: '100000' + fiatDecimals, path: [USD_hash, FAU_address], @@ -75,7 +75,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }; - const daiConvDetail: PaymentTypes.ConversionDetail = { + const daiConvRequest: PaymentTypes.RequestDetail = { recipient: '', requestAmount: '100000' + fiatDecimals, path: [EUR_hash, USD_hash, DAI_address], @@ -85,7 +85,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }; - const ethConvDetail: PaymentTypes.ConversionDetail = { + const ethConvRequest: PaymentTypes.RequestDetail = { recipient: '', requestAmount: '1000', path: [USD_hash, ETH_hash], @@ -123,9 +123,9 @@ describe('contract: BatchConversionPayments', async () => { await adminSigner.getAddress(), ); - fauConvDetail.recipient = to; - daiConvDetail.recipient = to; - ethConvDetail.recipient = to; + fauConvRequest.recipient = to; + daiConvRequest.recipient = to; + ethConvRequest.recipient = to; // set batch proxy fees and connect fromSigner await batchConversionProxy.setBatchFee(BATCH_FEE); @@ -328,7 +328,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '100000', @@ -365,7 +365,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail, daiConvDetail, daiConvDetail], + requestDetails: [fauConvRequest, daiConvRequest, daiConvRequest], }, ], [ @@ -388,7 +388,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '1000', @@ -425,7 +425,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [ethConvDetail], + requestDetails: [ethConvRequest], }, ], [], @@ -452,26 +452,15 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const ethCryptoDetail: PaymentTypes.ConversionDetail = { - recipient: to, - requestAmount: '1000', - path: [], - paymentReference: referenceExample, - feeAmount: '1', - maxToSpend: '0', - maxRateTimespan: '0', - }; - tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail], + requestDetails: [fauConvRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '100000', @@ -485,11 +474,21 @@ describe('contract: BatchConversionPayments', async () => { }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [ethCryptoDetail], + requestDetails: [ + { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [ethConvDetail], + requestDetails: [ethConvRequest], }, ], [[FAU_address, USD_hash]], @@ -575,7 +574,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '20100' + daiDecimals, @@ -619,7 +618,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '10100' + daiDecimals, @@ -663,7 +662,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '10100' + daiDecimals, @@ -710,7 +709,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail, daiConvDetail], + requestDetails: [fauConvRequest, daiConvRequest], }, ], [ @@ -776,7 +775,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: reqAmount, @@ -816,13 +815,13 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); const reqAmount = 200000000000; - const increasedEthConvDetail = Utils.deepCopy(ethConvDetail); - increasedEthConvDetail.requestAmount = reqAmount.toString(); + const copyEthConvRequest = Utils.deepCopy(ethConvRequest); + copyEthConvRequest.requestAmount = reqAmount.toString(); tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [increasedEthConvDetail], + requestDetails: [copyEthConvRequest], }, ], [], @@ -854,26 +853,15 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - // set inputs: ERC20 cryptoDetails & ethCryptoDetails - const ethCryptoDetail: PaymentTypes.ConversionDetail = { - recipient: to, - requestAmount: '1000', - path: [], - paymentReference: referenceExample, - feeAmount: '1', - maxToSpend: '0', - maxRateTimespan: '0', - }; - tx = await batchConversionProxy.batchRouter( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: [fauConvDetail], + requestDetails: [fauConvRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [ + requestDetails: [ { recipient: to, requestAmount: '100000', @@ -887,19 +875,27 @@ describe('contract: BatchConversionPayments', async () => { }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - conversionDetails: [ethCryptoDetail], + requestDetails: [ + { + recipient: to, + requestAmount: '1000', + path: [], + paymentReference: referenceExample, + feeAmount: '1', + maxToSpend: '0', + maxRateTimespan: '0', + }, + ], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - conversionDetails: [ethConvDetail], + requestDetails: [ethConvRequest], }, ], [[FAU_address, USD_hash]], feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees ); - ethCryptoDetail; - ethConvDetail; // Chech FAU Balances // let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); @@ -968,7 +964,7 @@ describe('contract: BatchConversionPayments', async () => { batchConversionProxy.batchRouter( Array(6).fill({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [], + requestDetails: [], }), [[FAU_address, USD_hash]], feeAddress, @@ -981,7 +977,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: 6, - conversionDetails: [], + requestDetails: [], }, ], [[FAU_address, USD_hash]], @@ -998,7 +994,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) .batchMultiERC20ConversionPayments( - [fauConvDetail], + [fauConvRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1024,7 +1020,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) .batchMultiERC20ConversionPayments( - [daiConvDetail], + [daiConvRequest], 0, [[DAI_address, USD_hash]], feeAddress, @@ -1046,7 +1042,7 @@ describe('contract: BatchConversionPayments', async () => { it('make 3 payments with different tokens and conversion length', async () => { const batchPayment = async () => { return await batchConversionProxy.connect(fromSigner).batchMultiERC20ConversionPayments( - [fauConvDetail, daiConvDetail, daiConvDetail], + [fauConvRequest, daiConvRequest, daiConvRequest], 0, [ [FAU_address, USD_hash], @@ -1060,11 +1056,11 @@ describe('contract: BatchConversionPayments', async () => { }); describe('batchMultiERC20ConversionPayments errors', async () => { it('cannot transfer with invalid path', async () => { - const convDetail = Utils.deepCopy(fauConvDetail); - convDetail.path = [EUR_hash, ETH_hash, DAI_address]; + const convRequest = Utils.deepCopy(fauConvRequest); + convRequest.path = [EUR_hash, ETH_hash, DAI_address]; await expect( batchConversionProxy.batchMultiERC20ConversionPayments( - [convDetail], + [convRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1073,11 +1069,11 @@ describe('contract: BatchConversionPayments', async () => { }); it('cannot transfer if max to spend too low', async () => { - const convDetail = Utils.deepCopy(fauConvDetail); - convDetail.maxToSpend = '1000000'; // not enough + const convRequest = Utils.deepCopy(fauConvRequest); + convRequest.maxToSpend = '1000000'; // not enough await expect( batchConversionProxy.batchMultiERC20ConversionPayments( - [convDetail], + [convRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1086,11 +1082,11 @@ describe('contract: BatchConversionPayments', async () => { }); it('cannot transfer if rate is too old', async () => { - const convDetail = Utils.deepCopy(fauConvDetail); - convDetail.maxRateTimespan = '10'; + const convRequest = Utils.deepCopy(fauConvRequest); + convRequest.maxRateTimespan = '10'; await expect( batchConversionProxy.batchMultiERC20ConversionPayments( - [convDetail], + [convRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1099,18 +1095,18 @@ describe('contract: BatchConversionPayments', async () => { }); it('Not enough allowance', async () => { - const convDetail = Utils.deepCopy(fauConvDetail); + const convRequest = Utils.deepCopy(fauConvRequest); // reduce fromSigner± allowance await fauERC20.approve( batchConversionProxy.address, - BigNumber.from(convDetail.maxToSpend).sub(2), + BigNumber.from(convRequest.maxToSpend).sub(2), { from, }, ); await expect( batchConversionProxy.batchMultiERC20ConversionPayments( - [convDetail], + [convRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1119,11 +1115,11 @@ describe('contract: BatchConversionPayments', async () => { }); it('Not enough funds even if partially enough funds', async () => { - const convDetail = Utils.deepCopy(fauConvDetail); + const convRequest = Utils.deepCopy(fauConvRequest); // fromSigner transfer enough token to pay just 1 invoice to signer4 await fauERC20 .connect(fromSigner) - .transfer(await signer4.getAddress(), BigNumber.from(convDetail.maxToSpend)); + .transfer(await signer4.getAddress(), BigNumber.from(convRequest.maxToSpend)); // increase signer4 allowance await fauERC20 .connect(signer4) @@ -1134,7 +1130,7 @@ describe('contract: BatchConversionPayments', async () => { batchConversionProxy .connect(signer4) .batchMultiERC20ConversionPayments( - [convDetail, convDetail, convDetail], + [convRequest, convRequest, convRequest], 0, [[FAU_address, USD_hash]], feeAddress, @@ -1153,7 +1149,7 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchEthConversionPayments([ethConvDetail], 0, feeAddress, { + tx = await batchConversionProxy.batchEthConversionPayments([ethConvRequest], 0, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }); await expectETHBalanceDiffs( @@ -1170,11 +1166,11 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const EurConvDetail = Utils.deepCopy(ethConvDetail); - EurConvDetail.path = [EUR_hash, USD_hash, ETH_hash]; + const EurConvRequest = Utils.deepCopy(ethConvRequest); + EurConvRequest.path = [EUR_hash, USD_hash, ETH_hash]; tx = await batchConversionProxy.batchEthConversionPayments( - [ethConvDetail, EurConvDetail, ethConvDetail], + [ethConvRequest, EurConvRequest, ethConvRequest], 0, feeAddress, { @@ -1194,10 +1190,10 @@ describe('contract: BatchConversionPayments', async () => { }); describe('batchEthConversionPayments errors', () => { it('cannot transfer with invalid path', async () => { - const wrongConvDetail = Utils.deepCopy(ethConvDetail); - wrongConvDetail.path = [USD_hash, EUR_hash, ETH_hash]; + const wrongConvRequest = Utils.deepCopy(ethConvRequest); + wrongConvRequest.path = [USD_hash, EUR_hash, ETH_hash]; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvDetail], 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], 0, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }), ).to.be.revertedWith('No aggregator found'); @@ -1205,7 +1201,7 @@ describe('contract: BatchConversionPayments', async () => { it('not enough funds even if partially enough funds', async () => { await expect( batchConversionProxy.batchEthConversionPayments( - [ethConvDetail, ethConvDetail], + [ethConvRequest, ethConvRequest], 0, feeAddress, { @@ -1216,10 +1212,10 @@ describe('contract: BatchConversionPayments', async () => { }); it('cannot transfer if rate is too old', async () => { - const wrongConvDetail = Utils.deepCopy(ethConvDetail); - wrongConvDetail.maxRateTimespan = '1'; + const wrongConvRequest = Utils.deepCopy(ethConvRequest); + wrongConvRequest.maxRateTimespan = '1'; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvDetail], 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], 0, feeAddress, { value: 1000 + 1 + 11, // + 11 to pay batch fees }), ).to.be.revertedWith('aggregator rate is outdated'); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 1f3a7e49ab..7a2bb27f16 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -13,7 +13,7 @@ import { } from '../../src/types'; import { chainlinkConversionPath } from '../../src/lib'; import { CurrencyManager } from '@requestnetwork/currency'; -import { ConversionDetail } from 'types/dist/payment-types'; +import { RequestDetail } from 'types/dist/payment-types'; const logGasInfos = false; @@ -548,9 +548,9 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }); describe('Batch revert, issues with: args, or funds, or approval', () => { - let conversionDetails: ConversionDetail[] = []; + let requestDetails: RequestDetail[] = []; beforeEach(async () => { - conversionDetails = [ + requestDetails = [ { recipient: payee1, requestAmount: '5', @@ -585,11 +585,11 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 100); await token1.connect(spender3).approve(batchAddress, 1000); - conversionDetails[2].requestAmount = '400'; + requestDetails[2].requestAmount = '400'; await expect( batch .connect(spender3) - .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Not enough funds, including fees'); }); @@ -597,13 +597,13 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 1000); - conversionDetails[0].requestAmount = '100'; - conversionDetails[1].requestAmount = '200'; + requestDetails[0].requestAmount = '100'; + requestDetails[1].requestAmount = '200'; await expect( batch .connect(spender3) .batchERC20Payments( - conversionDetails.slice(0, 2), + requestDetails.slice(0, 2), [[token1Address, USD_hash]], 0, feeAddress, @@ -614,11 +614,11 @@ describe('contract: batchNoConversionPayments: ERC20', () => { it('Should revert batch without approval', async () => { await token1.connect(owner).transfer(spender3Address, 303); await token1.connect(spender3).approve(batchAddress, 10); - conversionDetails[0].requestAmount = '20'; + requestDetails[0].requestAmount = '20'; await expect( batch .connect(spender3) - .batchERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); @@ -626,11 +626,11 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 400); await token1.connect(spender3).approve(batchAddress, 1000); - conversionDetails[2].requestAmount = '400'; + requestDetails[2].requestAmount = '400'; await expect( batch .connect(spender3) - .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Not enough funds'); }); @@ -638,14 +638,14 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 607); await token1.connect(spender3).approve(batchAddress, 1000); - conversionDetails[0].requestAmount = '100'; - conversionDetails[1].requestAmount = '200'; - conversionDetails[2].requestAmount = '300'; - conversionDetails[2].recipient = payee2; + requestDetails[0].requestAmount = '100'; + requestDetails[1].requestAmount = '200'; + requestDetails[2].requestAmount = '300'; + requestDetails[2].recipient = payee2; await expect( batch .connect(spender3) - .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Not enough funds'); }); @@ -653,13 +653,13 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await token1.connect(owner).transfer(spender3Address, 1000); await token1.connect(spender3).approve(batchAddress, 10); - conversionDetails[0].requestAmount = '100'; - conversionDetails[1].requestAmount = '200'; - conversionDetails[2].requestAmount = '300'; + requestDetails[0].requestAmount = '100'; + requestDetails[1].requestAmount = '200'; + requestDetails[2].requestAmount = '300'; await expect( batch .connect(spender3) - .batchMultiERC20Payments(conversionDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); }); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index b781b84b3a..7683271bdd 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -40,7 +40,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const provider = new ethers.providers.JsonRpcProvider(networkConfig.url); const currencyManager = CurrencyManager.getDefault(); - const ethConvDetail1: PaymentTypes.ConversionDetail = { + const ethRequestDetail1: PaymentTypes.RequestDetail = { recipient: '', requestAmount: '200', path: [], @@ -49,7 +49,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { maxToSpend: '0', maxRateTimespan: '0', }; - const ethConvDetail2: PaymentTypes.ConversionDetail = { + const ethRequestDetail2: PaymentTypes.RequestDetail = { recipient: '', requestAmount: '300', path: [], @@ -79,8 +79,8 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { currencyManager.fromSymbol('ETH')!.hash, currencyManager.fromSymbol('USD')!.hash, ); - ethConvDetail1.recipient = payee1; - ethConvDetail2.recipient = payee2; + ethRequestDetail1.recipient = payee1; + ethRequestDetail2.recipient = payee2; }); describe('Batch Eth normal flow', () => { @@ -89,17 +89,17 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { beforeEthBalance1 = await provider.getBalance(payee1); beforeEthBalance2 = await provider.getBalance(payee2); - const copyEthConvDetail1 = Utils.deepCopy(ethConvDetail1); - copyEthConvDetail1.requestAmount = '2000'; - copyEthConvDetail1.feeAmount = '100'; + const copyEthRequestDetail1 = Utils.deepCopy(ethRequestDetail1); + copyEthRequestDetail1.requestAmount = '2000'; + copyEthRequestDetail1.feeAmount = '100'; - const copyEthConvDetail2 = Utils.deepCopy(ethConvDetail2); - copyEthConvDetail2.requestAmount = '3000'; - copyEthConvDetail2.feeAmount = '200'; + const copyEthRequestDetail2 = Utils.deepCopy(ethRequestDetail2); + copyEthRequestDetail2.requestAmount = '3000'; + copyEthRequestDetail2.feeAmount = '200'; await expect( batch .connect(owner) - .batchEthPayments([copyEthConvDetail1, copyEthConvDetail2], 0, feeAddress, { + .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], 0, feeAddress, { value: BigNumber.from('6000'), }), ) @@ -128,7 +128,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { value: totalAmount, }); await tx.wait(); @@ -149,14 +149,14 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const feeAmount = 1; const nbTxs = 10; // to compare gas optim, go to 100. - const copyEthConvDetail = Utils.deepCopy(ethConvDetail2); - copyEthConvDetail.requestAmount = amount.toString(); - copyEthConvDetail.feeAmount = feeAmount.toString(); + const copyEthRequestDetail = Utils.deepCopy(ethRequestDetail2); + copyEthRequestDetail.requestAmount = amount.toString(); + copyEthRequestDetail.feeAmount = feeAmount.toString(); const totalAmount = BigNumber.from(((amount + feeAmount) * nbTxs).toString()); const tx = await batch .connect(owner) - .batchEthPayments(Array(nbTxs).fill(copyEthConvDetail), 0, feeAddress, { + .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), 0, feeAddress, { value: totalAmount, }); @@ -180,9 +180,11 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const totalAmount = BigNumber.from('400'); await expect( - batch.connect(owner).batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { - value: totalAmount, - }), + batch + .connect(owner) + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + value: totalAmount, + }), ).revertedWith('Not enough funds'); afterEthBalance1 = await provider.getBalance(payee1); @@ -201,9 +203,11 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const totalAmount = BigNumber.from('530'); // missing 5 (= (200+300) * 1%) await expect( - batch.connect(owner).batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { - value: totalAmount, - }), + batch + .connect(owner) + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + value: totalAmount, + }), ).revertedWith('Not enough funds for batch fee'); afterEthBalance1 = await provider.getBalance(payee1); @@ -231,7 +235,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethConvDetail1, ethConvDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { value: BigNumber.from('1000'), }); await tx.wait(); diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 3e87eead05..4ef388e420 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -323,8 +323,9 @@ export type AllNetworkRetrieverEvents = { }; // Types used by batch conversion smart contract -/** Input type used by batch conversion proxy to make an ERC20/ETH conversion payment */ -export interface ConversionDetail { +/** Input type used by batch conversion proxy to make + * an ERC20/ETH conversion or no-conversion payment */ +export interface RequestDetail { recipient: string; requestAmount: string; path: string[]; @@ -334,15 +335,6 @@ export interface ConversionDetail { 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; -} - /** Each paymentNetworkId is linked with a batch function */ export enum BATCH_PAYMENT_NETWORK_ID { BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -353,9 +345,8 @@ export enum BATCH_PAYMENT_NETWORK_ID { } /** Input type used by batch conversion proxy to make an ERC20 & ETH, - * and conversion & no-conversion payment through batchRouter */ + * and conversion & no-conversion payment through batchPayment */ export interface MetaDetail { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID; - conversionDetails: ConversionDetail[]; - cryptoDetails: CryptoDetails; + requestDetails: RequestDetail[]; } From aec302ced05e9b1e28fbdbf5a086bb449d44ee2c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 12:16:57 +0200 Subject: [PATCH 101/138] rename batchRouter function into batchPayment --- .../src/contracts/BatchConversionPayments.sol | 10 +++--- .../contracts/BatchNoConversionPayments.sol | 2 +- .../BatchConversionPayments/0.1.0.json | 2 +- .../contracts/BatchConversionPayments.test.ts | 34 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 93b6e5061e..a864d41a16 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -13,9 +13,9 @@ import './BatchNoConversionPayments.sol'; * - 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 kinds of payments at once. + * batchPayment is the main function to batch all kinds of payments at once. * If one transaction of the batch fails, all transactions are reverted. - * @dev batchRouter is the main function, but other batch payment functions are "public" in order to do + * @dev batchPayment is the main function, but other batch payment functions are "public" in order to do * gas optimization in some cases. */ contract BatchConversionPayments is BatchNoConversionPayments { @@ -25,8 +25,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { IEthConversionProxy public paymentEthConversionProxy; /** - * @dev Used by the batchRouter to handle information for heterogeneous batches, grouped by payment network. - * - paymentNetworkId: from 0 to 4, cf. `batchRouter()` method. + * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network. + * - paymentNetworkId: from 0 to 4, cf. `batchPayment()` method. * - requestDetails all the data required for conversion and no conversion requests to be paid */ struct MetaDetail { @@ -73,7 +73,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. * Without paths, there is not limitation. * @param feeAddress The address where fees should be paid - * @dev batchRouter only reduces gas consumption when using more than a single payment network. + * @dev batchPayment 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 batchPayment( diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index afd2287846..a507e29da6 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -37,7 +37,7 @@ contract BatchNoConversionPayments is Ownable { /** batchPayment function is the caller */ bool internal batchPaymentOrigin = false; - /** transferBackRemainingEth is set to false only if the payer use batchRouter + /** transferBackRemainingEth is set to false only if the payer use batchPayment and call both batchEthPayments and batchConversionEthPaymentsWithReference */ bool internal transferBackRemainingEth = true; 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 d6aac7cfa0..55a2cd3003 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 @@ -519,7 +519,7 @@ "type": "address" } ], - "name": "batchRouter", + "name": "batchPayment", "outputs": [], "stateMutability": "payable", "type": "function" diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index af07ee3951..10037f6506 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -319,12 +319,12 @@ describe('contract: BatchConversionPayments', async () => { ); }; - describe('batchRouter', async () => { + describe('batchPayment', async () => { describe('payment under the fee limit', async () => { it(`make 1 ERC20 payment with no conversion`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchRouter( + await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, @@ -361,7 +361,7 @@ describe('contract: BatchConversionPayments', async () => { }); it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { const batchPayment = async () => { - return await batchConversionProxy.batchRouter( + return await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -384,7 +384,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, @@ -421,7 +421,7 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, @@ -452,7 +452,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -570,7 +570,7 @@ describe('contract: BatchConversionPayments', async () => { it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchRouter( + await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, @@ -614,7 +614,7 @@ describe('contract: BatchConversionPayments', async () => { it(`make 1 ERC20 payment with no conversion`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchRouter( + await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, @@ -658,7 +658,7 @@ describe('contract: BatchConversionPayments', async () => { it(`make 1 ERC20 payment with no conversion and wrong paths to USD`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchRouter( + await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, @@ -705,7 +705,7 @@ describe('contract: BatchConversionPayments', async () => { const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = await getERC20Balances(daiERC20); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -771,7 +771,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); const USDReqAmount = 200000000000; const reqAmount = BigNumber.from(USDReqAmount).mul(USD_ETH_RATE).toString(); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, @@ -817,7 +817,7 @@ describe('contract: BatchConversionPayments', async () => { const reqAmount = 200000000000; const copyEthConvRequest = Utils.deepCopy(ethConvRequest); copyEthConvRequest.requestAmount = reqAmount.toString(); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, @@ -853,7 +853,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchRouter( + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -958,10 +958,10 @@ describe('contract: BatchConversionPayments', async () => { }); }); }); - describe('batchRouter errors', async () => { - it(`too many elements within batchRouter metaDetails input`, async () => { + describe('batchPayment errors', async () => { + it(`too many elements within batchPayment metaDetails input`, async () => { await expect( - batchConversionProxy.batchRouter( + batchConversionProxy.batchPayment( Array(6).fill({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, requestDetails: [], @@ -973,7 +973,7 @@ describe('contract: BatchConversionPayments', async () => { }); it(`wrong paymentNetworkId set in metaDetails input`, async () => { await expect( - batchConversionProxy.batchRouter( + batchConversionProxy.batchPayment( [ { paymentNetworkId: 6, From ce934f278eaca9755114535e11c3d409c8e92b4f Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 12:44:17 +0200 Subject: [PATCH 102/138] typo corrections --- .../src/contracts/BatchConversionPayments.sol | 19 +++++--------- .../contracts/BatchNoConversionPayments.sol | 26 +++++++++++++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index a864d41a16..8e8656febd 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -25,8 +25,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { IEthConversionProxy public paymentEthConversionProxy; /** - * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network. - * - paymentNetworkId: from 0 to 4, cf. `batchPayment()` method. + * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network: + * - paymentNetworkId: from 0 to 4, cf. `batchPayment()` method * - requestDetails all the data required for conversion and no conversion requests to be paid */ struct MetaDetail { @@ -39,7 +39,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 address of the conversion path contract + * @param _chainlinkConversionPathAddress The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( @@ -85,7 +85,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { // Check that there are paths to USD, and more than one paymentNetworkId if (pathsToUSD.length > 0 && metaDetails.length > 1) { - // Set to true to limit the batch fee to pay + // Set to true to avoid batchFeeAmountUSD to be reset by each batch function batchPaymentOrigin = true; } @@ -137,6 +137,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { } } if (pathsToUSD.length > 0 && metaDetails.length > 1) { + // Set back to false, its default value batchPaymentOrigin = false; } } @@ -148,7 +149,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. * Without paths, there is not limitation. - * @param feeAddress The fee recipient + * @param feeAddress The fee recipient. */ function batchMultiERC20ConversionPayments( RequestDetail[] calldata requestDetails, @@ -299,12 +300,4 @@ contract BatchConversionPayments is BatchNoConversionPayments { function setPaymentEthConversionProxy(address _paymentEthConversionProxy) external onlyOwner { paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionProxy); } - - /** - * @notice Update the conversion path contract used to fetch conversions. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. - */ - function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); - } } diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index a507e29da6..8727b6ad5b 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -28,26 +28,26 @@ contract BatchNoConversionPayments is Ownable { IEthereumFeeProxy public paymentEthProxy; ChainlinkConversionPath public chainlinkConversionPath; + /** Used to calculate batch fees: batchFee = 30 represent 0.30% of fee */ uint256 public batchFee; - /** Used to to calculate batch fees */ + /** Used to calculate batch fees: divide batchFee by feeDenominator */ uint256 internal feeDenominator = 10000; + /** The amount of the batch fee cannot exceed a predefined amount in USD */ + uint256 public batchFeeAmountUSDLimit; /** payerAuthorized is set to true only when needed for batch Eth conversion */ bool internal payerAuthorized = false; /** batchPayment function is the caller */ bool internal batchPaymentOrigin = false; - /** transferBackRemainingEth is set to false only if the payer use batchPayment and call both batchEthPayments and batchConversionEthPaymentsWithReference */ bool internal transferBackRemainingEth = true; - /** The amount of the batch fee cannot exceed a predefined amount in USD */ - uint256 public batchFeeAmountUSDLimit; address public USDAddress; address public ETHAddress; address[][] public pathsEthToUSD; - /** Contain the address of a token, the sum of the amount and fees paid with it, and the batch fee amount */ + /** Contains the address of a token, the sum of the amount and fees paid with it, and the batch fee amount */ struct Token { address tokenAddress; uint256 amountAndFee; @@ -80,7 +80,7 @@ contract BatchNoConversionPayments is Ownable { /** * @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 _chainlinkConversionPathAddress The address of the conversion path contract + * @param _chainlinkConversionPathAddress The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( @@ -499,9 +499,11 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Fees added when using Erc20/Eth batch functions - * @param _batchFee Between 0 and 10000, i.e: batchFee = 50 represent 0.50% of fee + * @param _batchFee Between 0 and 200, i.e: batchFee = 30 represent 0.30% of fee */ function setBatchFee(uint256 _batchFee) external onlyOwner { + // safety to avoid wrong setting + require(_batchFee <= 200, 'The batch fee value is too high: > 2%'); batchFee = _batchFee; } @@ -519,9 +521,17 @@ contract BatchNoConversionPayments is Ownable { paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); } + /** + * @notice Update the conversion path contract used to fetch conversions. + * @param _chainlinkConversionPathAddress The address of the conversion path contract. + */ + function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + } + /** * This function define variables allowing to limit the fees: - * ETHAddress, USDAddress, and pathsEthToUSD + * ETHAddress, USDAddress, and pathsEthToUSD. * @param _ETHAddress The address representing the Ethereum currency. * @param _USDAddress The address representing the USD currency. */ From 385c36f8d1a1dc03f1081016adf14c8d17961b3a Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 13:08:55 +0200 Subject: [PATCH 103/138] fix erc20 tests with batchFee value limited to 200 --- .../BatchNoConversionErc20Payments.test.ts | 157 +++++++++--------- 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index 7a2bb27f16..a08e1e210f 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -87,7 +87,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { token3Address = token3.address; batchAddress = batch.address; - await batch.connect(owner).setBatchFee(1000); + await batch.connect(owner).setBatchFee(100); // 1% of batch fees // batch fee amount USD limited to 1$ await batch.connect(owner).setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); }); @@ -112,8 +112,8 @@ describe('contract: batchNoConversionPayments: ERC20', () => { describe('Batch working well: right args, and approvals', () => { it('Should pay 3 ERC20 payments with paymentRef and pay batch fee', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); - await token1.connect(spender3).approve(batchAddress, 1000); + await token1.connect(owner).transfer(spender3Address, 10000); + await token1.connect(spender3).approve(batchAddress, 10000); beforeERC20Balance1 = await token1.balanceOf(payee1); beforeERC20Balance2 = await token1.balanceOf(payee2); @@ -124,28 +124,28 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [ { recipient: payee1, - requestAmount: 200, + requestAmount: 2000, path: [token1Address], paymentReference: referenceExample1, - feeAmount: 20, + feeAmount: 200, maxToSpend: '0', maxRateTimespan: '0', }, { recipient: payee2, - requestAmount: 30, + requestAmount: 300, path: [token1Address], paymentReference: referenceExample2, - feeAmount: 2, + feeAmount: 20, maxToSpend: '0', maxRateTimespan: '0', }, { recipient: payee2, - requestAmount: 40, + requestAmount: 400, path: [token1Address], paymentReference: referenceExample3, - feeAmount: 3, + feeAmount: 30, maxToSpend: '0', maxRateTimespan: '0', }, @@ -156,14 +156,14 @@ describe('contract: batchNoConversionPayments: ERC20', () => { ), ) .to.emit(token1, 'Transfer') - .withArgs(spender3Address, batchAddress, 200 + 30 + 40 + 20 + 2 + 3) + .withArgs(spender3Address, batchAddress, 2000 + 300 + 400 + 200 + 20 + 30) .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') .withArgs( token1Address, payee1, - '200', + '2000', ethers.utils.keccak256(referenceExample1), - '20', + '200', feeAddress, ) .to.emit(token1, 'Transfer') @@ -171,9 +171,9 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( token1Address, payee2, - '30', + '300', ethers.utils.keccak256(referenceExample2), - '2', + '20', feeAddress, ) .to.emit(token1, 'Transfer') @@ -181,9 +181,9 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( token1Address, payee2, - '40', + '400', ethers.utils.keccak256(referenceExample3), - '3', + '30', feeAddress, ) // batch fee amount from the spender to feeAddress @@ -191,27 +191,27 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( spender3Address, feeAddress, - 27, // batch fee amount = (200+30+40)*10% + 27, // batch fee amount = (2000+300+400)*1% ); afterERC20Balance1 = await token1.balanceOf(payee1); - expect(afterERC20Balance1).to.be.equal(beforeERC20Balance1.add(200)); + expect(afterERC20Balance1).to.be.equal(beforeERC20Balance1.add(2000)); afterERC20Balance2 = await token1.balanceOf(payee2); - expect(afterERC20Balance2).to.be.equal(beforeERC20Balance2.add(30 + 40)); + expect(afterERC20Balance2).to.be.equal(beforeERC20Balance2.add(300 + 400)); afterERC20Balance3 = await token1.balanceOf(spender3Address); expect(beforeERC20Balance3).to.be.equal( - afterERC20Balance3.add(200 + 20 + 20 + (30 + 2 + 3) + (40 + 3 + 4)), + afterERC20Balance3.add(2000 + 200 + 20 + (300 + 20 + 3) + (400 + 30 + 4)), ); }); it('Should pay 3 ERC20 payments Multi tokens with paymentRef and pay batch fee', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); - await token2.connect(owner).transfer(spender3Address, 1000); - await token3.connect(owner).transfer(spender3Address, 1000); + await token1.connect(owner).transfer(spender3Address, 10000); + await token2.connect(owner).transfer(spender3Address, 10000); + await token3.connect(owner).transfer(spender3Address, 10000); - await token1.connect(spender3).approve(batchAddress, 1000); - await token2.connect(spender3).approve(batchAddress, 1000); - await token3.connect(spender3).approve(batchAddress, 1000); + await token1.connect(spender3).approve(batchAddress, 10000); + await token2.connect(spender3).approve(batchAddress, 10000); + await token3.connect(spender3).approve(batchAddress, 10000); beforeERC20Balance1 = await token1.balanceOf(payee1); const beforeERC20Balance2_token2 = await token2.balanceOf(payee2); @@ -227,28 +227,28 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [ { recipient: payee1, - requestAmount: 500, + requestAmount: 5000, path: [token1Address], paymentReference: referenceExample1, - feeAmount: 60, + feeAmount: 600, maxToSpend: '0', maxRateTimespan: '0', }, { recipient: payee2, - requestAmount: 300, + requestAmount: 3000, path: [token2Address], paymentReference: referenceExample2, - feeAmount: 20, + feeAmount: 200, maxToSpend: '0', maxRateTimespan: '0', }, { recipient: payee2, - requestAmount: 400, + requestAmount: 4000, path: [token3Address], paymentReference: referenceExample3, - feeAmount: 30, + feeAmount: 300, maxToSpend: '0', maxRateTimespan: '0', }, @@ -260,18 +260,18 @@ describe('contract: batchNoConversionPayments: ERC20', () => { ) // Transfer event of each token from the spender to the batch proxy .to.emit(token1, 'Transfer') - .withArgs(spender3Address, batchAddress, 500 + 60) + .withArgs(spender3Address, batchAddress, 5000 + 600) .to.emit(token2, 'Transfer') - .withArgs(spender3Address, batchAddress, 300 + 20) + .withArgs(spender3Address, batchAddress, 3000 + 200) .to.emit(token3, 'Transfer') - .withArgs(spender3Address, batchAddress, 400 + 30) + .withArgs(spender3Address, batchAddress, 4000 + 300) .to.emit(erc20FeeProxy, 'TransferWithReferenceAndFee') .withArgs( token1Address, payee1, - '500', + '5000', ethers.utils.keccak256(referenceExample1), - '60', + '600', feeAddress, ) .to.emit(token2, 'Transfer') @@ -279,9 +279,9 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( token2Address, payee2, - '300', + '3000', ethers.utils.keccak256(referenceExample2), - '20', + '200', feeAddress, ) .to.emit(token3, 'Transfer') @@ -289,9 +289,9 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( token3Address, payee2, - '400', + '4000', ethers.utils.keccak256(referenceExample3), - '30', + '300', feeAddress, ) // batch fee amount from the spender to feeAddress for each token @@ -299,35 +299,35 @@ describe('contract: batchNoConversionPayments: ERC20', () => { .withArgs( spender3Address, feeAddress, - 50, // batch fee amount = 500*10% + 50, // batch fee amount = 5000*1% ) .to.emit(token2, 'Transfer') .withArgs(spender3Address, feeAddress, 30) .to.emit(token3, 'Transfer') .withArgs(spender3Address, feeAddress, 40); - expect(await token1.balanceOf(payee1)).to.be.equal(beforeERC20Balance1.add(500)); - expect(await token2.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token2.add(300)); - expect(await token3.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token3.add(400)); + expect(await token1.balanceOf(payee1)).to.be.equal(beforeERC20Balance1.add(5000)); + expect(await token2.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token2.add(3000)); + expect(await token3.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token3.add(4000)); expect(beforeERC20Balance3).to.be.equal( - (await token1.balanceOf(spender3Address)).add(500 + 60 + 50), + (await token1.balanceOf(spender3Address)).add(5000 + 600 + 50), // 50 batch fees ); - expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(50 + 60)); - expect(await token2.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token2.add(20 + 30)); + expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(600 + 50)); // 50 batch fees + expect(await token2.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token2.add(200 + 30)); // 30 batch fees expect(await token3.balanceOf(feeAddress)).to.be.equal( - beforeFeeAddress_token3.add((30 + 40) * 1), + beforeFeeAddress_token3.add((300 + 40) * 1), // 40 batch fees ); }); it('Should pay 3 ERC20 payments Multi tokens, with one payment of 0 token', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); - await token2.connect(owner).transfer(spender3Address, 1000); - await token3.connect(owner).transfer(spender3Address, 1000); + await token1.connect(owner).transfer(spender3Address, 10000); + await token2.connect(owner).transfer(spender3Address, 10000); + await token3.connect(owner).transfer(spender3Address, 10000); - await token1.connect(spender3).approve(batchAddress, 1000); - await token2.connect(spender3).approve(batchAddress, 1000); - await token3.connect(spender3).approve(batchAddress, 1000); + await token1.connect(spender3).approve(batchAddress, 10000); + await token2.connect(spender3).approve(batchAddress, 10000); + await token3.connect(spender3).approve(batchAddress, 10000); beforeERC20Balance1 = await token1.balanceOf(payee1); const beforeERC20Balance2_token2 = await token2.balanceOf(payee2); @@ -342,10 +342,10 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [ { recipient: payee1, - requestAmount: 500, + requestAmount: 5000, path: [token1Address], paymentReference: referenceExample1, - feeAmount: 60, + feeAmount: 600, maxToSpend: '0', maxRateTimespan: '0', }, @@ -360,10 +360,10 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }, { recipient: payee2, - requestAmount: 400, + requestAmount: 4000, path: [token3Address], paymentReference: referenceExample3, - feeAmount: 30, + feeAmount: 300, maxToSpend: '0', maxRateTimespan: '0', }, @@ -378,35 +378,34 @@ describe('contract: batchNoConversionPayments: ERC20', () => { ); await tx.wait(); - - expect(await token1.balanceOf(payee1)).to.be.equal(beforeERC20Balance1.add(500)); + expect(await token1.balanceOf(payee1)).to.be.equal(beforeERC20Balance1.add(5000)); expect(await token2.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token2.add(0)); - expect(await token3.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token3.add(400)); + expect(await token3.balanceOf(payee2)).to.be.equal(beforeERC20Balance2_token3.add(4000)); expect(beforeERC20Balance3).to.be.equal( - (await token1.balanceOf(spender3Address)).add(500 + 60 + 50), + (await token1.balanceOf(spender3Address)).add(5000 + 600 + 50), ); - expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(50 + 60)); + expect(await token1.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token1.add(50 + 600)); expect(await token2.balanceOf(feeAddress)).to.be.equal(beforeFeeAddress_token2.add(0)); expect(await token3.balanceOf(feeAddress)).to.be.equal( - beforeFeeAddress_token3.add((30 + 40) * 1), + beforeFeeAddress_token3.add((300 + 40) * 1), ); }); it('Should pay 4 ERC20 payments on 2 tokens', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); - await token2.connect(owner).transfer(spender3Address, 1000); + await token1.connect(owner).transfer(spender3Address, 10000); + await token2.connect(owner).transfer(spender3Address, 10000); - await token1.connect(spender3).approve(batchAddress, 1000); - await token2.connect(spender3).approve(batchAddress, 1000); + await token1.connect(spender3).approve(batchAddress, 10000); + await token2.connect(spender3).approve(batchAddress, 10000); beforeERC20Balance1 = await token1.balanceOf(payee2); beforeERC20Balance2 = await token2.balanceOf(payee2); beforeERC20Balance3 = await token1.balanceOf(spender3Address); const beforeERC20Balance3Token2 = await token2.balanceOf(spender3Address); - const amount = 20; - const feeAmount = 1; + const amount = 200; + const feeAmount = 10; const tx = await batch.connect(spender3).batchMultiERC20Payments( [ @@ -445,21 +444,23 @@ describe('contract: batchNoConversionPayments: ERC20', () => { expect(afterERC20Balance2).to.be.equal(beforeERC20Balance2.add(amount * 2)); afterERC20Balance3 = await token1.balanceOf(spender3Address); - expect(beforeERC20Balance3).to.be.equal(afterERC20Balance3.add((20 + 1 + 2) * 2)); + expect(beforeERC20Balance3).to.be.equal(afterERC20Balance3.add((200 + 10 + 2) * 2)); const afterERC20Balance3Token2 = await token2.balanceOf(spender3Address); - expect(beforeERC20Balance3Token2).to.be.equal(afterERC20Balance3Token2.add((20 + 1 + 2) * 2)); + expect(beforeERC20Balance3Token2).to.be.equal( + afterERC20Balance3Token2.add((200 + 10 + 2) * 2), + ); }); it('Should pay 10 ERC20 payments', async () => { - await token1.connect(owner).transfer(spender3Address, 1000); - await token1.connect(spender3).approve(batchAddress, 1000); + await token1.connect(owner).transfer(spender3Address, 10000); + await token1.connect(spender3).approve(batchAddress, 10000); beforeERC20Balance1 = await token1.balanceOf(payee1); const beforeFeeAddress_token1 = await token1.balanceOf(feeAddress); - const amount = 20; - const feeAmount = 10; + const amount = 200; + const feeAmount = 100; const nbTxs = 10; const tx = await batch.connect(spender3).batchERC20Payments( @@ -489,7 +490,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { expect(afterERC20Balance1).to.be.equal(beforeERC20Balance1.add(amount * nbTxs)); const afterFeeAddress_token1 = await token1.balanceOf(feeAddress); expect(afterFeeAddress_token1).to.be.equal( - beforeFeeAddress_token1.add(feeAmount * nbTxs + (amount * nbTxs) / 10), + beforeFeeAddress_token1.add(feeAmount * nbTxs + (amount * nbTxs) / 100), ); }); From ccbc6d61b9ea7f99a678d69da661e360e89c045d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 18:56:39 +0200 Subject: [PATCH 104/138] add the gas optimization for batch eth functions and clean tests --- .../src/contracts/BatchConversionPayments.sol | 30 +- .../contracts/BatchNoConversionPayments.sol | 18 +- .../BatchConversionPayments/0.1.0.json | 10 + .../contracts/BatchConversionPayments.test.ts | 318 ++++++++---------- .../BatchNoConversionEthPayments.test.ts | 12 +- 5 files changed, 178 insertions(+), 210 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 8e8656febd..1e8da93004 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -71,9 +71,12 @@ contract BatchConversionPayments is BatchNoConversionPayments { * - batchEthConversionPayments, paymentNetworkId=4 * If metaDetails use paymentNetworkId = 4, it must be at the end of the list, or the transaction can be reverted. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * Without paths, there is not limitation. - * @param feeAddress The address where fees should be paid - * @dev batchPayment only reduces gas consumption when using more than a single payment network. + * For batchEth, mock an array of array to apply the limit, e.g: [[]] + * Without paths, there is not limitation, neither for the batchEth functions. + * @param feeAddress The address where fees should be paid. + * @dev Use pathsToUSD only if you are pretty sure the batch fees will higher than the + * USD limit batchFeeAmountUSDLimit, because it increase gas consumption. + * batchPayment 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 batchPayment( @@ -120,6 +123,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { } batchFeeAmountUSD += batchEthPayments( metaDetail.requestDetails, + pathsToUSD.length > 0, batchFeeAmountUSD, payable(feeAddress) ); @@ -129,6 +133,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { } else if (metaDetail.paymentNetworkId == 4) { batchFeeAmountUSD += batchEthConversionPayments( metaDetail.requestDetails, + pathsToUSD.length > 0, batchFeeAmountUSD, payable(feeAddress) ); @@ -226,6 +231,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 requestDetails List of ETH requests denominated in fiat to pay. + * @param applyFeeLimitUSD It set to true to apply the USD fee limit. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. @@ -236,11 +242,12 @@ contract BatchConversionPayments is BatchNoConversionPayments { */ function batchEthConversionPayments( RequestDetail[] calldata requestDetails, + bool applyFeeLimitUSD, uint256 batchFeeAmountUSD, address payable feeAddress ) public payable returns (uint256) { // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true) { + if (batchPaymentOrigin != true && applyFeeLimitUSD) { batchFeeAmountUSD = 0; } uint256 contractBalance = address(this).balance; @@ -264,12 +271,15 @@ contract BatchConversionPayments is BatchNoConversionPayments { uint256 batchFeeToPay = (((contractBalance - address(this).balance)) * batchFee) / feeDenominator; - (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( - batchFeeToPay, - pathsEthToUSD[0][0], - batchFeeAmountUSD, - pathsEthToUSD - ); + if (applyFeeLimitUSD == true) { + (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( + batchFeeToPay, + pathsEthToUSD[0][0], + batchFeeAmountUSD, + pathsEthToUSD + ); + } + require(address(this).balance >= batchFeeToPay, 'Not enough funds for batch conversion fees'); feeAddress.transfer(batchFeeToPay); diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 8727b6ad5b..cb0b1f031e 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -108,6 +108,7 @@ contract BatchNoConversionPayments is Ownable { * @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 requestDetails List of ETH requests to pay. + * @param applyFeeLimitUSD It set to true to apply the USD fee limit. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. @@ -115,11 +116,12 @@ contract BatchNoConversionPayments is Ownable { */ function batchEthPayments( RequestDetail[] calldata requestDetails, + bool applyFeeLimitUSD, uint256 batchFeeAmountUSD, address payable feeAddress ) public payable returns (uint256) { // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true) { + if (batchPaymentOrigin != true && applyFeeLimitUSD == true) { batchFeeAmountUSD = 0; } // amount is used to get the total amount and then used as batch fee amount @@ -141,12 +143,14 @@ contract BatchNoConversionPayments is Ownable { // amount is updated into batch fee amount amount = (amount * batchFee) / feeDenominator; - (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( - amount, - pathsEthToUSD[0][0], - batchFeeAmountUSD, - pathsEthToUSD - ); + if (applyFeeLimitUSD == true) { + (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( + amount, + pathsEthToUSD[0][0], + batchFeeAmountUSD, + pathsEthToUSD + ); + } // 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 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 55a2cd3003..06ee8dc623 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 @@ -196,6 +196,11 @@ "name": "requestDetails", "type": "tuple[]" }, + { + "internalType": "bool", + "name": "applyFeeLimitUSD", + "type": "bool" + }, { "internalType": "uint256", "name": "batchFeeAmountUSD", @@ -262,6 +267,11 @@ "name": "requestDetails", "type": "tuple[]" }, + { + "internalType": "bool", + "name": "applyFeeLimitUSD", + "type": "bool" + }, { "internalType": "uint256", "name": "batchFeeAmountUSD", diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 10037f6506..a1ed1617d2 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -42,7 +42,7 @@ describe('contract: BatchConversionPayments', async () => { // constants used to set up batch conversion proxy, and also requests payment const BATCH_FEE = 100; // 1% const BATCH_DENOMINATOR = 10000; - const daiDecimals = '1000000000000000000'; // 10^18 + const daiDecimals = '000000000000000000'; // 10^18 const fiatDecimals = '00000000'; const thousandWith18Decimal = '1000000000000000000000'; const referenceExample = '0xaaaa'; @@ -87,9 +87,29 @@ describe('contract: BatchConversionPayments', async () => { const ethConvRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '1000', + requestAmount: '1000' + fiatDecimals, path: [USD_hash, ETH_hash], paymentReference: referenceExample, + feeAmount: '1' + fiatDecimals, + maxToSpend: '0', + maxRateTimespan: '0', + }; + + const fauRequest: PaymentTypes.RequestDetail = { + recipient: '', + requestAmount: '100000' + daiDecimals, + path: [FAU_address], + paymentReference: referenceExample, + feeAmount: '100' + daiDecimals, + maxToSpend: '0', + maxRateTimespan: '0', + }; + + const ethRequest: PaymentTypes.RequestDetail = { + recipient: '', + requestAmount: '1000', + path: [], + paymentReference: referenceExample, feeAmount: '1', maxToSpend: '0', maxRateTimespan: '0', @@ -126,6 +146,8 @@ describe('contract: BatchConversionPayments', async () => { fauConvRequest.recipient = to; daiConvRequest.recipient = to; ethConvRequest.recipient = to; + fauRequest.recipient = to; + ethRequest.recipient = to; // set batch proxy fees and connect fromSigner await batchConversionProxy.setBatchFee(BATCH_FEE); @@ -171,8 +193,14 @@ describe('contract: BatchConversionPayments', async () => { const precision = 1_000_000; const conversionRate = path === 'EUR_DAI' - ? BigNumber.from(daiDecimals).mul(precision).mul(EUR_USD_RATE).div(DAI_USD_RATE) - : BigNumber.from(daiDecimals).mul(precision).mul(PRECISION_RATE).div(FAU_USD_RATE); + ? BigNumber.from('1' + daiDecimals) + .mul(precision) + .mul(EUR_USD_RATE) + .div(DAI_USD_RATE) + : BigNumber.from('1' + daiDecimals) + .mul(precision) + .mul(PRECISION_RATE) + .div(FAU_USD_RATE); const expectedToBalanceDiff = BigNumber.from(amount).mul(conversionRate).mul(nPayment); const expectedFeeBalanceDiff = // fee added by the batch @@ -328,17 +356,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '100000', - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '100', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, ], [[FAU_address, USD_hash]], @@ -347,7 +365,7 @@ describe('contract: BatchConversionPayments', async () => { // check the balance fauERC20 token const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('100000', '100', 1); + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); await expectERC20BalanceDiffs( 'FAU', @@ -388,17 +406,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '1000', - path: [], - paymentReference: referenceExample, - feeAmount: '1', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [ethRequest], }, ], [], @@ -431,13 +439,15 @@ describe('contract: BatchConversionPayments', async () => { [], feeAddress, { - value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(2), }, ); await expectETHBalanceDiffs( - BigNumber.from(1000 * USD_ETH_RATE), - BigNumber.from(1 * USD_ETH_RATE), + BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), + BigNumber.from('1' + fiatDecimals).mul(USD_ETH_RATE), initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -460,31 +470,11 @@ describe('contract: BatchConversionPayments', async () => { }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '100000', - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '100', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '1000', - path: [], - paymentReference: referenceExample, - feeAmount: '1', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [ethRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, @@ -493,7 +483,11 @@ describe('contract: BatchConversionPayments', async () => { ], [[FAU_address, USD_hash]], feeAddress, - { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees + { + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(2), + }, // + 11 to pay batch fees ); // Chech FAU Balances // @@ -504,7 +498,7 @@ describe('contract: BatchConversionPayments', async () => { noConvExpectedFromFAUBalanceDiff, noConvExpectedToFAUBalanceDiff, noConvExpectedFeeFAUBalanceDiff, - ] = getExpectedERC20Balances('100000', '100', 1); + ] = getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); await expectERC20BalanceDiffs( 'FAU', @@ -531,27 +525,29 @@ describe('contract: BatchConversionPayments', async () => { const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); // expectedFeeETHBalanceDiff includes batch conversion fees now + const expectedFeeETHBalanceDiff = // Batch conversion - BigNumber.from(1000 * USD_ETH_RATE) - .add(1 * USD_ETH_RATE) + BigNumber.from(1000) + .mul(1e8) + .mul(USD_ETH_RATE) + .add(1e8 * USD_ETH_RATE) .mul(BATCH_FEE) .div(BATCH_DENOMINATOR) + .add(1e8 * USD_ETH_RATE) // Batch no-conversion - .add(1 * USD_ETH_RATE) .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); + const expectedToETHBalanceDiff = BigNumber.from(1000).mul(1e8).mul(USD_ETH_RATE).add(1000); + const expectedFromETHBalanceDiff = gasAmount - .add(1000 * USD_ETH_RATE + 1000) + .add(expectedToETHBalanceDiff) .add(expectedFeeETHBalanceDiff) .mul(-1); // Check balance changes expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); - expect(toETHBalanceDiff).to.equals( - BigNumber.from(1000 * USD_ETH_RATE + 1000), - 'toETHBalanceDiff', - ); + expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); expect(batchETHBalance).to.equals('0', 'batchETHBalance'); }); @@ -560,7 +556,7 @@ describe('contract: BatchConversionPayments', async () => { before(async () => { await batchConversionProxy .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); // 1_000 $ + .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10 $ }); after(async () => { await batchConversionProxy @@ -570,21 +566,12 @@ describe('contract: BatchConversionPayments', async () => { it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); + await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '20100' + daiDecimals, - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '0' + daiDecimals, - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, ], [[FAU_address, USD_hash]], @@ -593,10 +580,9 @@ describe('contract: BatchConversionPayments', async () => { // check the balance fauERC20 token let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('20100' + daiDecimals, '0' + daiDecimals, 1); - - // 1000$ of batch fees in FAU - expectedFeeFAUBalanceDiff = BigNumber.from('498512437810945273631'); + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + // 10$ of batch fees in FAU and request fee + expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) .mul(-1); @@ -618,17 +604,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '10100' + daiDecimals, - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '0' + daiDecimals, - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, ], [[FAU_address, USD_hash]], @@ -637,10 +613,9 @@ describe('contract: BatchConversionPayments', async () => { // check the balance fauERC20 token let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('10100' + daiDecimals, '0' + daiDecimals, 1); - - // 1000$ of batch fees in FAU - expectedFeeFAUBalanceDiff = BigNumber.from('498512437810945273631'); + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + // 10$ of batch fees in FAU and request fees + expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) .mul(-1); @@ -662,17 +637,7 @@ describe('contract: BatchConversionPayments', async () => { [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '10100' + daiDecimals, - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '0' + daiDecimals, - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, ], [[DAI_address, USD_hash]], // it should be FAU_address @@ -680,10 +645,10 @@ describe('contract: BatchConversionPayments', async () => { ); // check the balance fauERC20 token let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('10100' + daiDecimals, '0' + daiDecimals, 1); - - // around 1001$ of batch fees in FAU - the fee limit is not applied because of the wrong path - expectedFeeFAUBalanceDiff = BigNumber.from('1011010000000000000000'); + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + // TODO 5000+daiDecimals FAU eq 10_000$ + // The batch fee limit is not applied because of the wrong path + expectedFeeFAUBalanceDiff = BigNumber.from('1100000000000000000000'); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) .mul(-1); @@ -723,9 +688,9 @@ describe('contract: BatchConversionPayments', async () => { let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); - // 1000$ of batch fees converted in FAU, and 100$ of fees converted in FAU. + // 10$ of batch fees converted in FAU, and 100$ of request fees converted in FAU. // instead of (1000$ + 1$) + 100$ of fees - expectedFeeFAUBalanceDiff = BigNumber.from('547263681597009955218'); + expectedFeeFAUBalanceDiff = BigNumber.from('54726368159253681641'); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) .mul(-1); @@ -762,14 +727,11 @@ describe('contract: BatchConversionPayments', async () => { ); }); it('make 1 ETH payment without conversion', async () => { - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 1 $ // get Eth balances const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const USDReqAmount = 200000000000; + const USDReqAmount = 1100_00000000; // const reqAmount = BigNumber.from(USDReqAmount).mul(USD_ETH_RATE).toString(); tx = await batchConversionProxy.batchPayment( [ @@ -788,11 +750,10 @@ describe('contract: BatchConversionPayments', async () => { ], }, ], - [], + [[]], // caution the list must not be empty to apply fee limit feeAddress, { value: BigNumber.from(reqAmount).mul(2) }, ); - await expectETHBalanceDiffs( BigNumber.from(reqAmount), BigNumber.from('2000'), @@ -800,51 +761,42 @@ describe('contract: BatchConversionPayments', async () => { initialToETHBalance, initialFeeETHBalance, false, - BigNumber.from('20000000000000000').add('2000'), // batch fees limited + invoice fees + BigNumber.from('20000000000000000').add('2000'), // 10$ batch fees limited + invoice fees ); - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); }); it('make 1 ETH payment with 1-step conversion', async () => { - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10 $ // get Eth balances const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const reqAmount = 200000000000; - const copyEthConvRequest = Utils.deepCopy(ethConvRequest); - copyEthConvRequest.requestAmount = reqAmount.toString(); + tx = await batchConversionProxy.batchPayment( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - requestDetails: [copyEthConvRequest], + requestDetails: [ethConvRequest], }, ], - [], + [[]], // caution, the list must not be empty to apply batch fee limit feeAddress, { - value: BigNumber.from(2 * reqAmount + 1) + value: BigNumber.from('1000' + fiatDecimals) + .mul(2) + .add(1) .mul(USD_ETH_RATE) .toString(), }, ); await expectETHBalanceDiffs( - BigNumber.from(reqAmount).mul(USD_ETH_RATE), + BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), BigNumber.from(1).mul(USD_ETH_RATE), initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, true, - BigNumber.from('20000000020100000'), // equal the sum of the batch fee (10$) and the invoice fee + BigNumber.from('22000000000000000'), // equal the sum of the batch fee (10$) and the invoice fee 1$ ); - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(1000).mul(1e8)); // 1000 $ }); it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = @@ -861,31 +813,11 @@ describe('contract: BatchConversionPayments', async () => { }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '100000', - path: [FAU_address], - paymentReference: referenceExample, - feeAmount: '100', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [fauRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: '1000', - path: [], - paymentReference: referenceExample, - feeAmount: '1', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], + requestDetails: [ethRequest], }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, @@ -894,19 +826,23 @@ describe('contract: BatchConversionPayments', async () => { ], [[FAU_address, USD_hash]], feeAddress, - { value: (1000 + 1 + 11) * USD_ETH_RATE + (1000 + 1 + 11) }, // + 11 to pay batch fees + { + value: BigNumber.from(1000 + 1 + 11) + .mul(1e8) + .mul(USD_ETH_RATE) + .add(1000 + 1 + 11), + }, // + 11 to pay batch fees ); // Chech FAU Balances // let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); - const [ _noConvExpectedFromFAUBalanceDiff, noConvExpectedToFAUBalanceDiff, _noConvExpectedFeeFAUBalanceDiff, - ] = getExpectedERC20Balances('100000', '100', 1); + ] = getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - expectedFeeFAUBalanceDiff = BigNumber.from('547263681597009955318'); // 1000$ of batch fee and the invoice fees + expectedFeeFAUBalanceDiff = BigNumber.from('154726368159253681641'); // 10$ of batch fee and the invoices fees expectedToFAUBalanceDiff = expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff); expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff .add(expectedFeeFAUBalanceDiff) @@ -920,6 +856,7 @@ describe('contract: BatchConversionPayments', async () => { expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff, ); + console.log('ETh'); // Check ETH balances // const receipt = await tx.wait(); @@ -936,23 +873,15 @@ describe('contract: BatchConversionPayments', async () => { const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); // expectedFeeETHBalanceDiff => batch conversion fees = 0 - const expectedFeeETHBalanceDiff = BigNumber.from(1 * USD_ETH_RATE).add(1); - + const expectedFeeETHBalanceDiff = BigNumber.from(1).mul(1e8).mul(USD_ETH_RATE).add(1); + const expectedToETHBalanceDiff = BigNumber.from(1000).mul(1e8).mul(USD_ETH_RATE).add(1000); const expectedFromETHBalanceDiff = gasAmount - .add(1000 * USD_ETH_RATE + 1000) + .add(expectedToETHBalanceDiff) .add(expectedFeeETHBalanceDiff) .mul(-1); - // Check balance changes - console.log('ETH'); - console.log('to'); - expect(toETHBalanceDiff).to.equals( - BigNumber.from(1000 * USD_ETH_RATE + 1000), - 'toETHBalanceDiff', - ); - console.log('fee'); + expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); - console.log('from'); expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); expect(batchETHBalance).to.equals('0', 'batchETHBalance'); }); @@ -1149,12 +1078,20 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchEthConversionPayments([ethConvRequest], 0, feeAddress, { - value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees - }); + tx = await batchConversionProxy.batchEthConversionPayments( + [ethConvRequest], + false, + 0, + feeAddress, + { + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(2), + }, + ); await expectETHBalanceDiffs( - BigNumber.from(1000 * USD_ETH_RATE), - BigNumber.from(1 * USD_ETH_RATE), + BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), + BigNumber.from('1' + fiatDecimals).mul(USD_ETH_RATE), initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -1166,22 +1103,27 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const EurConvRequest = Utils.deepCopy(ethConvRequest); - EurConvRequest.path = [EUR_hash, USD_hash, ETH_hash]; + const EurEthConvRequest = Utils.deepCopy(ethConvRequest); + EurEthConvRequest.path = [EUR_hash, USD_hash, ETH_hash]; tx = await batchConversionProxy.batchEthConversionPayments( - [ethConvRequest, EurConvRequest, ethConvRequest], + [ethConvRequest, EurEthConvRequest, ethConvRequest], + false, 0, feeAddress, { - value: BigNumber.from('100000000000000000'), + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(4), }, ); + const ethAmount = BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(2) + .add(BigNumber.from('1000' + fiatDecimals).mul(24000000)); // 24000000 is EUR_ETH_RATE await expectETHBalanceDiffs( - BigNumber.from(1000 * USD_ETH_RATE) - .mul(2) - .add(1000 * 24000000), // 24000000 is EUR_ETH_RATE - BigNumber.from(USD_ETH_RATE).mul(2).add(24000000), + ethAmount, + ethAmount.div(1000), // within the request: feeAmount is 1000 smaller than amount initialFromETHBalance, initialToETHBalance, initialFeeETHBalance, @@ -1193,7 +1135,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.path = [USD_hash, EUR_hash, ETH_hash]; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, 0, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }), ).to.be.revertedWith('No aggregator found'); @@ -1202,6 +1144,7 @@ describe('contract: BatchConversionPayments', async () => { await expect( batchConversionProxy.batchEthConversionPayments( [ethConvRequest, ethConvRequest], + false, 0, feeAddress, { @@ -1215,7 +1158,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.maxRateTimespan = '1'; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, 0, feeAddress, { value: 1000 + 1 + 11, // + 11 to pay batch fees }), ).to.be.revertedWith('aggregator rate is outdated'); @@ -1306,6 +1249,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], + false, 0, feeAddress, { value: 1000 + 1 + 11 }, // + 11 to pay batch fees diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 7683271bdd..3987c3782a 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -99,7 +99,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], 0, feeAddress, { + .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], false, 0, feeAddress, { value: BigNumber.from('6000'), }), ) @@ -128,7 +128,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { value: totalAmount, }); await tx.wait(); @@ -156,7 +156,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), 0, feeAddress, { + .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), false, 0, feeAddress, { value: totalAmount, }); @@ -182,7 +182,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds'); @@ -205,7 +205,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds for batch fee'); @@ -235,7 +235,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { value: BigNumber.from('1000'), }); await tx.wait(); From 878c84e46a05e3ed0de3acc8e89995c2d9132566 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 12 Oct 2022 22:02:23 +0200 Subject: [PATCH 105/138] test ready --- .../contracts/BatchConversionPayments.test.ts | 941 +++++++----------- 1 file changed, 343 insertions(+), 598 deletions(-) diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index a1ed1617d2..ee0c3b32f3 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -42,9 +42,9 @@ describe('contract: BatchConversionPayments', async () => { // constants used to set up batch conversion proxy, and also requests payment const BATCH_FEE = 100; // 1% const BATCH_DENOMINATOR = 10000; - const daiDecimals = '000000000000000000'; // 10^18 - const fiatDecimals = '00000000'; - const thousandWith18Decimal = '1000000000000000000000'; + const daiDecimals = '000000000000000000'; // 18 decimals + const fiatDecimals = '00000000'; // 8 decimals + const thousandWith18Decimal = '1000000000000000000000'; // 21 decimals const referenceExample = '0xaaaa'; const gasPrice = 2 * 10 ** 10; // await provider.getGasPrice() @@ -67,7 +67,7 @@ describe('contract: BatchConversionPayments', async () => { // constants inputs for batch conversion functions const fauConvRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '100000' + fiatDecimals, + requestAmount: '100000' + fiatDecimals, // eq to $100_000 path: [USD_hash, FAU_address], paymentReference: referenceExample, feeAmount: '100' + fiatDecimals, @@ -77,7 +77,7 @@ describe('contract: BatchConversionPayments', async () => { const daiConvRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '100000' + fiatDecimals, + requestAmount: '100000' + fiatDecimals, // eq to $100_000 path: [EUR_hash, USD_hash, DAI_address], paymentReference: referenceExample, feeAmount: '100' + fiatDecimals, @@ -87,7 +87,7 @@ describe('contract: BatchConversionPayments', async () => { const ethConvRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '1000' + fiatDecimals, + requestAmount: '1000' + fiatDecimals, // eq to $1000 path: [USD_hash, ETH_hash], paymentReference: referenceExample, feeAmount: '1' + fiatDecimals, @@ -97,7 +97,7 @@ describe('contract: BatchConversionPayments', async () => { const fauRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '100000' + daiDecimals, + requestAmount: '100000' + daiDecimals, // eq to $100_000 path: [FAU_address], paymentReference: referenceExample, feeAmount: '100' + daiDecimals, @@ -107,10 +107,14 @@ describe('contract: BatchConversionPayments', async () => { const ethRequest: PaymentTypes.RequestDetail = { recipient: '', - requestAmount: '1000', + requestAmount: BigNumber.from('1100' + fiatDecimals) // eq to $1100, batch fees = $11 + .mul(USD_ETH_RATE) + .toString(), path: [], paymentReference: referenceExample, - feeAmount: '1', + feeAmount: BigNumber.from('2' + fiatDecimals) // eq to $2 + .mul(USD_ETH_RATE) + .toString(), maxToSpend: '0', maxRateTimespan: '0', }; @@ -153,7 +157,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy.setBatchFee(BATCH_FEE); await batchConversionProxy.setETHAndUSDAddress(ETH_hash, USD_hash); - await batchConversionProxy.setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); + await batchConversionProxy.setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10$ batchConversionProxy = batchConversionProxy.connect(fromSigner); @@ -165,7 +169,8 @@ describe('contract: BatchConversionPayments', async () => { fauERC20 = new TestERC20__factory(adminSigner).attach(FAU_address); await fauERC20.transfer(from, BigNumber.from(thousandWith18Decimal + '0000000')); fauERC20 = fauERC20.connect(fromSigner); - + }); + beforeEach(async () => { await daiERC20.approve(batchConversionProxy.address, thousandWith18Decimal + fiatDecimals, { from, }); @@ -251,13 +256,9 @@ describe('contract: BatchConversionPayments', async () => { const fromBalanceDiff = BigNumber.from(fromBalance).sub(initialFromBalance); const toBalanceDiff = BigNumber.from(toBalance).sub(initialToBalance); const feeBalanceDiff = BigNumber.from(feeBalance).sub(initialFeeBalance); - console.log('check balance to'); expect(toBalanceDiff).to.equals(expectedToBalanceDiff, `toBalanceDiff in ${token}`); - console.log('check balance fee'); expect(feeBalanceDiff).to.equals(expectedFeeBalanceDiff, `feeBalanceDiff in ${token}`); - console.log('check balance from'); expect(fromBalanceDiff).to.equals(expectedFromBalanceDiff, `fromBalanceDiff in ${token}`); - console.log('here 1'); expect(batchBalance).to.equals('0', `batchBalance in ${token}`); }; @@ -298,21 +299,23 @@ describe('contract: BatchConversionPayments', async () => { .add(expectedFeeETHBalanceDiff); // Check balance changes - console.log('to'); expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); - console.log('fee:', feeETHBalanceDiff.toString()); expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); - console.log('from'); expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); - console.log('contract'); expect(batchETHBalance).to.equals('0', 'batchETHBalance'); }; /** * 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 forceExpectedFeeFAUBalanceDiff used when batch fees are limited + * @param forceExpectedFeeDAIBalanceDiff used when batch fees are limited */ - const manyPaymentsBatchConv = async (paymentBatch: () => Promise) => { + const manyPaymentsBatchConv = async ( + paymentBatch: () => Promise, + forceExpectedFeeFAUBalanceDiff = BigNumber.from(-1), + forceExpectedFeeDAIBalanceDiff = BigNumber.from(-1), + ) => { const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = await getERC20Balances(daiERC20); const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = @@ -321,8 +324,13 @@ describe('contract: BatchConversionPayments', async () => { await paymentBatch(); // check the balance daiERC20 token - const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = + let [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 2, 'EUR_DAI'); + + if (forceExpectedFeeDAIBalanceDiff.gt(-1)) { + expectedFeeDAIBalanceDiff = forceExpectedFeeDAIBalanceDiff; + expectedFromDAIBalanceDiff = expectedFeeDAIBalanceDiff.add(expectedToDAIBalanceDiff).mul(-1); + } await expectERC20BalanceDiffs( 'DAI', initialFromDAIBalance, @@ -332,10 +340,14 @@ describe('contract: BatchConversionPayments', async () => { expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff, ); - // check the balance fauERC20 token - const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + if (forceExpectedFeeFAUBalanceDiff.gt(-1)) { + expectedFeeFAUBalanceDiff = forceExpectedFeeFAUBalanceDiff; + expectedFromFAUBalanceDiff = expectedFeeFAUBalanceDiff.add(expectedToFAUBalanceDiff).mul(-1); + } await expectERC20BalanceDiffs( 'FAU', initialFromFAUBalance, @@ -348,545 +360,312 @@ describe('contract: BatchConversionPayments', async () => { }; describe('batchPayment', async () => { - describe('payment under the fee limit', async () => { - it(`make 1 ERC20 payment with no conversion`, async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - ); - - // check the balance fauERC20 token - const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - }); - it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { - const batchPayment = async () => { - return await batchConversionProxy.batchPayment( + const testBatchPayment = async (applyLimit: boolean) => { + // Limit is applied if there are paths to USD + const pathsToUSD = applyLimit + ? [ + [FAU_address, USD_hash], + [DAI_address, USD_hash], + ] + : []; + + describe(`payment with${ + applyLimit ? '' : 'out' + } application of the batch fee limit USD`, async () => { + it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + + await batchConversionProxy.batchPayment( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: [fauConvRequest, daiConvRequest, daiConvRequest], + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, + requestDetails: [fauRequest], }, ], + pathsToUSD, + feeAddress, + ); + + // check the balance fauERC20 token + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + + if (applyLimit) { + // 10$ of batch fees in FAU and request fees + expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + } + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it(`make 1 ERC20 payment with no conversion`, async () => { + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + await batchConversionProxy.batchPayment( [ - [FAU_address, USD_hash], - [DAI_address, USD_hash], + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + requestDetails: [fauRequest], + }, ], + pathsToUSD, feeAddress, ); - }; - await manyPaymentsBatchConv(batchPayment); - }); - - it('make 1 ETH payment without conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchPayment( - [ + // check the balance fauERC20 token + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + + if (applyLimit) { + // 10$ of batch fees in FAU and request fees + expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .mul(-1); + } + + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff, + expectedFeeFAUBalanceDiff, + ); + }); + it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { + const batchPayment = async () => { + return await batchConversionProxy.batchPayment( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + requestDetails: [fauConvRequest, daiConvRequest, daiConvRequest], + }, + ], + pathsToUSD, + feeAddress, + ); + }; + + if (applyLimit) { + // 10$ of batch fees converted in FAU, and 100$ of request fees converted in FAU. + // instead of (1000$ + 1$) + 100$ of fees + const expectedFeeFAUBalanceDiff = BigNumber.from('54726368159253681641'); + // 0€ of batch fees converted in DAI, and 2*100€ of fees converted in DAI. + // instead of (1000€ + 1€) + 100€ of fees without the limit + const expectedFeeDAIBalanceDiff = BigNumber.from('237623762376237623762'); + + await manyPaymentsBatchConv( + batchPayment, + expectedFeeFAUBalanceDiff, + expectedFeeDAIBalanceDiff, + ); + } else { + await manyPaymentsBatchConv(batchPayment); + } + }); + it('make 1 ETH payment without conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + tx = await batchConversionProxy.batchPayment( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + requestDetails: [ethRequest], + }, + ], + pathsToUSD, + feeAddress, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ethRequest], + value: BigNumber.from(1100 * 1e8) + .mul(USD_ETH_RATE) + .mul(2), }, - ], - [], - feeAddress, - { value: 1000 + 1 + 11 }, // + 11 to pay batch fees - ); - - await expectETHBalanceDiffs( - BigNumber.from(1000), - BigNumber.from(1), - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - false, - ); - }); + ); - it('make 1 ETH payment with 1-step conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchPayment( - [ + await expectETHBalanceDiffs( + BigNumber.from(1100 * 1e8).mul(USD_ETH_RATE), + BigNumber.from(2 * 1e8).mul(USD_ETH_RATE), + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + false, + BigNumber.from(applyLimit ? '24000000000000000' : '26000000000000000'), // 10$ + request fees in ETH + ); + }); + it('make 1 ETH payment with 1-step conversion', async () => { + // get Eth balances + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + tx = await batchConversionProxy.batchPayment( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + requestDetails: [ethConvRequest], + }, + ], + pathsToUSD, + feeAddress, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - requestDetails: [ethConvRequest], + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(2), }, - ], - [], - feeAddress, - { - value: BigNumber.from('1000' + fiatDecimals) - .mul(USD_ETH_RATE) - .mul(2), - }, - ); - - await expectETHBalanceDiffs( - BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), - BigNumber.from('1' + fiatDecimals).mul(USD_ETH_RATE), - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - ); - }); - - it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { - // get balances - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + ); - tx = await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: [fauConvRequest], - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ethRequest], - }, + await expectETHBalanceDiffs( + BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), + BigNumber.from('1' + fiatDecimals).mul(USD_ETH_RATE), + initialFromETHBalance, + initialToETHBalance, + initialFeeETHBalance, + true, + BigNumber.from(applyLimit ? '22000000000000000' : '22020000000000000'), + ); + }); + it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { + // get balances + const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = + await getERC20Balances(fauERC20); + const initialToETHBalance = await provider.getBalance(to); + const initialFeeETHBalance = await provider.getBalance(feeAddress); + const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + + tx = await batchConversionProxy.batchPayment( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + requestDetails: [fauConvRequest], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + requestDetails: [fauRequest], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, + requestDetails: [ethRequest], + }, + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, + requestDetails: [ethConvRequest], + }, + ], + pathsToUSD, + feeAddress, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - requestDetails: [ethConvRequest], - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - { - value: BigNumber.from('1000' + fiatDecimals) - .mul(USD_ETH_RATE) - .mul(2), - }, // + 11 to pay batch fees - ); - - // Chech FAU Balances // - const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); - - const [ - noConvExpectedFromFAUBalanceDiff, - noConvExpectedToFAUBalanceDiff, - noConvExpectedFeeFAUBalanceDiff, - ] = getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff.add(noConvExpectedFromFAUBalanceDiff), - expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), - expectedFeeFAUBalanceDiff.add(noConvExpectedFeeFAUBalanceDiff), - ); - - // Check ETH balances // - const receipt = await tx.wait(); - const gasAmount = receipt.gasUsed.mul(gasPrice); - - const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const toETHBalance = await provider.getBalance(to); - const feeETHBalance = await provider.getBalance(feeAddress); - const batchETHBalance = await provider.getBalance(batchConversionProxy.address); - - // Calculate the difference of the balance : now - initial - const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); - const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); - const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); - - // expectedFeeETHBalanceDiff includes batch conversion fees now - - const expectedFeeETHBalanceDiff = - // Batch conversion - BigNumber.from(1000) - .mul(1e8) - .mul(USD_ETH_RATE) - .add(1e8 * USD_ETH_RATE) - .mul(BATCH_FEE) - .div(BATCH_DENOMINATOR) - .add(1e8 * USD_ETH_RATE) - // Batch no-conversion - .add(BigNumber.from(1000).add(1).mul(BATCH_FEE).div(BATCH_DENOMINATOR).add(1)); - - const expectedToETHBalanceDiff = BigNumber.from(1000).mul(1e8).mul(USD_ETH_RATE).add(1000); - - const expectedFromETHBalanceDiff = gasAmount - .add(expectedToETHBalanceDiff) - .add(expectedFeeETHBalanceDiff) - .mul(-1); - - // Check balance changes - expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); - expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); - expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); - expect(batchETHBalance).to.equals('0', 'batchETHBalance'); - }); - }); - describe('payment above the fee limit', async () => { - before(async () => { - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10 $ - }); - after(async () => { - await batchConversionProxy - .connect(adminSigner) - .setBatchFeeAmountUSDLimit(BigNumber.from(100_000).mul(1e8)); // 100_000 $ - }); - it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); + value: BigNumber.from('1000' + fiatDecimals) + .mul(USD_ETH_RATE) + .mul(4), + }, // + 11 to pay batch fees + ); - await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - ); - - // check the balance fauERC20 token - let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - // 10$ of batch fees in FAU and request fee - expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); - expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff - .add(expectedFeeFAUBalanceDiff) - .mul(-1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - }); - it(`make 1 ERC20 payment with no conversion`, async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - ); - - // check the balance fauERC20 token - let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - // 10$ of batch fees in FAU and request fees - expectedFeeFAUBalanceDiff = BigNumber.from('104975124378109452736'); - expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff - .add(expectedFeeFAUBalanceDiff) - .mul(-1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - }); - it(`make 1 ERC20 payment with no conversion and wrong paths to USD`, async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - ], - [[DAI_address, USD_hash]], // it should be FAU_address - feeAddress, - ); - // check the balance fauERC20 token - let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - // TODO 5000+daiDecimals FAU eq 10_000$ - // The batch fee limit is not applied because of the wrong path - expectedFeeFAUBalanceDiff = BigNumber.from('1100000000000000000000'); - expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff - .add(expectedFeeFAUBalanceDiff) - .mul(-1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - }); - it('make 2 ERC20 payments on two tokens with conversion and only FAU token pays batch fee', async () => { - // get balances - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - const [initialFromDAIBalance, initialToDAIBalance, initialFeeDAIBalance] = - await getERC20Balances(daiERC20); - - tx = await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: [fauConvRequest, daiConvRequest], - }, - ], - [ - [FAU_address, USD_hash], - [DAI_address, USD_hash], - ], - feeAddress, - ); - - // Check FAU Balances // - let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); - - // 10$ of batch fees converted in FAU, and 100$ of request fees converted in FAU. - // instead of (1000$ + 1$) + 100$ of fees - expectedFeeFAUBalanceDiff = BigNumber.from('54726368159253681641'); - expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff - .add(expectedFeeFAUBalanceDiff) - .mul(-1); - - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - - // Check DAI Balances // - let [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = - getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); - - // 0€ of batch fees converted in DAI, and 100€ of fees converted in DAI. - // instead of (1000€ + 1€) + 100€ of fees without the limit - expectedFeeDAIBalanceDiff = BigNumber.from('118811881188118811881'); - expectedFromDAIBalanceDiff = expectedToDAIBalanceDiff - .add(expectedFeeDAIBalanceDiff) - .mul(-1); - - await expectERC20BalanceDiffs( - 'DAI', - initialFromDAIBalance, - initialToDAIBalance, - initialFeeDAIBalance, - expectedFromDAIBalanceDiff, - expectedToDAIBalanceDiff, - expectedFeeDAIBalanceDiff, - ); - }); - it('make 1 ETH payment without conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const USDReqAmount = 1100_00000000; // - const reqAmount = BigNumber.from(USDReqAmount).mul(USD_ETH_RATE).toString(); - tx = await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ - { - recipient: to, - requestAmount: reqAmount, - path: [], - paymentReference: referenceExample, - feeAmount: '2000', - maxToSpend: '0', - maxRateTimespan: '0', - }, - ], - }, - ], - [[]], // caution the list must not be empty to apply fee limit - feeAddress, - { value: BigNumber.from(reqAmount).mul(2) }, - ); - await expectETHBalanceDiffs( - BigNumber.from(reqAmount), - BigNumber.from('2000'), - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - false, - BigNumber.from('20000000000000000').add('2000'), // 10$ batch fees limited + invoice fees - ); - }); - it('make 1 ETH payment with 1-step conversion', async () => { - // get Eth balances - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + // Chech FAU Balances // + let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = + getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); + + const [ + noConvExpectedFromFAUBalanceDiff, + noConvExpectedToFAUBalanceDiff, + noConvExpectedFeeFAUBalanceDiff, + ] = getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); + + if (applyLimit) { + expectedFeeFAUBalanceDiff = BigNumber.from('154726368159253681641'); // 10$ of batch fee and the invoices fees + expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff + .add(expectedFeeFAUBalanceDiff) + .add(noConvExpectedToFAUBalanceDiff) + .mul(-1); + } else { + expectedFeeFAUBalanceDiff = expectedFeeFAUBalanceDiff.add( + noConvExpectedFeeFAUBalanceDiff, + ); + expectedFromFAUBalanceDiff = expectedFromFAUBalanceDiff.add( + noConvExpectedFromFAUBalanceDiff, + ); + } + await expectERC20BalanceDiffs( + 'FAU', + initialFromFAUBalance, + initialToFAUBalance, + initialFeeFAUBalance, + expectedFromFAUBalanceDiff, + expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff), + expectedFeeFAUBalanceDiff, + ); - tx = await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - requestDetails: [ethConvRequest], - }, - ], - [[]], // caution, the list must not be empty to apply batch fee limit - feeAddress, - { - value: BigNumber.from('1000' + fiatDecimals) - .mul(2) - .add(1) - .mul(USD_ETH_RATE) - .toString(), - }, - ); - - await expectETHBalanceDiffs( - BigNumber.from('1000' + fiatDecimals).mul(USD_ETH_RATE), - BigNumber.from(1).mul(USD_ETH_RATE), - initialFromETHBalance, - initialToETHBalance, - initialFeeETHBalance, - true, - BigNumber.from('22000000000000000'), // equal the sum of the batch fee (10$) and the invoice fee 1$ - ); - }); - it('make n heterogeneous (ERC20 and ETH) payments with and without conversion', async () => { - const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = - await getERC20Balances(fauERC20); - const initialToETHBalance = await provider.getBalance(to); - const initialFeeETHBalance = await provider.getBalance(feeAddress); - const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - - tx = await batchConversionProxy.batchPayment( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: [fauConvRequest], - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: [fauRequest], - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, - requestDetails: [ethRequest], - }, - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, - requestDetails: [ethConvRequest], - }, - ], - [[FAU_address, USD_hash]], - feeAddress, - { - value: BigNumber.from(1000 + 1 + 11) + // Check ETH balances // + const receipt = await tx.wait(); + const gasAmount = receipt.gasUsed.mul(gasPrice); + + const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); + const toETHBalance = await provider.getBalance(to); + const feeETHBalance = await provider.getBalance(feeAddress); + const batchETHBalance = await provider.getBalance(batchConversionProxy.address); + + // Calculate the difference of the balance : now - initial + const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); + const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); + const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); + + // expectedFeeETHBalanceDiff includes batch conversion fees now + // expect if there is the fee USD limit: batch conversion fees = 0 + const expectedFeeETHBalanceDiff = + // Batch conversion + BigNumber.from(1000) .mul(1e8) .mul(USD_ETH_RATE) - .add(1000 + 1 + 11), - }, // + 11 to pay batch fees - ); - // Chech FAU Balances // - let [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = - getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); - const [ - _noConvExpectedFromFAUBalanceDiff, - noConvExpectedToFAUBalanceDiff, - _noConvExpectedFeeFAUBalanceDiff, - ] = getExpectedERC20Balances('100000' + daiDecimals, '100' + daiDecimals, 1); - - expectedFeeFAUBalanceDiff = BigNumber.from('154726368159253681641'); // 10$ of batch fee and the invoices fees - expectedToFAUBalanceDiff = expectedToFAUBalanceDiff.add(noConvExpectedToFAUBalanceDiff); - expectedFromFAUBalanceDiff = expectedToFAUBalanceDiff - .add(expectedFeeFAUBalanceDiff) - .mul(-1); - await expectERC20BalanceDiffs( - 'FAU', - initialFromFAUBalance, - initialToFAUBalance, - initialFeeFAUBalance, - expectedFromFAUBalanceDiff, - expectedToFAUBalanceDiff, - expectedFeeFAUBalanceDiff, - ); - console.log('ETh'); - - // Check ETH balances // - const receipt = await tx.wait(); - const gasAmount = receipt.gasUsed.mul(gasPrice); - - const fromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - const toETHBalance = await provider.getBalance(to); - const feeETHBalance = await provider.getBalance(feeAddress); - const batchETHBalance = await provider.getBalance(batchConversionProxy.address); - - // Calculate the difference of the balance : now - initial - const fromETHBalanceDiff = fromETHBalance.sub(initialFromETHBalance); - const toETHBalanceDiff = toETHBalance.sub(initialToETHBalance); - const feeETHBalanceDiff = feeETHBalance.sub(initialFeeETHBalance); - - // expectedFeeETHBalanceDiff => batch conversion fees = 0 - const expectedFeeETHBalanceDiff = BigNumber.from(1).mul(1e8).mul(USD_ETH_RATE).add(1); - const expectedToETHBalanceDiff = BigNumber.from(1000).mul(1e8).mul(USD_ETH_RATE).add(1000); - const expectedFromETHBalanceDiff = gasAmount - .add(expectedToETHBalanceDiff) - .add(expectedFeeETHBalanceDiff) - .mul(-1); - // Check balance changes - expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); - expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); - expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); - expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + .add(1e8 * USD_ETH_RATE) + .mul(applyLimit ? 0 : BATCH_FEE) + .div(BATCH_DENOMINATOR) + .add(1e8 * USD_ETH_RATE) + // Batch no-conversion + .add( + BigNumber.from(1100 * 1e8) + .mul(USD_ETH_RATE) + .mul(applyLimit ? 0 : BATCH_FEE) + .div(BATCH_DENOMINATOR) + .add(BigNumber.from(2 * 1e8).mul(USD_ETH_RATE)), + ); + + const expectedToETHBalanceDiff = BigNumber.from(1000) + .mul(1e8) + .mul(USD_ETH_RATE) + .add(BigNumber.from(1100).mul(1e8).mul(USD_ETH_RATE)); + + const expectedFromETHBalanceDiff = gasAmount + .add(expectedToETHBalanceDiff) + .add(expectedFeeETHBalanceDiff) + .mul(-1); + + // Check balance changes + expect(toETHBalanceDiff).to.equals(expectedToETHBalanceDiff, 'toETHBalanceDiff'); + expect(feeETHBalanceDiff).to.equals(expectedFeeETHBalanceDiff, 'feeETHBalanceDiff'); + expect(fromETHBalanceDiff).to.equals(expectedFromETHBalanceDiff, 'DiffBalance'); + expect(batchETHBalance).to.equals('0', 'batchETHBalance'); + }); }); - }); + }; + + await testBatchPayment(true); + await testBatchPayment(false); }); + describe('batchPayment errors', async () => { it(`too many elements within batchPayment metaDetails input`, async () => { await expect( @@ -895,7 +674,7 @@ describe('contract: BatchConversionPayments', async () => { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, requestDetails: [], }), - [[FAU_address, USD_hash]], + [], feeAddress, ), ).to.be.revertedWith('more than 5 metaDetails'); @@ -909,12 +688,13 @@ describe('contract: BatchConversionPayments', async () => { requestDetails: [], }, ], - [[FAU_address, USD_hash]], + [], feeAddress, ), ).to.be.revertedWith('Wrong paymentNetworkId'); }); }); + describe('batchMultiERC20ConversionPayments', async () => { it('make 1 payment with 1-step conversion', async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = @@ -922,12 +702,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments( - [fauConvRequest], - 0, - [[FAU_address, USD_hash]], - feeAddress, - ); + .batchMultiERC20ConversionPayments([fauConvRequest], 0, [], feeAddress); const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); @@ -948,12 +723,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments( - [daiConvRequest], - 0, - [[DAI_address, USD_hash]], - feeAddress, - ); + .batchMultiERC20ConversionPayments([daiConvRequest], 0, [], feeAddress); const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); @@ -970,59 +740,41 @@ describe('contract: BatchConversionPayments', async () => { }); it('make 3 payments with different tokens and conversion length', async () => { const batchPayment = async () => { - return await batchConversionProxy.connect(fromSigner).batchMultiERC20ConversionPayments( - [fauConvRequest, daiConvRequest, daiConvRequest], - 0, - [ - [FAU_address, USD_hash], - [DAI_address, USD_hash], - ], - feeAddress, - ); + return await batchConversionProxy + .connect(fromSigner) + .batchMultiERC20ConversionPayments( + [fauConvRequest, daiConvRequest, daiConvRequest], + 0, + [], + feeAddress, + ); }; await manyPaymentsBatchConv(batchPayment); }); }); + describe('batchMultiERC20ConversionPayments errors', async () => { it('cannot transfer with invalid path', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.path = [EUR_hash, ETH_hash, DAI_address]; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments( - [convRequest], - 0, - [[FAU_address, USD_hash]], - feeAddress, - ), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), ).to.be.revertedWith('revert No aggregator found'); }); - it('cannot transfer if max to spend too low', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.maxToSpend = '1000000'; // not enough await expect( - batchConversionProxy.batchMultiERC20ConversionPayments( - [convRequest], - 0, - [[FAU_address, USD_hash]], - feeAddress, - ), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), ).to.be.revertedWith('Amount to pay is over the user limit'); }); - it('cannot transfer if rate is too old', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.maxRateTimespan = '10'; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments( - [convRequest], - 0, - [[FAU_address, USD_hash]], - feeAddress, - ), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), ).to.be.revertedWith('aggregator rate is outdated'); }); - it('Not enough allowance', async () => { const convRequest = Utils.deepCopy(fauConvRequest); // reduce fromSigner± allowance @@ -1034,15 +786,9 @@ describe('contract: BatchConversionPayments', async () => { }, ); await expect( - batchConversionProxy.batchMultiERC20ConversionPayments( - [convRequest], - 0, - [[FAU_address, USD_hash]], - feeAddress, - ), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), ).to.be.revertedWith('Insufficient allowance for batch to pay'); }); - it('Not enough funds even if partially enough funds', async () => { const convRequest = Utils.deepCopy(fauConvRequest); // fromSigner transfer enough token to pay just 1 invoice to signer4 @@ -1061,7 +807,7 @@ describe('contract: BatchConversionPayments', async () => { .batchMultiERC20ConversionPayments( [convRequest, convRequest, convRequest], 0, - [[FAU_address, USD_hash]], + [], feeAddress, ), ).to.be.revertedWith('Not enough funds, including fees'); @@ -1072,6 +818,7 @@ describe('contract: BatchConversionPayments', async () => { .transfer(from, await fauERC20.balanceOf(await signer4.getAddress())); }); }); + describe(`batchEthConversionPayments`, () => { it('make 1 payment with 1-step conversion', async function () { // get Eth balances @@ -1097,7 +844,6 @@ describe('contract: BatchConversionPayments', async () => { initialFeeETHBalance, ); }); - it('make 3 payments with different conversion lengths', async () => { // get Eth balances const initialToETHBalance = await provider.getBalance(to); @@ -1130,6 +876,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); }); + describe('batchEthConversionPayments errors', () => { it('cannot transfer with invalid path', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); @@ -1153,7 +900,6 @@ describe('contract: BatchConversionPayments', async () => { ), ).to.be.revertedWith('paymentProxy transferExactEthWithReferenceAndFee failed'); }); - it('cannot transfer if rate is too old', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.maxRateTimespan = '1'; @@ -1164,6 +910,7 @@ describe('contract: BatchConversionPayments', async () => { ).to.be.revertedWith('aggregator rate is outdated'); }); }); + describe('Functions inherited from contract BatchNoConversionPayments ', () => { it(`make 1 ERC20 payment without conversion, using batchERC20Payments`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = @@ -1180,7 +927,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - [[FAU_address, USD_hash]], + [], 0, feeAddress, ); @@ -1198,7 +945,6 @@ describe('contract: BatchConversionPayments', async () => { expectedFeeFAUBalanceDiff, ); }); - it(`make 1 ERC20 payment without conversion, using batchMultiERC20Payments`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); @@ -1214,7 +960,7 @@ describe('contract: BatchConversionPayments', async () => { maxRateTimespan: '0', }, ], - [[FAU_address, USD_hash]], + [], 0, feeAddress, ); @@ -1231,7 +977,6 @@ describe('contract: BatchConversionPayments', async () => { expectedFeeFAUBalanceDiff, ); }); - it('make 1 ETH payment without conversion', async () => { // get Eth balances const initialToETHBalance = await provider.getBalance(to); From 1d2855d65a0ce693171dcaa621002253feef5119 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 09:50:59 +0200 Subject: [PATCH 106/138] legacy - delete BatchPayment modification --- 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 df303d115d..0947637f5f 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -97,7 +97,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { // amount is updated into batch fee amount amount = (amount * batchFee) / 1000; // Check that batch contract has enough funds to pay batch fee - require(address(this).balance >= amount, 'Not enough funds for batch fee'); + require(address(this).balance >= amount, 'not enough funds for batch fee'); // Batch pays batch fee _feeAddress.transfer(amount); @@ -147,7 +147,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { requestedToken.allowance(msg.sender, address(this)) >= amount, 'Not sufficient allowance for batch to pay' ); - require(requestedToken.balanceOf(msg.sender) >= amount, 'Not enough funds'); + require(requestedToken.balanceOf(msg.sender) >= amount, 'not enough funds'); require( safeTransferFrom(_tokenAddress, address(this), amount), 'payment transferFrom() failed' From d2f0cfef8042c6c6275e3c75835f3100b3cfe226 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 13:04:07 +0200 Subject: [PATCH 107/138] add privates functions - rename enable by skip fee USD limit - change variable uint size - --- .../src/contracts/BatchConversionPayments.sol | 86 +++++++++------ .../contracts/BatchNoConversionPayments.sol | 101 ++++++++++++------ .../BatchConversionPayments/0.1.0.json | 45 ++------ .../contracts/BatchConversionPayments.test.ts | 28 ++--- .../BatchNoConversionErc20Payments.test.ts | 23 ++-- .../BatchNoConversionEthPayments.test.ts | 16 +-- 6 files changed, 156 insertions(+), 143 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 1e8da93004..e3e4a09b29 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -86,31 +86,25 @@ contract BatchConversionPayments is BatchNoConversionPayments { ) external payable { require(metaDetails.length < 6, 'more than 5 metaDetails'); - // Check that there are paths to USD, and more than one paymentNetworkId - if (pathsToUSD.length > 0 && metaDetails.length > 1) { - // Set to true to avoid batchFeeAmountUSD to be reset by each batch function - batchPaymentOrigin = true; - } - uint256 batchFeeAmountUSD = 0; for (uint256 i = 0; i < metaDetails.length; i++) { MetaDetail calldata metaDetail = metaDetails[i]; if (metaDetail.paymentNetworkId == 0) { - batchFeeAmountUSD += batchMultiERC20ConversionPayments( + batchFeeAmountUSD += _batchMultiERC20ConversionPayments( metaDetail.requestDetails, batchFeeAmountUSD, pathsToUSD, feeAddress ); } else if (metaDetail.paymentNetworkId == 1) { - batchFeeAmountUSD += batchERC20Payments( + batchFeeAmountUSD += _batchERC20Payments( metaDetail.requestDetails, pathsToUSD, batchFeeAmountUSD, - feeAddress + payable(feeAddress) ); } else if (metaDetail.paymentNetworkId == 2) { - batchFeeAmountUSD += batchMultiERC20Payments( + batchFeeAmountUSD += _batchMultiERC20Payments( metaDetail.requestDetails, pathsToUSD, batchFeeAmountUSD, @@ -121,9 +115,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { // Set to false only if batchEthConversionPayments is called after this function transferBackRemainingEth = false; } - batchFeeAmountUSD += batchEthPayments( + batchFeeAmountUSD += _batchEthPayments( metaDetail.requestDetails, - pathsToUSD.length > 0, + pathsToUSD.length == 0, batchFeeAmountUSD, payable(feeAddress) ); @@ -131,9 +125,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { transferBackRemainingEth = true; } } else if (metaDetail.paymentNetworkId == 4) { - batchFeeAmountUSD += batchEthConversionPayments( + batchFeeAmountUSD += _batchEthConversionPayments( metaDetail.requestDetails, - pathsToUSD.length > 0, + pathsToUSD.length == 0, batchFeeAmountUSD, payable(feeAddress) ); @@ -141,31 +135,59 @@ contract BatchConversionPayments is BatchNoConversionPayments { revert('Wrong paymentNetworkId'); } } - if (pathsToUSD.length > 0 && metaDetails.length > 1) { - // Set back to false, its default value - batchPaymentOrigin = false; - } } /** * @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 requestDetails List of ERC20 requests denominated in fiat to pay. - * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * Without paths, there is not limitation. + * Without paths, there is not a fee limitation, and it consumes less gas. * @param feeAddress The fee recipient. */ function batchMultiERC20ConversionPayments( RequestDetail[] calldata requestDetails, - uint256 batchFeeAmountUSD, address[][] calldata pathsToUSD, address feeAddress ) public returns (uint256) { - // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true) { - batchFeeAmountUSD = 0; - } + return _batchMultiERC20ConversionPayments(requestDetails, 0, pathsToUSD, feeAddress); + } + + /** + * @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 requestDetails List of ETH requests denominated in fiat to pay. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param feeAddress The fee recipient. + * @dev It uses EthereumConversionProxy to pay an invoice and fees. + * Please: + * 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 batchEthConversionPayments( + RequestDetail[] calldata requestDetails, + bool skipFeeUSDLimit, + address payable feeAddress + ) public payable returns (uint256) { + return _batchEthConversionPayments(requestDetails, skipFeeUSDLimit, 0, feeAddress); + } + + /** + * @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 requestDetails List of ERC20 requests denominated in fiat to pay. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not a fee limitation, and it consumes less gas. + * @param feeAddress The fee recipient. + */ + function _batchMultiERC20ConversionPayments( + RequestDetail[] calldata requestDetails, + uint256 batchFeeAmountUSD, + address[][] calldata pathsToUSD, + address feeAddress + ) private returns (uint256) { Token[] memory uTokens = getUTokens(requestDetails); IERC20 requestedToken; @@ -231,7 +253,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @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 requestDetails List of ETH requests denominated in fiat to pay. - * @param applyFeeLimitUSD It set to true to apply the USD fee limit. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses EthereumConversionProxy to pay an invoice and fees. @@ -240,16 +262,12 @@ contract BatchConversionPayments is BatchNoConversionPayments { * 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 batchEthConversionPayments( + function _batchEthConversionPayments( RequestDetail[] calldata requestDetails, - bool applyFeeLimitUSD, + bool skipFeeUSDLimit, uint256 batchFeeAmountUSD, address payable feeAddress - ) public payable returns (uint256) { - // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true && applyFeeLimitUSD) { - batchFeeAmountUSD = 0; - } + ) private returns (uint256) { uint256 contractBalance = address(this).balance; payerAuthorized = true; @@ -271,7 +289,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { uint256 batchFeeToPay = (((contractBalance - address(this).balance)) * batchFee) / feeDenominator; - if (applyFeeLimitUSD == true) { + if (skipFeeUSDLimit == false) { (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( batchFeeToPay, pathsEthToUSD[0][0], diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index cb0b1f031e..141782b563 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -29,16 +29,14 @@ contract BatchNoConversionPayments is Ownable { ChainlinkConversionPath public chainlinkConversionPath; /** Used to calculate batch fees: batchFee = 30 represent 0.30% of fee */ - uint256 public batchFee; + uint16 public batchFee; /** Used to calculate batch fees: divide batchFee by feeDenominator */ - uint256 internal feeDenominator = 10000; + uint16 internal feeDenominator = 10000; /** The amount of the batch fee cannot exceed a predefined amount in USD */ - uint256 public batchFeeAmountUSDLimit; + uint64 public batchFeeAmountUSDLimit; /** payerAuthorized is set to true only when needed for batch Eth conversion */ bool internal payerAuthorized = false; - /** batchPayment function is the caller */ - bool internal batchPaymentOrigin = false; /** transferBackRemainingEth is set to false only if the payer use batchPayment and call both batchEthPayments and batchConversionEthPaymentsWithReference */ bool internal transferBackRemainingEth = true; @@ -108,22 +106,71 @@ contract BatchNoConversionPayments is Ownable { * @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 requestDetails List of ETH requests to pay. - * @param applyFeeLimitUSD It set to true to apply the USD fee limit. - * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @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 batchEthPayments( RequestDetail[] calldata requestDetails, - bool applyFeeLimitUSD, - uint256 batchFeeAmountUSD, + bool skipFeeUSDLimit, address payable feeAddress ) public payable returns (uint256) { - // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true && applyFeeLimitUSD == true) { - batchFeeAmountUSD = 0; - } + return _batchEthPayments(requestDetails, skipFeeUSDLimit, 0, payable(feeAddress)); + } + + /** + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. + * @param requestDetails List of ERC20 requests to pay, with only one ERC20 token. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not a fee limitation, and it consumes less gas. + * @param feeAddress The fee recipient. + * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * 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 batchERC20Payments( + RequestDetail[] calldata requestDetails, + address[][] calldata pathsToUSD, + address feeAddress + ) public returns (uint256) { + return _batchERC20Payments(requestDetails, pathsToUSD, 0, feeAddress); + } + + /** + * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. + * @param requestDetails List of ERC20 requests to pay. + * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. + * Without paths, there is not a fee limitation, and it consumes less gas. + * @param feeAddress The fee recipient. + * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. + * 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 batchMultiERC20Payments( + RequestDetail[] calldata requestDetails, + address[][] calldata pathsToUSD, + address feeAddress + ) public returns (uint256) { + return _batchMultiERC20Payments(requestDetails, pathsToUSD, 0, feeAddress); + } + + /** + * @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 requestDetails List of ETH requests to pay. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param batchFeeAmountUSD The batch fee amount in USD already paid. + * @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 _batchEthPayments( + RequestDetail[] calldata requestDetails, + bool skipFeeUSDLimit, + uint256 batchFeeAmountUSD, + address payable feeAddress + ) internal returns (uint256) { // amount is used to get the total amount and then used as batch fee amount uint256 amount = 0; @@ -143,7 +190,7 @@ contract BatchNoConversionPayments is Ownable { // amount is updated into batch fee amount amount = (amount * batchFee) / feeDenominator; - if (applyFeeLimitUSD == true) { + if (skipFeeUSDLimit == false) { (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( amount, pathsEthToUSD[0][0], @@ -168,23 +215,19 @@ contract BatchNoConversionPayments is Ownable { * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts. * @param requestDetails List of ERC20 requests to pay, with only one ERC20 token. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * Without paths, there is not limitation. + * Without paths, there is not a fee limitation, and it consumes less gas. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev Uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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 batchERC20Payments( + function _batchERC20Payments( RequestDetail[] calldata requestDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, address feeAddress - ) public returns (uint256) { - // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true) { - batchFeeAmountUSD = 0; - } + ) internal returns (uint256) { uint256 amountAndFee = 0; uint256 batchFeeAmount = 0; for (uint256 i = 0; i < requestDetails.length; i++) { @@ -236,23 +279,19 @@ contract BatchNoConversionPayments is Ownable { * @notice Send a batch of ERC20 payments with fees and paymentReferences to multiple accounts, with multiple tokens. * @param requestDetails List of ERC20 requests to pay. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * Without paths, there is not limitation. + * Without paths, there is not a fee limitation, and it consumes less gas. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses ERC20FeeProxy to pay an invoice and fees, with a payment reference. * 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 batchMultiERC20Payments( + function _batchMultiERC20Payments( RequestDetail[] calldata requestDetails, address[][] calldata pathsToUSD, uint256 batchFeeAmountUSD, address feeAddress - ) public returns (uint256) { - // Avoid the possibility to manually put high value to batchFeeAmountUSD - if (batchPaymentOrigin != true) { - batchFeeAmountUSD = 0; - } + ) internal returns (uint256) { Token[] memory uTokens = getUTokens(requestDetails); // The payer transfers tokens to the batch contract and pays batch fee @@ -398,7 +437,7 @@ contract BatchNoConversionPayments is Ownable { * @param tokenAddress The address of the token * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * Without paths, there is not limitation. + * Without paths, there is not a fee limitation, and it consumes less gas. */ function calculateBatchFeeToPay( uint256 batchFeeToPay, @@ -505,7 +544,7 @@ contract BatchNoConversionPayments is Ownable { * @notice Fees added when using Erc20/Eth batch functions * @param _batchFee Between 0 and 200, i.e: batchFee = 30 represent 0.30% of fee */ - function setBatchFee(uint256 _batchFee) external onlyOwner { + function setBatchFee(uint16 _batchFee) external onlyOwner { // safety to avoid wrong setting require(_batchFee <= 200, 'The batch fee value is too high: > 2%'); batchFee = _batchFee; @@ -548,7 +587,7 @@ contract BatchNoConversionPayments is Ownable { /** * @param _batchFeeAmountUSDLimit The limitation of the batch fee amount in USD. */ - function setBatchFeeAmountUSDLimit(uint256 _batchFeeAmountUSDLimit) external onlyOwner { + function setBatchFeeAmountUSDLimit(uint64 _batchFeeAmountUSDLimit) external onlyOwner { batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; } } 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 06ee8dc623..ef868c09e2 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 @@ -130,11 +130,6 @@ "name": "pathsToUSD", "type": "address[][]" }, - { - "internalType": "uint256", - "name": "batchFeeAmountUSD", - "type": "uint256" - }, { "internalType": "address", "name": "feeAddress", @@ -198,14 +193,9 @@ }, { "internalType": "bool", - "name": "applyFeeLimitUSD", + "name": "skipFeeUSDLimit", "type": "bool" }, - { - "internalType": "uint256", - "name": "batchFeeAmountUSD", - "type": "uint256" - }, { "internalType": "address payable", "name": "feeAddress", @@ -269,14 +259,9 @@ }, { "internalType": "bool", - "name": "applyFeeLimitUSD", + "name": "skipFeeUSDLimit", "type": "bool" }, - { - "internalType": "uint256", - "name": "batchFeeAmountUSD", - "type": "uint256" - }, { "internalType": "address payable", "name": "feeAddress", @@ -299,9 +284,9 @@ "name": "batchFee", "outputs": [ { - "internalType": "uint256", + "internalType": "uint16", "name": "", - "type": "uint256" + "type": "uint16" } ], "stateMutability": "view", @@ -312,9 +297,9 @@ "name": "batchFeeAmountUSDLimit", "outputs": [ { - "internalType": "uint256", + "internalType": "uint64", "name": "", - "type": "uint256" + "type": "uint64" } ], "stateMutability": "view", @@ -364,11 +349,6 @@ "name": "requestDetails", "type": "tuple[]" }, - { - "internalType": "uint256", - "name": "batchFeeAmountUSD", - "type": "uint256" - }, { "internalType": "address[][]", "name": "pathsToUSD", @@ -440,11 +420,6 @@ "name": "pathsToUSD", "type": "address[][]" }, - { - "internalType": "uint256", - "name": "batchFeeAmountUSD", - "type": "uint256" - }, { "internalType": "address", "name": "feeAddress", @@ -646,9 +621,9 @@ { "inputs": [ { - "internalType": "uint256", + "internalType": "uint16", "name": "_batchFee", - "type": "uint256" + "type": "uint16" } ], "name": "setBatchFee", @@ -659,9 +634,9 @@ { "inputs": [ { - "internalType": "uint256", + "internalType": "uint64", "name": "_batchFeeAmountUSDLimit", - "type": "uint256" + "type": "uint64" } ], "name": "setBatchFeeAmountUSDLimit", diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index ee0c3b32f3..6778c8c20d 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -702,7 +702,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments([fauConvRequest], 0, [], feeAddress); + .batchMultiERC20ConversionPayments([fauConvRequest], [], feeAddress); const [expectedFromFAUBalanceDiff, expectedToFAUBalanceDiff, expectedFeeFAUBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'USD_FAU'); @@ -723,7 +723,7 @@ describe('contract: BatchConversionPayments', async () => { await batchConversionProxy .connect(fromSigner) - .batchMultiERC20ConversionPayments([daiConvRequest], 0, [], feeAddress); + .batchMultiERC20ConversionPayments([daiConvRequest], [], feeAddress); const [expectedFromDAIBalanceDiff, expectedToDAIBalanceDiff, expectedFeeDAIBalanceDiff] = getExpectedConvERC20Balances(100000, 100, 1, 'EUR_DAI'); @@ -744,7 +744,6 @@ describe('contract: BatchConversionPayments', async () => { .connect(fromSigner) .batchMultiERC20ConversionPayments( [fauConvRequest, daiConvRequest, daiConvRequest], - 0, [], feeAddress, ); @@ -758,21 +757,21 @@ describe('contract: BatchConversionPayments', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.path = [EUR_hash, ETH_hash, DAI_address]; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], [], feeAddress), ).to.be.revertedWith('revert No aggregator found'); }); it('cannot transfer if max to spend too low', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.maxToSpend = '1000000'; // not enough await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], [], feeAddress), ).to.be.revertedWith('Amount to pay is over the user limit'); }); it('cannot transfer if rate is too old', async () => { const convRequest = Utils.deepCopy(fauConvRequest); convRequest.maxRateTimespan = '10'; await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], [], feeAddress), ).to.be.revertedWith('aggregator rate is outdated'); }); it('Not enough allowance', async () => { @@ -786,7 +785,7 @@ describe('contract: BatchConversionPayments', async () => { }, ); await expect( - batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], 0, [], feeAddress), + batchConversionProxy.batchMultiERC20ConversionPayments([convRequest], [], feeAddress), ).to.be.revertedWith('Insufficient allowance for batch to pay'); }); it('Not enough funds even if partially enough funds', async () => { @@ -806,7 +805,6 @@ describe('contract: BatchConversionPayments', async () => { .connect(signer4) .batchMultiERC20ConversionPayments( [convRequest, convRequest, convRequest], - 0, [], feeAddress, ), @@ -827,8 +825,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); tx = await batchConversionProxy.batchEthConversionPayments( [ethConvRequest], - false, - 0, + true, feeAddress, { value: BigNumber.from('1000' + fiatDecimals) @@ -854,8 +851,7 @@ describe('contract: BatchConversionPayments', async () => { tx = await batchConversionProxy.batchEthConversionPayments( [ethConvRequest, EurEthConvRequest, ethConvRequest], - false, - 0, + true, feeAddress, { value: BigNumber.from('1000' + fiatDecimals) @@ -882,7 +878,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.path = [USD_hash, EUR_hash, ETH_hash]; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }), ).to.be.revertedWith('No aggregator found'); @@ -892,7 +888,6 @@ describe('contract: BatchConversionPayments', async () => { batchConversionProxy.batchEthConversionPayments( [ethConvRequest, ethConvRequest], false, - 0, feeAddress, { value: (2000 + 1) * USD_ETH_RATE, // no enough to pay the amount AND the fees @@ -904,7 +899,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.maxRateTimespan = '1'; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, 0, feeAddress, { + batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, feeAddress, { value: 1000 + 1 + 11, // + 11 to pay batch fees }), ).to.be.revertedWith('aggregator rate is outdated'); @@ -928,7 +923,6 @@ describe('contract: BatchConversionPayments', async () => { }, ], [], - 0, feeAddress, ); @@ -961,7 +955,6 @@ describe('contract: BatchConversionPayments', async () => { }, ], [], - 0, feeAddress, ); @@ -995,7 +988,6 @@ describe('contract: BatchConversionPayments', async () => { }, ], false, - 0, feeAddress, { value: 1000 + 1 + 11 }, // + 11 to pay batch fees ); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts index a08e1e210f..eb02ac317a 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionErc20Payments.test.ts @@ -151,7 +151,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }, ], [[token1Address, USD_hash]], - 0, feeAddress, ), ) @@ -254,7 +253,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }, ], [], - 0, feeAddress, ), ) @@ -373,7 +371,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [token2Address, USD_hash], [token3Address, USD_hash], ], - 0, feeAddress, ); @@ -432,7 +429,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [token1Address, USD_hash], [token2Address, USD_hash], ], - 0, feeAddress, ); await tx.wait(); @@ -476,7 +472,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { }), ], [[token1Address, USD_hash]], - 0, feeAddress, ); await tx.wait(); @@ -532,7 +527,6 @@ describe('contract: batchNoConversionPayments: ERC20', () => { [token1Address, USD_hash], [token2Address, USD_hash], ], - 0, feeAddress, ); @@ -590,7 +584,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], feeAddress), ).revertedWith('Not enough funds, including fees'); }); @@ -603,12 +597,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20Payments( - requestDetails.slice(0, 2), - [[token1Address, USD_hash]], - 0, - feeAddress, - ), + .batchERC20Payments(requestDetails.slice(0, 2), [[token1Address, USD_hash]], feeAddress), ).revertedWith('Not enough funds, including fees'); }); @@ -619,7 +608,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchERC20Payments(requestDetails, [[token1Address, USD_hash]], feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); @@ -631,7 +620,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], feeAddress), ).revertedWith('Not enough funds'); }); @@ -646,7 +635,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], feeAddress), ).revertedWith('Not enough funds'); }); @@ -660,7 +649,7 @@ describe('contract: batchNoConversionPayments: ERC20', () => { await expect( batch .connect(spender3) - .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], 0, feeAddress), + .batchMultiERC20Payments(requestDetails, [[token1Address, USD_hash]], feeAddress), ).revertedWith('Insufficient allowance for batch to pay'); }); }); diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 3987c3782a..1679e3c99b 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -99,7 +99,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], false, 0, feeAddress, { + .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], true, feeAddress, { value: BigNumber.from('6000'), }), ) @@ -128,7 +128,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }); await tx.wait(); @@ -156,7 +156,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), false, 0, feeAddress, { + .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), true, feeAddress, { value: totalAmount, }); @@ -182,7 +182,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds'); @@ -205,7 +205,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds for batch fee'); @@ -222,10 +222,10 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { describe('Function allowed only to the owner', () => { it('Should allow the owner to update batchFee', async () => { - const beforeBatchFee = await batch.batchFee.call({ from: owner }); + const beforeBatchFee = BigNumber.from(await batch.batchFee.call({ from: owner })); let tx = await batch.connect(owner).setBatchFee(beforeBatchFee.add(100)); await tx.wait(); - const afterBatchFee = await batch.batchFee.call({ from: owner }); + const afterBatchFee = BigNumber.from(await batch.batchFee.call({ from: owner })); expect(afterBatchFee).to.be.equal(beforeBatchFee.add(100)); }); @@ -235,7 +235,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], false, 0, feeAddress, { + .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: BigNumber.from('1000'), }); await tx.wait(); From 05cd4666cbac98ff92f34f4b491660d391807b3d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 15:25:17 +0200 Subject: [PATCH 108/138] rename ETH functions into Native functions --- ...test-deploy-batch-conversion-deployment.ts | 11 +-- .../src/contracts/BatchConversionPayments.sol | 75 ++++++++--------- .../contracts/BatchNoConversionPayments.sol | 82 ++++++++++--------- .../src/contracts/BatchPayments.sol | 2 +- .../BatchConversionPayments/0.1.0.json | 28 +++---- .../BatchNoConversionPayments/0.1.0.json | 10 +-- .../lib/artifacts/BatchPayments/0.1.0.json | 2 +- .../contracts/BatchConversionPayments.test.ts | 18 ++-- .../BatchNoConversionEthPayments.test.ts | 14 ++-- 9 files changed, 123 insertions(+), 119 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 9fbc0b2ffe..ce9bdd5368 100644 --- a/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts +++ b/packages/smart-contracts/scripts/test-deploy-batch-conversion-deployment.ts @@ -4,6 +4,7 @@ import { deployOne } from './deploy-one'; import { batchConversionPaymentsArtifact, + chainlinkConversionPath, erc20ConversionProxy, erc20FeeProxyArtifact, ethConversionArtifact, @@ -27,8 +28,8 @@ export async function deployBatchConversionPayment( const _ERC20FeeProxyAddress = erc20FeeProxyArtifact.getAddress('private'); const _EthereumFeeProxyAddress = ethereumFeeProxyArtifact.getAddress('private'); const _paymentErc20ConversionFeeProxy = erc20ConversionProxy.getAddress('private'); - const _paymentEthConversionFeeProxy = ethConversionArtifact.getAddress('private'); - const _chainlinkConversionPath = '0x4e71920b7330515faf5EA0c690f1aD06a85fB60c'; + const _paymentNativeConversionFeeProxy = ethConversionArtifact.getAddress('private'); + const _chainlinkConversionPath = chainlinkConversionPath.getAddress('private'); // Deploy BatchConversionPayments contract const { address: BatchConversionPaymentsAddress } = await deployOne( @@ -40,7 +41,7 @@ export async function deployBatchConversionPayment( _ERC20FeeProxyAddress, _EthereumFeeProxyAddress, _paymentErc20ConversionFeeProxy, - _paymentEthConversionFeeProxy, + _paymentNativeConversionFeeProxy, _chainlinkConversionPath, await (await hre.ethers.getSigners())[0].getAddress(), ], @@ -78,11 +79,11 @@ export async function deployBatchConversionPayment( await batchConversion.connect(owner).setBatchFee(30); await batchConversion .connect(owner) - .setETHAndUSDAddress( + .setNativeAndUSDAddress( currencyManager.fromSymbol('ETH')!.hash, currencyManager.fromSymbol('USD')!.hash, ); - await batchConversion.connect(owner).setBatchFeeAmountUSDLimit(300); // * 1_000_000_000_000_000_000); + await batchConversion.connect(owner).setBatchFeeAmountUSDLimit(150 * 1e8); // 150$ // ---------------------------------- console.log('Contracts deployed'); diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index e3e4a09b29..3a47a0def2 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -22,7 +22,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { using SafeERC20 for IERC20; IERC20ConversionProxy public paymentErc20ConversionProxy; - IEthConversionProxy public paymentEthConversionProxy; + IEthConversionProxy public paymentNativeConversionProxy; /** * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network: @@ -36,29 +36,29 @@ contract BatchConversionPayments is BatchNoConversionPayments { /** * @param _paymentErc20Proxy The ERC20 payment proxy address to use. - * @param _paymentEthProxy The ETH payment proxy address to use. + * @param _paymentNativeProxy The native 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 _paymentNativeConversionFeeProxy The native Conversion payment proxy address to use. * @param _chainlinkConversionPathAddress The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( address _paymentErc20Proxy, - address _paymentEthProxy, + address _paymentNativeProxy, address _paymentErc20ConversionProxy, - address _paymentEthConversionFeeProxy, + address _paymentNativeConversionFeeProxy, address _chainlinkConversionPathAddress, address _owner ) BatchNoConversionPayments( _paymentErc20Proxy, - _paymentEthProxy, + _paymentNativeProxy, _chainlinkConversionPathAddress, _owner ) { paymentErc20ConversionProxy = IERC20ConversionProxy(_paymentErc20ConversionProxy); - paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionFeeProxy); + paymentNativeConversionProxy = IEthConversionProxy(_paymentNativeConversionFeeProxy); } /** @@ -67,12 +67,12 @@ contract BatchConversionPayments is BatchNoConversionPayments { * - batchMultiERC20ConversionPayments, paymentNetworkId=0 * - batchERC20Payments, paymentNetworkId=1 * - batchMultiERC20Payments, paymentNetworkId=2 - * - batchEthPayments, paymentNetworkId=3 - * - batchEthConversionPayments, paymentNetworkId=4 + * - batchNativePayments, paymentNetworkId=3 + * - batchNativeConversionPayments, paymentNetworkId=4 * If metaDetails use paymentNetworkId = 4, it must be at the end of the list, or the transaction can be reverted. * @param pathsToUSD The list of paths into USD for every token, used to limit the batch fees. - * For batchEth, mock an array of array to apply the limit, e.g: [[]] - * Without paths, there is not limitation, neither for the batchEth functions. + * For batch native, mock an array of array to apply the limit, e.g: [[]] + * Without paths, there is not limitation, neither for the batch native functions. * @param feeAddress The address where fees should be paid. * @dev Use pathsToUSD only if you are pretty sure the batch fees will higher than the * USD limit batchFeeAmountUSDLimit, because it increase gas consumption. @@ -112,20 +112,20 @@ contract BatchConversionPayments is BatchNoConversionPayments { ); } else if (metaDetail.paymentNetworkId == 3) { if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { - // Set to false only if batchEthConversionPayments is called after this function - transferBackRemainingEth = false; + // Set to false only if batchNativeConversionPayments is called after this function + transferBackRemainingNativeTokens = false; } - batchFeeAmountUSD += _batchEthPayments( + batchFeeAmountUSD += _batchNativePayments( metaDetail.requestDetails, pathsToUSD.length == 0, batchFeeAmountUSD, payable(feeAddress) ); if (metaDetails[metaDetails.length - 1].paymentNetworkId == 4) { - transferBackRemainingEth = true; + transferBackRemainingNativeTokens = true; } } else if (metaDetail.paymentNetworkId == 4) { - batchFeeAmountUSD += _batchEthConversionPayments( + batchFeeAmountUSD += _batchNativeConversionPayments( metaDetail.requestDetails, pathsToUSD.length == 0, batchFeeAmountUSD, @@ -154,23 +154,22 @@ contract BatchConversionPayments is BatchNoConversionPayments { } /** - * @notice Send a batch of ETH conversion payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native conversion payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch is reverted. - * @param requestDetails List of ETH requests denominated in fiat to pay. + * @param requestDetails List of native requests denominated in fiat to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @param feeAddress The fee recipient. - * @dev It uses EthereumConversionProxy to pay an invoice and fees. + * @dev It uses NativeConversionProxy (EthereumConversionProxy) to pay an invoice and fees. * Please: - * Note that if there is not enough ether attached to the function call, + * Note that if there is not enough Native token 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 batchEthConversionPayments( + function batchNativeConversionPayments( RequestDetail[] calldata requestDetails, bool skipFeeUSDLimit, address payable feeAddress ) public payable returns (uint256) { - return _batchEthConversionPayments(requestDetails, skipFeeUSDLimit, 0, feeAddress); + return _batchNativeConversionPayments(requestDetails, skipFeeUSDLimit, 0, feeAddress); } /** @@ -250,19 +249,18 @@ contract BatchConversionPayments is BatchNoConversionPayments { } /** - * @notice Send a batch of ETH conversion payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native conversion payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch is reverted. - * @param requestDetails List of ETH requests denominated in fiat to pay. + * @param requestDetails List of native requests denominated in fiat to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. - * @dev It uses EthereumConversionProxy to pay an invoice and fees. + * @dev It uses NativeConversionProxy (EthereumConversionProxy) to pay an invoice and fees. * Please: - * Note that if there is not enough ether attached to the function call, + * Note that if there is not enough Native token 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 _batchEthConversionPayments( + function _batchNativeConversionPayments( RequestDetail[] calldata requestDetails, bool skipFeeUSDLimit, uint256 batchFeeAmountUSD, @@ -271,10 +269,10 @@ contract BatchConversionPayments is BatchNoConversionPayments { uint256 contractBalance = address(this).balance; payerAuthorized = true; - // Batch contract pays the requests through EthConversionProxy + // Batch contract pays the requests through nativeConversionProxy for (uint256 i = 0; i < requestDetails.length; i++) { RequestDetail memory rD = requestDetails[i]; - paymentEthConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( + paymentNativeConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( payable(rD.recipient), rD.requestAmount, rD.path, @@ -292,16 +290,16 @@ contract BatchConversionPayments is BatchNoConversionPayments { if (skipFeeUSDLimit == false) { (batchFeeToPay, batchFeeAmountUSD) = calculateBatchFeeToPay( batchFeeToPay, - pathsEthToUSD[0][0], + pathsNativeToUSD[0][0], batchFeeAmountUSD, - pathsEthToUSD + pathsNativeToUSD ); } require(address(this).balance >= batchFeeToPay, 'Not enough funds for batch conversion fees'); feeAddress.transfer(batchFeeToPay); - // Batch contract transfers the remaining ethers to the payer + // Batch contract transfers the remaining native tokens 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; @@ -322,10 +320,13 @@ contract BatchConversionPayments is BatchNoConversionPayments { } /** - * @param _paymentEthConversionProxy The address of the Ethereum Conversion payment proxy to use. + * @param _paymentNativeConversionProxy The address of the native Conversion payment proxy to use. * Update cautiously, the proxy has to match the invoice proxy. */ - function setPaymentEthConversionProxy(address _paymentEthConversionProxy) external onlyOwner { - paymentEthConversionProxy = IEthConversionProxy(_paymentEthConversionProxy); + function setPaymentNativeConversionProxy(address _paymentNativeConversionProxy) + external + onlyOwner + { + paymentNativeConversionProxy = IEthConversionProxy(_paymentNativeConversionProxy); } } diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 141782b563..2dd11e68af 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -11,9 +11,9 @@ import './ChainlinkConversionPath.sol'; /** * @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 + * - on: ERC20 Payment Proxy and Native (ETH) Payment Proxy of the Request Network protocol * - to: multiple addresses - * - fees: ERC20 and ETH proxies fees are paid to the same address + * - fees: ERC20 and Native (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 three main modifications: @@ -25,25 +25,26 @@ contract BatchNoConversionPayments is Ownable { using SafeERC20 for IERC20; IERC20FeeProxy public paymentErc20Proxy; - IEthereumFeeProxy public paymentEthProxy; + IEthereumFeeProxy public paymentNativeProxy; ChainlinkConversionPath public chainlinkConversionPath; /** Used to calculate batch fees: batchFee = 30 represent 0.30% of fee */ uint16 public batchFee; /** Used to calculate batch fees: divide batchFee by feeDenominator */ uint16 internal feeDenominator = 10000; - /** The amount of the batch fee cannot exceed a predefined amount in USD */ + /** The amount of the batch fee cannot exceed a predefined amount in USD, e.g: + batchFeeAmountUSDLimit = 150 * 1e8 represents $150 */ uint64 public batchFeeAmountUSDLimit; - /** payerAuthorized is set to true only when needed for batch Eth conversion */ + /** payerAuthorized is set to true only when needed for batch native conversion */ bool internal payerAuthorized = false; - /** transferBackRemainingEth is set to false only if the payer use batchPayment - and call both batchEthPayments and batchConversionEthPaymentsWithReference */ - bool internal transferBackRemainingEth = true; + /** transferBackRemainingNativeTokens is set to false only if the payer use batchPayment + and call both batchNativePayments and batchNativeConversionPayments */ + bool internal transferBackRemainingNativeTokens = true; address public USDAddress; - address public ETHAddress; - address[][] public pathsEthToUSD; + address public NativeAddress; + address[][] public pathsNativeToUSD; /** Contains the address of a token, the sum of the amount and fees paid with it, and the batch fee amount */ struct Token { @@ -77,46 +78,46 @@ contract BatchNoConversionPayments is Ownable { /** * @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 _paymentNativeProxy The address to the Native fee payment proxy to use. * @param _chainlinkConversionPathAddress The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( address _paymentErc20Proxy, - address _paymentEthProxy, + address _paymentNativeProxy, address _chainlinkConversionPathAddress, address _owner ) { paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); - paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); + paymentNativeProxy = IEthereumFeeProxy(_paymentNativeProxy); chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); transferOwnership(_owner); batchFee = 0; } /** - * This contract is non-payable. Making an ETH payment with conversion requires the contract to accept incoming ETH. - * @dev See the end of `paymentEthConversionProxy.transferWithReferenceAndFee` where the leftover is given back. + * This contract is non-payable. Making a Native payment with conversion requires the contract to accept incoming Native tokens. + * @dev See the end of `paymentNativeConversionProxy.transferWithReferenceAndFee` where the leftover is given back. */ receive() external payable { require(payerAuthorized || msg.value == 0, 'Non-payable'); } /** - * @notice Send a batch of ETH (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native (or EVM native token) payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. - * @param requestDetails List of ETH requests to pay. + * @param requestDetails List of Native tokens requests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @param feeAddress The fee recipient. - * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. + * @dev It uses NativeFeeProxy (EthereumFeeProxy) to pay an invoice and fees with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount */ - function batchEthPayments( + function batchNativePayments( RequestDetail[] calldata requestDetails, bool skipFeeUSDLimit, address payable feeAddress ) public payable returns (uint256) { - return _batchEthPayments(requestDetails, skipFeeUSDLimit, 0, payable(feeAddress)); + return _batchNativePayments(requestDetails, skipFeeUSDLimit, 0, payable(feeAddress)); } /** @@ -156,16 +157,16 @@ contract BatchNoConversionPayments is Ownable { } /** - * @notice Send a batch of ETH (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native (or EVM native token) payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. - * @param requestDetails List of ETH requests to pay. + * @param requestDetails List of Native tokens requests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. - * @dev It uses EthereumFeeProxy to pay an invoice and fees with a payment reference. + * @dev It uses NativeFeeProxy (EthereumFeeProxy) to pay an invoice and fees with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount */ - function _batchEthPayments( + function _batchNativePayments( RequestDetail[] calldata requestDetails, bool skipFeeUSDLimit, uint256 batchFeeAmountUSD, @@ -174,13 +175,13 @@ contract BatchNoConversionPayments is Ownable { // 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 + // Batch contract pays the requests thourgh NativeFeeProxy (EthFeeProxy) for (uint256 i = 0; i < requestDetails.length; i++) { RequestDetail memory rD = requestDetails[i]; require(address(this).balance >= rD.requestAmount + rD.feeAmount, 'Not enough funds'); amount += rD.requestAmount; - paymentEthProxy.transferWithReferenceAndFee{value: rD.requestAmount + rD.feeAmount}( + paymentNativeProxy.transferWithReferenceAndFee{value: rD.requestAmount + rD.feeAmount}( payable(rD.recipient), rD.paymentReference, rD.feeAmount, @@ -193,9 +194,9 @@ contract BatchNoConversionPayments is Ownable { if (skipFeeUSDLimit == false) { (amount, batchFeeAmountUSD) = calculateBatchFeeToPay( amount, - pathsEthToUSD[0][0], + pathsNativeToUSD[0][0], batchFeeAmountUSD, - pathsEthToUSD + pathsNativeToUSD ); } // Check that batch contract has enough funds to pay batch fee @@ -203,8 +204,8 @@ contract BatchNoConversionPayments is Ownable { // Batch pays batch fee feeAddress.transfer(amount); - // Batch contract transfers the remaining ethers to the payer - if (transferBackRemainingEth && address(this).balance > 0) { + // Batch contract transfers the remaining Native tokens to the payer + if (transferBackRemainingNativeTokens && address(this).balance > 0) { (bool sendBackSuccess, ) = payable(msg.sender).call{value: address(this).balance}(''); require(sendBackSuccess, 'Could not send remaining funds to the payer'); } @@ -541,7 +542,7 @@ contract BatchNoConversionPayments is Ownable { */ /** - * @notice Fees added when using Erc20/Eth batch functions + * @notice Fees added when using Erc20/Native batch functions * @param _batchFee Between 0 and 200, i.e: batchFee = 30 represent 0.30% of fee */ function setBatchFee(uint16 _batchFee) external onlyOwner { @@ -558,10 +559,10 @@ contract BatchNoConversionPayments is Ownable { } /** - * @param _paymentEthProxy The address to the Ethereum fee payment proxy to use. + * @param _paymentNativeProxy The address to the Native fee payment proxy to use. */ - function setPaymentEthProxy(address _paymentEthProxy) external onlyOwner { - paymentEthProxy = IEthereumFeeProxy(_paymentEthProxy); + function setPaymentNativeProxy(address _paymentNativeProxy) external onlyOwner { + paymentNativeProxy = IEthereumFeeProxy(_paymentNativeProxy); } /** @@ -574,18 +575,19 @@ contract BatchNoConversionPayments is Ownable { /** * This function define variables allowing to limit the fees: - * ETHAddress, USDAddress, and pathsEthToUSD. - * @param _ETHAddress The address representing the Ethereum currency. + * NativeAddress, USDAddress, and pathsNativeToUSD. + * @param _NativeAddress The address representing the Native currency. * @param _USDAddress The address representing the USD currency. */ - function setETHAndUSDAddress(address _ETHAddress, address _USDAddress) external onlyOwner { - ETHAddress = _ETHAddress; + function setNativeAndUSDAddress(address _NativeAddress, address _USDAddress) external onlyOwner { + NativeAddress = _NativeAddress; USDAddress = _USDAddress; - pathsEthToUSD = [[ETHAddress, USDAddress]]; + pathsNativeToUSD = [[NativeAddress, USDAddress]]; } /** - * @param _batchFeeAmountUSDLimit The limitation of the batch fee amount in USD. + * @param _batchFeeAmountUSDLimit The limitation of the batch fee amount in USD, e.g: + * batchFeeAmountUSDLimit = 150 * 1e8 represents $150 */ function setBatchFeeAmountUSDLimit(uint64 _batchFeeAmountUSDLimit) external onlyOwner { batchFeeAmountUSDLimit = _batchFeeAmountUSDLimit; diff --git a/packages/smart-contracts/src/contracts/BatchPayments.sol b/packages/smart-contracts/src/contracts/BatchPayments.sol index 0947637f5f..5a00956aca 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -64,7 +64,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { * @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 batchNativePaymentsWithReference( address[] calldata _recipients, uint256[] calldata _amounts, bytes[] calldata _paymentReferences, 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 ef868c09e2..efe79ec848 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 @@ -9,7 +9,7 @@ }, { "internalType": "address", - "name": "_paymentEthProxy", + "name": "_paymentNativeProxy", "type": "address" }, { @@ -19,7 +19,7 @@ }, { "internalType": "address", - "name": "_paymentEthConversionFeeProxy", + "name": "_paymentNativeConversionFeeProxy", "type": "address" }, { @@ -57,7 +57,7 @@ }, { "inputs": [], - "name": "ETHAddress", + "name": "NativeAddress", "outputs": [ { "internalType": "address", @@ -202,7 +202,7 @@ "type": "address" } ], - "name": "batchEthConversionPayments", + "name": "batchNativeConversionPayments", "outputs": [ { "internalType": "uint256", @@ -268,7 +268,7 @@ "type": "address" } ], - "name": "batchEthPayments", + "name": "batchNativePayments", "outputs": [ { "internalType": "uint256", @@ -548,7 +548,7 @@ "type": "uint256" } ], - "name": "pathsEthToUSD", + "name": "pathsNativeToUSD", "outputs": [ { "internalType": "address", @@ -587,7 +587,7 @@ }, { "inputs": [], - "name": "paymentEthConversionProxy", + "name": "paymentNativeConversionProxy", "outputs": [ { "internalType": "contract IEthConversionProxy", @@ -600,7 +600,7 @@ }, { "inputs": [], - "name": "paymentEthProxy", + "name": "paymentNativeProxy", "outputs": [ { "internalType": "contract IEthereumFeeProxy", @@ -661,7 +661,7 @@ "inputs": [ { "internalType": "address", - "name": "_ETHAddress", + "name": "_NativeAddress", "type": "address" }, { @@ -670,7 +670,7 @@ "type": "address" } ], - "name": "setETHAndUSDAddress", + "name": "setNativeAndUSDAddress", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -705,11 +705,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthConversionProxy", + "name": "_paymentNativeConversionProxy", "type": "address" } ], - "name": "setPaymentEthConversionProxy", + "name": "setPaymentNativeConversionProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -718,11 +718,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthProxy", + "name": "_paymentNativeProxy", "type": "address" } ], - "name": "setPaymentEthProxy", + "name": "setPaymentNativeProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json index 7803175769..e936498539 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json @@ -9,7 +9,7 @@ }, { "internalType": "address", - "name": "_paymentEthProxy", + "name": "_paymentNativeProxy", "type": "address" }, { @@ -106,7 +106,7 @@ "type": "address" } ], - "name": "batchEthPayments", + "name": "batchNativePayments", "outputs": [], "stateMutability": "payable", "type": "function" @@ -190,7 +190,7 @@ }, { "inputs": [], - "name": "paymentEthProxy", + "name": "paymentNativeProxy", "outputs": [ { "internalType": "contract IEthereumFeeProxy", @@ -238,11 +238,11 @@ "inputs": [ { "internalType": "address", - "name": "_paymentEthProxy", + "name": "_paymentNativeProxy", "type": "address" } ], - "name": "setPaymentEthProxy", + "name": "setPaymentNativeProxy", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json index 2f268df1f4..ab7316a8bc 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json @@ -157,7 +157,7 @@ "type": "address" } ], - "name": "batchEthPaymentsWithReference", + "name": "batchNativePaymentsWithReference", "outputs": [], "stateMutability": "payable", "type": "function" diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index 6778c8c20d..dfb6520659 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -155,7 +155,7 @@ describe('contract: BatchConversionPayments', async () => { // set batch proxy fees and connect fromSigner await batchConversionProxy.setBatchFee(BATCH_FEE); - await batchConversionProxy.setETHAndUSDAddress(ETH_hash, USD_hash); + await batchConversionProxy.setNativeAndUSDAddress(ETH_hash, USD_hash); await batchConversionProxy.setBatchFeeAmountUSDLimit(BigNumber.from(10).mul(1e8)); // 10$ @@ -817,13 +817,13 @@ describe('contract: BatchConversionPayments', async () => { }); }); - describe(`batchEthConversionPayments`, () => { + describe(`batchNativeConversionPayments`, () => { it('make 1 payment with 1-step conversion', async function () { // get Eth balances const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchEthConversionPayments( + tx = await batchConversionProxy.batchNativeConversionPayments( [ethConvRequest], true, feeAddress, @@ -849,7 +849,7 @@ describe('contract: BatchConversionPayments', async () => { const EurEthConvRequest = Utils.deepCopy(ethConvRequest); EurEthConvRequest.path = [EUR_hash, USD_hash, ETH_hash]; - tx = await batchConversionProxy.batchEthConversionPayments( + tx = await batchConversionProxy.batchNativeConversionPayments( [ethConvRequest, EurEthConvRequest, ethConvRequest], true, feeAddress, @@ -873,19 +873,19 @@ describe('contract: BatchConversionPayments', async () => { }); }); - describe('batchEthConversionPayments errors', () => { + describe('batchNativeConversionPayments errors', () => { it('cannot transfer with invalid path', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.path = [USD_hash, EUR_hash, ETH_hash]; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, feeAddress, { + batchConversionProxy.batchNativeConversionPayments([wrongConvRequest], false, feeAddress, { value: (1000 + 1 + 11) * USD_ETH_RATE, // + 11 to pay batch fees }), ).to.be.revertedWith('No aggregator found'); }); it('not enough funds even if partially enough funds', async () => { await expect( - batchConversionProxy.batchEthConversionPayments( + batchConversionProxy.batchNativeConversionPayments( [ethConvRequest, ethConvRequest], false, feeAddress, @@ -899,7 +899,7 @@ describe('contract: BatchConversionPayments', async () => { const wrongConvRequest = Utils.deepCopy(ethConvRequest); wrongConvRequest.maxRateTimespan = '1'; await expect( - batchConversionProxy.batchEthConversionPayments([wrongConvRequest], false, feeAddress, { + batchConversionProxy.batchNativeConversionPayments([wrongConvRequest], false, feeAddress, { value: 1000 + 1 + 11, // + 11 to pay batch fees }), ).to.be.revertedWith('aggregator rate is outdated'); @@ -975,7 +975,7 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchEthPayments( + tx = await batchConversionProxy.batchNativePayments( [ { recipient: to, diff --git a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts index 1679e3c99b..d08067258c 100644 --- a/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchNoConversionEthPayments.test.ts @@ -75,7 +75,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { batchAddress = batch.address; await batch.connect(owner).setBatchFee(100); await batch.setBatchFeeAmountUSDLimit(BigNumber.from(1e8).div(1000)); // 1$ - await batch.setETHAndUSDAddress( + await batch.setNativeAndUSDAddress( currencyManager.fromSymbol('ETH')!.hash, currencyManager.fromSymbol('USD')!.hash, ); @@ -99,7 +99,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([copyEthRequestDetail1, copyEthRequestDetail2], true, feeAddress, { + .batchNativePayments([copyEthRequestDetail1, copyEthRequestDetail2], true, feeAddress, { value: BigNumber.from('6000'), }), ) @@ -128,7 +128,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { + .batchNativePayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }); await tx.wait(); @@ -156,7 +156,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments(Array(nbTxs).fill(copyEthRequestDetail), true, feeAddress, { + .batchNativePayments(Array(nbTxs).fill(copyEthRequestDetail), true, feeAddress, { value: totalAmount, }); @@ -182,7 +182,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { + .batchNativePayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds'); @@ -205,7 +205,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { await expect( batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { + .batchNativePayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: totalAmount, }), ).revertedWith('Not enough funds for batch fee'); @@ -235,7 +235,7 @@ describe('contract: batchNoConversionPayments: Ethereum', () => { const tx = await batch .connect(owner) - .batchEthPayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { + .batchNativePayments([ethRequestDetail1, ethRequestDetail2], true, feeAddress, { value: BigNumber.from('1000'), }); await tx.wait(); From aaf052be66ff9721d217f4ba2ea81d8b4f8d2e59 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 15:35:35 +0200 Subject: [PATCH 109/138] abi code --- .../BatchConversionPayments/0.1.0.json | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) 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 efe79ec848..f9591516be 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 @@ -147,6 +147,32 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "batchFee", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "batchFeeAmountUSDLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -192,17 +218,17 @@ "type": "tuple[]" }, { - "internalType": "bool", - "name": "skipFeeUSDLimit", - "type": "bool" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { - "internalType": "address payable", + "internalType": "address", "name": "feeAddress", "type": "address" } ], - "name": "batchNativeConversionPayments", + "name": "batchMultiERC20ConversionPayments", "outputs": [ { "internalType": "uint256", @@ -210,7 +236,7 @@ "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "nonpayable", "type": "function" }, { @@ -258,17 +284,17 @@ "type": "tuple[]" }, { - "internalType": "bool", - "name": "skipFeeUSDLimit", - "type": "bool" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { - "internalType": "address payable", + "internalType": "address", "name": "feeAddress", "type": "address" } ], - "name": "batchNativePayments", + "name": "batchMultiERC20Payments", "outputs": [ { "internalType": "uint256", @@ -276,33 +302,7 @@ "type": "uint256" } ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [], - "name": "batchFee", - "outputs": [ - { - "internalType": "uint16", - "name": "", - "type": "uint16" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "batchFeeAmountUSDLimit", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -350,17 +350,17 @@ "type": "tuple[]" }, { - "internalType": "address[][]", - "name": "pathsToUSD", - "type": "address[][]" + "internalType": "bool", + "name": "skipFeeUSDLimit", + "type": "bool" }, { - "internalType": "address", + "internalType": "address payable", "name": "feeAddress", "type": "address" } ], - "name": "batchMultiERC20ConversionPayments", + "name": "batchNativeConversionPayments", "outputs": [ { "internalType": "uint256", @@ -368,7 +368,7 @@ "type": "uint256" } ], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { @@ -416,17 +416,17 @@ "type": "tuple[]" }, { - "internalType": "address[][]", - "name": "pathsToUSD", - "type": "address[][]" + "internalType": "bool", + "name": "skipFeeUSDLimit", + "type": "bool" }, { - "internalType": "address", + "internalType": "address payable", "name": "feeAddress", "type": "address" } ], - "name": "batchMultiERC20Payments", + "name": "batchNativePayments", "outputs": [ { "internalType": "uint256", @@ -434,7 +434,7 @@ "type": "uint256" } ], - "stateMutability": "nonpayable", + "stateMutability": "payable", "type": "function" }, { From 6b853c26c666677df54eb171d4175b73292ae011 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:17:24 +0200 Subject: [PATCH 110/138] pretty batch contract --- .../src/contracts/BatchNoConversionPayments.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 2dd11e68af..af7d393e70 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -96,7 +96,8 @@ contract BatchNoConversionPayments is Ownable { } /** - * This contract is non-payable. Making a Native payment with conversion requires the contract to accept incoming Native tokens. + * This contract is non-payable. + * Making a Native payment with conversion requires the contract to accept incoming Native tokens. * @dev See the end of `paymentNativeConversionProxy.transferWithReferenceAndFee` where the leftover is given back. */ receive() external payable { From f428ea46d1d27394a3ed6fe7646ee81f9549626b Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 18:09:56 +0200 Subject: [PATCH 111/138] update batch conversion processor --- .../src/payment/batch-conversion-proxy.ts | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index d6edd6f0a9..dc7c9b9c02 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -5,11 +5,13 @@ import { ClientTypes, PaymentTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, + getAmountToPay, getPnAndNetwork, getProvider, getProxyAddress, getRequestPaymentValues, getSigner, + validateErc20FeeProxyRequest, } from './utils'; import { padAmountForChainlink, @@ -18,7 +20,6 @@ import { 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'; import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; import { IState } from 'types/dist/extension-types'; @@ -33,7 +34,7 @@ import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwo * @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: + * @dev We only implement batchPayments using two ERC20 functions: * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. */ export async function payBatchConversionProxyRequest( @@ -79,10 +80,10 @@ 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[] = []; + const requestDetailsERC20NoConversion: PaymentTypes.RequestDetail[] = []; + const requestDetailsERC20Conversion: PaymentTypes.RequestDetail[] = []; - // fill conversionDetails and requestsWithoutConversion lists + // fill requestDetailsERC20Conversion and requestDetailsERC20NoConversion lists for (const enrichedRequest of enrichedRequests) { if ( enrichedRequest.paymentNetworkId === @@ -100,7 +101,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques ) { throw new Error(`wrong request currencyInfo type`); } - conversionDetails.push(getInputConversionDetail(enrichedRequest)); + requestDetailsERC20Conversion.push(getInputConversionDetail(enrichedRequest)); } else if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { @@ -109,59 +110,63 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques // isERC20Currency is checked within getBatchArgs function comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request); - requestsWithoutConversion.push(enrichedRequest.request); + requestDetailsERC20NoConversion.push( + getInputRequestDetailERC20NoConversion(enrichedRequest.request), + ); } if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1]) throw new Error('All the requests must have the same network'); } const metaDetails: PaymentTypes.MetaDetail[] = []; - // Add conversionDetails to metaDetails - if (conversionDetails.length > 0) { + // Add requestDetailsERC20Conversion to metaDetails + if (requestDetailsERC20Conversion.length > 0) { metaDetails.push({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - conversionDetails: conversionDetails, - cryptoDetails: { - tokenAddresses: [], - recipients: [], - amounts: [], - paymentReferences: [], - feeAmounts: [], - }, // cryptoDetails is not used with paymentNetworkId 0 + requestDetails: requestDetailsERC20Conversion, }); } - // Get values and add cryptoDetails to metaDetails - if (requestsWithoutConversion.length > 0) { - const { tokenAddresses, paymentAddresses, amountsToPay, paymentReferences, feesToPay } = - getBatchArgs(requestsWithoutConversion, 'ERC20'); - + // Add cryptoDetails to metaDetails + if (requestDetailsERC20NoConversion.length > 0) { // add ERC20 no-conversion payments metaDetails.push({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - conversionDetails: [], - cryptoDetails: { - tokenAddresses: tokenAddresses, - recipients: paymentAddresses, - amounts: amountsToPay.map((x) => x.toString()), - paymentReferences: paymentReferences, - feeAmounts: feesToPay.map((x) => x.toString()), - }, + requestDetails: requestDetailsERC20NoConversion, }); } const proxyContract = BatchConversionPayments__factory.createInterface(); - return proxyContract.encodeFunctionData('batchRouter', [ + return proxyContract.encodeFunctionData('batchPayments', [ metaDetails, feeAddress || constants.AddressZero, ]); } +function getInputRequestDetailERC20NoConversion( + request: ClientTypes.IRequestData, +): PaymentTypes.RequestDetail { + validateErc20FeeProxyRequest(request); + + const tokenAddress = request.currencyInfo.value; + const { paymentReference, paymentAddress, feeAmount } = getRequestPaymentValues(request); + + return { + recipient: paymentAddress, + requestAmount: getAmountToPay(request).toString(), + path: [tokenAddress], + paymentReference: `0x${paymentReference}`, + feeAmount: feeAmount || '0', + maxToSpend: '', + maxRateTimespan: '', + }; +} + /** * Get the conversion detail values from one enriched request * @param enrichedRequest The enrichedRequest to pay */ -function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.ConversionDetail { +function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.RequestDetail { const paymentSettings = enrichedRequest.paymentSettings; if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); From b3b5f728da43cde1df6be04008445bdc8b3ca324 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 18:43:06 +0200 Subject: [PATCH 112/138] update contract comment and receiver function --- .../src/contracts/BatchConversionPayments.sol | 32 +++++++++++------ .../contracts/BatchNoConversionPayments.sol | 36 ++++++++----------- .../src/contracts/BatchPayments.sol | 2 +- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 3a47a0def2..037d53a904 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -24,6 +24,9 @@ contract BatchConversionPayments is BatchNoConversionPayments { IERC20ConversionProxy public paymentErc20ConversionProxy; IEthConversionProxy public paymentNativeConversionProxy; + /** payerAuthorized is set to true to workaround the non-payable aspect in batch native conversion */ + bool private payerAuthorized = false; + /** * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network: * - paymentNetworkId: from 0 to 4, cf. `batchPayment()` method @@ -61,6 +64,15 @@ contract BatchConversionPayments is BatchNoConversionPayments { paymentNativeConversionProxy = IEthConversionProxy(_paymentNativeConversionFeeProxy); } + /** + * This contract is non-payable. + * Making a Native payment with conversion requires the contract to accept incoming Native tokens. + * @dev See the end of `paymentNativeConversionProxy.transferWithReferenceAndFee` where the leftover is given back. + */ + receive() external payable override { + require(payerAuthorized || msg.value == 0, 'Non-payable'); + } + /** * @notice Batch payments on different payment networks at once. * @param metaDetails contains paymentNetworkId and requestDetails @@ -194,7 +206,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { for (uint256 k = 0; k < uTokens.length && uTokens[k].amountAndFee > 0; k++) { uTokens[k].batchFeeAmount = (uTokens[k].amountAndFee * batchFee) / feeDenominator; requestedToken = IERC20(uTokens[k].tokenAddress); - contractAllowanceApprovalTransfer( + transferToContract( requestedToken, uTokens[k].amountAndFee, uTokens[k].batchFeeAmount, @@ -204,16 +216,16 @@ contract BatchConversionPayments is BatchNoConversionPayments { // Batch pays the requests using Erc20ConversionFeeProxy for (uint256 i = 0; i < requestDetails.length; i++) { - RequestDetail memory rI = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; paymentErc20ConversionProxy.transferFromWithReferenceAndFee( - rI.recipient, - rI.requestAmount, - rI.path, - rI.paymentReference, - rI.feeAmount, + rD.recipient, + rD.requestAmount, + rD.path, + rD.paymentReference, + rD.feeAmount, feeAddress, - rI.maxToSpend, - rI.maxRateTimespan + rD.maxToSpend, + rD.maxRateTimespan ); } @@ -271,7 +283,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { // Batch contract pays the requests through nativeConversionProxy for (uint256 i = 0; i < requestDetails.length; i++) { - RequestDetail memory rD = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; paymentNativeConversionProxy.transferWithReferenceAndFee{value: address(this).balance}( payable(rD.recipient), rD.requestAmount, diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index af7d393e70..db843d0315 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -36,8 +36,6 @@ contract BatchNoConversionPayments is Ownable { batchFeeAmountUSDLimit = 150 * 1e8 represents $150 */ uint64 public batchFeeAmountUSDLimit; - /** payerAuthorized is set to true only when needed for batch native conversion */ - bool internal payerAuthorized = false; /** transferBackRemainingNativeTokens is set to false only if the payer use batchPayment and call both batchNativePayments and batchNativeConversionPayments */ bool internal transferBackRemainingNativeTokens = true; @@ -97,11 +95,10 @@ contract BatchNoConversionPayments is Ownable { /** * This contract is non-payable. - * Making a Native payment with conversion requires the contract to accept incoming Native tokens. - * @dev See the end of `paymentNativeConversionProxy.transferWithReferenceAndFee` where the leftover is given back. + * @dev See the end of `paymentNativeProxy.transferWithReferenceAndFee` where the leftover is given back. */ - receive() external payable { - require(payerAuthorized || msg.value == 0, 'Non-payable'); + receive() external payable virtual { + require(msg.value == 0, 'Non-payable'); } /** @@ -178,7 +175,7 @@ contract BatchNoConversionPayments is Ownable { // Batch contract pays the requests thourgh NativeFeeProxy (EthFeeProxy) for (uint256 i = 0; i < requestDetails.length; i++) { - RequestDetail memory rD = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; require(address(this).balance >= rD.requestAmount + rD.feeAmount, 'Not enough funds'); amount += rD.requestAmount; @@ -248,12 +245,7 @@ contract BatchNoConversionPayments is Ownable { IERC20 requestedToken = IERC20(requestDetails[0].path[0]); - contractAllowanceApprovalTransfer( - requestedToken, - amountAndFee, - batchFeeAmount, - address(paymentErc20Proxy) - ); + transferToContract(requestedToken, amountAndFee, batchFeeAmount, address(paymentErc20Proxy)); // Payer pays batch fee amount require( @@ -263,7 +255,7 @@ contract BatchNoConversionPayments is Ownable { // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < requestDetails.length; i++) { - RequestDetail memory rD = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; paymentErc20Proxy.transferFromWithReferenceAndFee( rD.path[0], rD.recipient, @@ -300,7 +292,7 @@ contract BatchNoConversionPayments is Ownable { for (uint256 i = 0; i < uTokens.length && uTokens[i].amountAndFee > 0; i++) { uTokens[i].batchFeeAmount = (uTokens[i].batchFeeAmount * batchFee) / feeDenominator; IERC20 requestedToken = IERC20(uTokens[i].tokenAddress); - contractAllowanceApprovalTransfer( + transferToContract( requestedToken, uTokens[i].amountAndFee, uTokens[i].batchFeeAmount, @@ -326,7 +318,7 @@ contract BatchNoConversionPayments is Ownable { // Batch contract pays the requests using Erc20FeeProxy for (uint256 i = 0; i < requestDetails.length; i++) { - RequestDetail memory rD = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; paymentErc20Proxy.transferFromWithReferenceAndFee( rD.path[0], rD.recipient, @@ -344,17 +336,19 @@ contract BatchNoConversionPayments is Ownable { */ /** - * It: + * Top up the contract with enough `requestedToken` to pay `amountAndFee`. + * + * It also performs a few checks: * - checks that the batch contract has enough allowance from the payer - * - checks that the payer has enough fund, including batch fees - * - does the transfer of token from the payer to the batch contract + * - checks that the payer has enough funds, including batch fees * - increases the allowance of the contract to use the payment proxy if needed + * * @param requestedToken The token to pay * @param amountAndFee The amount and the fee for a token to pay * @param batchFeeAmount The batch fee amount for a token to pay * @param paymentProxyAddress The payment proxy address used to pay */ - function contractAllowanceApprovalTransfer( + function transferToContract( IERC20 requestedToken, uint256 amountAndFee, uint256 batchFeeAmount, @@ -400,7 +394,7 @@ contract BatchNoConversionPayments is Ownable { uTokens = new Token[](requestDetails.length); for (uint256 i = 0; i < requestDetails.length; i++) { for (uint256 k = 0; k < requestDetails.length; k++) { - RequestDetail memory rD = requestDetails[i]; + RequestDetail calldata rD = requestDetails[i]; // If the token is already in the existing uTokens list if (uTokens[k].tokenAddress == rD.path[rD.path.length - 1]) { if (rD.path.length > 1) { diff --git a/packages/smart-contracts/src/contracts/BatchPayments.sol b/packages/smart-contracts/src/contracts/BatchPayments.sol index 5a00956aca..0947637f5f 100644 --- a/packages/smart-contracts/src/contracts/BatchPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchPayments.sol @@ -64,7 +64,7 @@ contract BatchPayments is Ownable, ReentrancyGuard { * @dev It uses EthereumFeeProxy to pay an invoice and fees, with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount */ - function batchNativePaymentsWithReference( + function batchEthPaymentsWithReference( address[] calldata _recipients, uint256[] calldata _amounts, bytes[] calldata _paymentReferences, From ac9b2a37400666e4644868b9c529fce17a7f89c7 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 13 Oct 2022 18:45:21 +0200 Subject: [PATCH 113/138] rename batchPayment into batchPayments --- .../src/contracts/BatchConversionPayments.sol | 12 +++---- .../contracts/BatchNoConversionPayments.sol | 2 +- .../BatchConversionPayments/0.1.0.json | 2 +- .../contracts/BatchConversionPayments.test.ts | 32 +++++++++---------- packages/types/src/payment-types.ts | 2 +- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 037d53a904..32b8b16cc5 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -13,9 +13,9 @@ import './BatchNoConversionPayments.sol'; * - 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. - * batchPayment is the main function to batch all kinds of payments at once. + * batchPayments is the main function to batch all kinds of payments at once. * If one transaction of the batch fails, all transactions are reverted. - * @dev batchPayment is the main function, but other batch payment functions are "public" in order to do + * @dev batchPayments is the main function, but other batch payment functions are "public" in order to do * gas optimization in some cases. */ contract BatchConversionPayments is BatchNoConversionPayments { @@ -28,8 +28,8 @@ contract BatchConversionPayments is BatchNoConversionPayments { bool private payerAuthorized = false; /** - * @dev Used by the batchPayment to handle information for heterogeneous batches, grouped by payment network: - * - paymentNetworkId: from 0 to 4, cf. `batchPayment()` method + * @dev Used by the batchPayments to handle information for heterogeneous batches, grouped by payment network: + * - paymentNetworkId: from 0 to 4, cf. `batchPayments()` method * - requestDetails all the data required for conversion and no conversion requests to be paid */ struct MetaDetail { @@ -88,10 +88,10 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @param feeAddress The address where fees should be paid. * @dev Use pathsToUSD only if you are pretty sure the batch fees will higher than the * USD limit batchFeeAmountUSDLimit, because it increase gas consumption. - * batchPayment only reduces gas consumption when using more than a single payment network. + * batchPayments 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 batchPayment( + function batchPayments( MetaDetail[] calldata metaDetails, address[][] calldata pathsToUSD, address feeAddress diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index db843d0315..42cca6bc06 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -36,7 +36,7 @@ contract BatchNoConversionPayments is Ownable { batchFeeAmountUSDLimit = 150 * 1e8 represents $150 */ uint64 public batchFeeAmountUSDLimit; - /** transferBackRemainingNativeTokens is set to false only if the payer use batchPayment + /** transferBackRemainingNativeTokens is set to false only if the payer use batchPayments and call both batchNativePayments and batchNativeConversionPayments */ bool internal transferBackRemainingNativeTokens = true; 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 f9591516be..c04ceade09 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 @@ -504,7 +504,7 @@ "type": "address" } ], - "name": "batchPayment", + "name": "batchPayments", "outputs": [], "stateMutability": "payable", "type": "function" diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index dfb6520659..babd8ed66b 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -359,7 +359,7 @@ describe('contract: BatchConversionPayments', async () => { ); }; - describe('batchPayment', async () => { + describe('batchPayments', async () => { const testBatchPayment = async (applyLimit: boolean) => { // Limit is applied if there are paths to USD const pathsToUSD = applyLimit @@ -376,7 +376,7 @@ describe('contract: BatchConversionPayments', async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchPayment( + await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ERC20_PAYMENTS, @@ -412,7 +412,7 @@ describe('contract: BatchConversionPayments', async () => { it(`make 1 ERC20 payment with no conversion`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); - await batchConversionProxy.batchPayment( + await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, @@ -446,8 +446,8 @@ describe('contract: BatchConversionPayments', async () => { ); }); it('make 3 ERC20 payments with different tokens and conversion lengths', async () => { - const batchPayment = async () => { - return await batchConversionProxy.batchPayment( + const batchPayments = async () => { + return await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -468,12 +468,12 @@ describe('contract: BatchConversionPayments', async () => { const expectedFeeDAIBalanceDiff = BigNumber.from('237623762376237623762'); await manyPaymentsBatchConv( - batchPayment, + batchPayments, expectedFeeFAUBalanceDiff, expectedFeeDAIBalanceDiff, ); } else { - await manyPaymentsBatchConv(batchPayment); + await manyPaymentsBatchConv(batchPayments); } }); it('make 1 ETH payment without conversion', async () => { @@ -482,7 +482,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchPayment( + tx = await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_PAYMENTS, @@ -513,7 +513,7 @@ describe('contract: BatchConversionPayments', async () => { const initialToETHBalance = await provider.getBalance(to); const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchPayment( + tx = await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_ETH_CONVERSION_PAYMENTS, @@ -547,7 +547,7 @@ describe('contract: BatchConversionPayments', async () => { const initialFeeETHBalance = await provider.getBalance(feeAddress); const initialFromETHBalance = await provider.getBalance(await fromSigner.getAddress()); - tx = await batchConversionProxy.batchPayment( + tx = await batchConversionProxy.batchPayments( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, @@ -666,10 +666,10 @@ describe('contract: BatchConversionPayments', async () => { await testBatchPayment(false); }); - describe('batchPayment errors', async () => { - it(`too many elements within batchPayment metaDetails input`, async () => { + describe('batchPayments errors', async () => { + it(`too many elements within batchPayments metaDetails input`, async () => { await expect( - batchConversionProxy.batchPayment( + batchConversionProxy.batchPayments( Array(6).fill({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, requestDetails: [], @@ -681,7 +681,7 @@ describe('contract: BatchConversionPayments', async () => { }); it(`wrong paymentNetworkId set in metaDetails input`, async () => { await expect( - batchConversionProxy.batchPayment( + batchConversionProxy.batchPayments( [ { paymentNetworkId: 6, @@ -739,7 +739,7 @@ describe('contract: BatchConversionPayments', async () => { ); }); it('make 3 payments with different tokens and conversion length', async () => { - const batchPayment = async () => { + const batchPayments = async () => { return await batchConversionProxy .connect(fromSigner) .batchMultiERC20ConversionPayments( @@ -748,7 +748,7 @@ describe('contract: BatchConversionPayments', async () => { feeAddress, ); }; - await manyPaymentsBatchConv(batchPayment); + await manyPaymentsBatchConv(batchPayments); }); }); diff --git a/packages/types/src/payment-types.ts b/packages/types/src/payment-types.ts index 985c76aa3a..8a3454fe37 100644 --- a/packages/types/src/payment-types.ts +++ b/packages/types/src/payment-types.ts @@ -346,7 +346,7 @@ export enum BATCH_PAYMENT_NETWORK_ID { } /** Input type used by batch conversion proxy to make an ERC20 & ETH, - * and conversion & no-conversion payment through batchPayment */ + * and conversion & no-conversion payment through batchPayments */ export interface MetaDetail { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID; requestDetails: RequestDetail[]; From 93b8bb66546023d4713e07cd4dab58523f12060a Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 14 Oct 2022 10:03:05 +0200 Subject: [PATCH 114/138] clean batch abi files --- .../BatchNoConversionPayments/0.1.0.json | 356 ++++++++++++++---- .../lib/artifacts/BatchPayments/0.1.0.json | 2 +- 2 files changed, 286 insertions(+), 72 deletions(-) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json index e936498539..06d163c967 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json @@ -12,6 +12,11 @@ "name": "_paymentNativeProxy", "type": "address" }, + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + }, { "internalType": "address", "name": "_owner", @@ -41,130 +46,295 @@ "type": "event" }, { - "inputs": [ + "inputs": [], + "name": "NativeAddress", + "outputs": [ { "internalType": "address", - "name": "_tokenAddress", + "name": "", "type": "address" - }, - { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" - }, + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "USDAddress", + "outputs": [ { - "internalType": "uint256[]", - "name": "_amounts", - "type": "uint256[]" - }, + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" + "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 BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", + "type": "tuple[]" }, { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { "internalType": "address", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], "name": "batchERC20Payments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "batchFee", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "batchFeeAmountUSDLimit", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { - "internalType": "address[]", - "name": "_recipients", - "type": "address[]" + "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 BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", + "type": "tuple[]" }, { - "internalType": "uint256[]", - "name": "_amounts", - "type": "uint256[]" + "internalType": "address[][]", + "name": "pathsToUSD", + "type": "address[][]" }, { - "internalType": "bytes[]", - "name": "_paymentReferences", - "type": "bytes[]" + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "batchMultiERC20Payments", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "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 BatchNoConversionPayments.RequestDetail[]", + "name": "requestDetails", + "type": "tuple[]" }, { - "internalType": "uint256[]", - "name": "_feeAmounts", - "type": "uint256[]" + "internalType": "bool", + "name": "skipFeeUSDLimit", + "type": "bool" }, { "internalType": "address payable", - "name": "_feeAddress", + "name": "feeAddress", "type": "address" } ], "name": "batchNativePayments", - "outputs": [], + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], "stateMutability": "payable", "type": "function" }, { "inputs": [], - "name": "batchFee", + "name": "chainlinkConversionPath", "outputs": [ { - "internalType": "uint256", + "internalType": "contract ChainlinkConversionPath", "name": "", - "type": "uint256" + "type": "address" } ], "stateMutability": "view", "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[]" - }, + "inputs": [], + "name": "owner", + "outputs": [ { "internalType": "address", - "name": "_feeAddress", + "name": "", "type": "address" } ], - "name": "batchMultiERC20Payments", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "owner", + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "pathsNativeToUSD", "outputs": [ { "internalType": "address", @@ -211,9 +381,9 @@ { "inputs": [ { - "internalType": "uint256", + "internalType": "uint16", "name": "_batchFee", - "type": "uint256" + "type": "uint16" } ], "name": "setBatchFee", @@ -221,6 +391,50 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_batchFeeAmountUSDLimit", + "type": "uint64" + } + ], + "name": "setBatchFeeAmountUSDLimit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_chainlinkConversionPathAddress", + "type": "address" + } + ], + "name": "setConversionPathAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_NativeAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "_USDAddress", + "type": "address" + } + ], + "name": "setNativeAndUSDAddress", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json index ab7316a8bc..2f268df1f4 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchPayments/0.1.0.json @@ -157,7 +157,7 @@ "type": "address" } ], - "name": "batchNativePaymentsWithReference", + "name": "batchEthPaymentsWithReference", "outputs": [], "stateMutability": "payable", "type": "function" From 42913a71c8ad480b511d458a48ab8c1c4c611ed0 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 14 Oct 2022 12:01:04 +0200 Subject: [PATCH 115/138] update payment processor batchPayments and fix one test --- .../payment-processor/src/payment/batch-conversion-proxy.ts | 1 + .../test/payment/any-to-erc20-batch-proxy.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index dc7c9b9c02..851efcc8b9 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -139,6 +139,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques const proxyContract = BatchConversionPayments__factory.createInterface(); return proxyContract.encodeFunctionData('batchPayments', [ metaDetails, + [], feeAddress || constants.AddressZero, ]); } 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 4b94551b61..f4a3d5a817 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 @@ -338,7 +338,7 @@ describe('erc20-batch-conversion-proxy', () => { { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ - data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'), value: 0, @@ -409,7 +409,7 @@ describe('erc20-batch-conversion-proxy', () => { // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) ).toEqual(expectedAmountToPay); }); - it('should convert and pay two requests in EUR with ERC20', async () => { + it.only('should convert and pay two requests in EUR with ERC20', async () => { // Get initial balances const initialETHFromBalance = await wallet.getBalance(); const initialDAIFromBalance = await getErc20Balance( @@ -417,7 +417,7 @@ describe('erc20-batch-conversion-proxy', () => { wallet.address, provider, ); - + alphaPaymentSettings.maxToSpend = alphaPaymentSettings.maxToSpend; // Convert and pay const tx = await payBatchConversionProxyRequest( Array(2).fill({ From 43baa374d9212835009293866a3123a7bf1b63c3 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Fri, 14 Oct 2022 18:26:46 +0200 Subject: [PATCH 116/138] add batch fee amount USD to batch payment processor and its tests --- .../src/payment/batch-conversion-proxy.ts | 104 +++++++++++++++--- .../payment/any-to-erc20-batch-proxy.test.ts | 46 +++++--- 2 files changed, 117 insertions(+), 33 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 851efcc8b9..f643e271f9 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -21,9 +21,16 @@ import { IPreparedTransaction } from './prepared-transaction'; import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; -import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; +import { BATCH_PAYMENT_NETWORK_ID, RequestDetail } from '@requestnetwork/types/dist/payment-types'; import { IState } from 'types/dist/extension-types'; -import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwork/currency'; +import { + CurrencyInput, + isERC20Currency, + isISO4217Currency, + CurrencyManager, +} from '@requestnetwork/currency'; + +const currencyManager = CurrencyManager.getDefault(); /** * Processes a transaction to pay a batch of requests with an ERC20 currency @@ -33,6 +40,9 @@ import { CurrencyInput, isERC20Currency, isISO4217Currency } from '@requestnetwo * @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 skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * It can be useful to set it to false if the total amount of the batch is important. + * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. * @param overrides Optionally, override default transaction values, like gas. * @dev We only implement batchPayments using two ERC20 functions: * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. @@ -41,25 +51,34 @@ export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], version: string, signerOrProvider: providers.Provider | Signer = getProvider(), + skipFeeUSDLimit = true, overrides?: ITransactionOverrides, ): Promise { - const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, version); + const { data, to, value } = prepareBatchConversionPaymentTransaction( + enrichedRequests, + version, + skipFeeUSDLimit, + ); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); } /** * 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. + * that can be different from the request currency (eg. fiat). + * It can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequests to pay * @param version The version of the batch conversion proxy + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * It can be useful to set it to false if the total amount of the batch is important. + * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. */ export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], version: string, + skipFeeUSDLimit = true, ): IPreparedTransaction { - const encodedTx = encodePayBatchConversionRequest(enrichedRequests); + const encodedTx = encodePayBatchConversionRequest(enrichedRequests, skipFeeUSDLimit); const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); return { data: encodedTx, @@ -70,11 +89,17 @@ 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) + * that can be different from the request currency (eg. fiat). * It can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequests to pay + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * It can be useful to set it to false if the total amount of the batch is important. + * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. */ -export function encodePayBatchConversionRequest(enrichedRequests: EnrichedRequest[]): string { +export function encodePayBatchConversionRequest( + enrichedRequests: EnrichedRequest[], + skipFeeUSDLimit = true, +): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1]; @@ -101,7 +126,7 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques ) { throw new Error(`wrong request currencyInfo type`); } - requestDetailsERC20Conversion.push(getInputConversionDetail(enrichedRequest)); + requestDetailsERC20Conversion.push(getInputRequestDetailERC20Conversion(enrichedRequest)); } else if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { @@ -136,14 +161,24 @@ export function encodePayBatchConversionRequest(enrichedRequests: EnrichedReques }); } + const pathsToUSD = getPathsToUSD( + [...requestDetailsERC20Conversion, ...requestDetailsERC20NoConversion], + firstNetwork, + skipFeeUSDLimit, + ); + const proxyContract = BatchConversionPayments__factory.createInterface(); return proxyContract.encodeFunctionData('batchPayments', [ metaDetails, - [], + skipFeeUSDLimit ? [] : pathsToUSD, feeAddress || constants.AddressZero, ]); } +/** + * Get the ERC20 no conversion input requestDetail from a request, that can be used by the batch contract. + * @param request The request to pay. + */ function getInputRequestDetailERC20NoConversion( request: ClientTypes.IRequestData, ): PaymentTypes.RequestDetail { @@ -157,17 +192,19 @@ function getInputRequestDetailERC20NoConversion( requestAmount: getAmountToPay(request).toString(), path: [tokenAddress], paymentReference: `0x${paymentReference}`, - feeAmount: feeAmount || '0', - maxToSpend: '', - maxRateTimespan: '', + feeAmount: feeAmount?.toString() || '0', + maxToSpend: '0', + maxRateTimespan: '0', }; } /** - * Get the conversion detail values from one enriched request - * @param enrichedRequest The enrichedRequest to pay + * Get the ERC20 conversion input requestDetail from an enriched request, that can be used by the batch contract. + * @param enrichedRequest The enrichedRequest to pay. */ -function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentTypes.RequestDetail { +function getInputRequestDetailERC20Conversion( + enrichedRequest: EnrichedRequest, +): PaymentTypes.RequestDetail { const paymentSettings = enrichedRequest.paymentSettings; if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); @@ -198,7 +235,40 @@ function getInputConversionDetail(enrichedRequest: EnrichedRequest): PaymentType } /** - * + * Get the list of conversion paths from tokens to the USD address through currencyManager. + * @param requestDetails List of ERC20 requests to pay. + * @param network The network targeted. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * It can be useful to set it to false if the total amount of the batch is important. + * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + */ +function getPathsToUSD( + requestDetails: RequestDetail[], + network: string, + skipFeeUSDLimit: boolean, +): string[][] { + const pathsToUSD: Array[] = []; + if (!skipFeeUSDLimit) { + const USDCurrency = currencyManager.fromSymbol('USD'); + // token's addresses paid with the batch + const tokenAddresses: Array = []; + for (const requestDetail of requestDetails) { + const tokenAddress = requestDetail.path[requestDetail.path.length - 1]; + // Check token to only unique paths token to USD. + if (!tokenAddresses.includes(tokenAddress)) { + tokenAddresses.push(tokenAddress); + const tokenCurrency = currencyManager.fromAddress(tokenAddress); + const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency!, network); + if (pathToUSD) { + pathsToUSD.push(pathToUSD); + } + } + } + } + return pathsToUSD; +} + +/** * @param network The network targeted * @param version The version of the batch conversion proxy * @returns 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 f4a3d5a817..feb15fe73c 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 @@ -221,6 +221,7 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, + true, ), ).rejects.toThrowError( new UnsupportedCurrencyError({ @@ -241,13 +242,14 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, + true, ), ).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), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError(`wrong request currencyInfo type`); }); it('should throw an error if the request has a wrong network', async () => { @@ -269,7 +271,7 @@ describe('erc20-batch-conversion-proxy', () => { }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).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 () => { @@ -290,7 +292,7 @@ describe('erc20-batch-conversion-proxy', () => { }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', ); @@ -298,13 +300,13 @@ describe('erc20-batch-conversion-proxy', () => { it("should throw an error if one request's currencyInfo has no value", async () => { EURRequest.currencyInfo.value = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).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), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError('no payment network found'); }); it('should throw an error if there is a wrong version mapping', async () => { @@ -315,7 +317,7 @@ describe('erc20-batch-conversion-proxy', () => { }, }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -335,6 +337,7 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, + true, { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ @@ -378,6 +381,7 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, + true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -409,7 +413,7 @@ describe('erc20-batch-conversion-proxy', () => { // (exact result) = 1.215516831683168316 (over 18 decimals for this ERC20) ).toEqual(expectedAmountToPay); }); - it.only('should convert and pay two requests in EUR with ERC20', async () => { + it('should convert and pay two requests in EUR with ERC20', async () => { // Get initial balances const initialETHFromBalance = await wallet.getBalance(); const initialDAIFromBalance = await getErc20Balance( @@ -427,6 +431,7 @@ describe('erc20-batch-conversion-proxy', () => { }), batchConvVersion, wallet, + true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -477,10 +482,12 @@ describe('erc20-batch-conversion-proxy', () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: DAIValidRequest, + paymentSettings: undefined, }, ], batchConvVersion, wallet, + true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -534,7 +541,7 @@ describe('erc20-batch-conversion-proxy', () => { it('should throw an error if the request is not erc20', async () => { FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', ); @@ -543,7 +550,7 @@ describe('erc20-batch-conversion-proxy', () => { it("should throw an error if one request's currencyInfo has no value", async () => { FAURequest.currencyInfo.value = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', ); @@ -552,14 +559,16 @@ describe('erc20-batch-conversion-proxy', () => { it("should throw an error if one request's currencyInfo has no network", async () => { FAURequest.currencyInfo.network = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('Payment currency must have a network'); + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + ).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 () => { FAURequest.extensions = [] as any; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError('no payment network found'); }); @@ -571,7 +580,7 @@ describe('erc20-batch-conversion-proxy', () => { }, }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -581,11 +590,11 @@ describe('erc20-batch-conversion-proxy', () => { const spy = jest.fn(); const originalSendTransaction = wallet.sendTransaction.bind(wallet); wallet.sendTransaction = spy; - await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, { + await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true, { gasPrice: '20000000000', }); expect(spy).toHaveBeenCalledWith({ - data: '0xf0fa379f0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa350000000000000000000000009fbda871d559710256a2502a2517b794b482db400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b732000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000186a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064', + data: '0x92cddb9100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a000000000000000000000000000000000000000000000000000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fbda871d559710256a2502a2517b794b482db40000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(DAIValidRequest, '0.1.0'), value: 0, @@ -627,7 +636,12 @@ describe('erc20-batch-conversion-proxy', () => { const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); // Batch payment - const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); + const tx = await payBatchConversionProxyRequest( + enrichedRequests, + batchConvVersion, + wallet, + true, + ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); From fc6887fc0296f5cb079b43ad81c5c6d7751162dd Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:03:25 +0200 Subject: [PATCH 117/138] batch conversion payment processor functions and tests --- .../src/payment/batch-conversion-proxy.ts | 56 ++++--- .../payment-processor/src/payment/index.ts | 1 - .../payment/any-to-erc20-batch-proxy.test.ts | 158 ++++++++++-------- 3 files changed, 116 insertions(+), 99 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index f643e271f9..ef0aca1dc3 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -34,7 +34,7 @@ const currencyManager = CurrencyManager.getDefault(); /** * Processes a transaction to pay a batch of requests with an ERC20 currency - * that is different from the request currency (eg. fiat) + * that can be 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 EnrichedRequests to pay @@ -102,13 +102,14 @@ export function encodePayBatchConversionRequest( ): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); - const firstNetwork = getPnAndNetwork(enrichedRequests[0].request)[1]; + const network = getPnAndNetwork(enrichedRequests[0].request)[1]; let firstConversionRequestExtension: IState | undefined; let firstNoConversionRequestExtension: IState | undefined; - const requestDetailsERC20NoConversion: PaymentTypes.RequestDetail[] = []; - const requestDetailsERC20Conversion: PaymentTypes.RequestDetail[] = []; - // fill requestDetailsERC20Conversion and requestDetailsERC20NoConversion lists + const ERC20NoConversionRequestDetails: PaymentTypes.RequestDetail[] = []; + const ERC20ConversionRequestDetails: PaymentTypes.RequestDetail[] = []; + + // fill ERC20ConversionRequestDetails and ERC20NoConversionRequestDetails lists for (const enrichedRequest of enrichedRequests) { if ( enrichedRequest.paymentNetworkId === @@ -126,7 +127,7 @@ export function encodePayBatchConversionRequest( ) { throw new Error(`wrong request currencyInfo type`); } - requestDetailsERC20Conversion.push(getInputRequestDetailERC20Conversion(enrichedRequest)); + ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest)); } else if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { @@ -135,42 +136,44 @@ export function encodePayBatchConversionRequest( // isERC20Currency is checked within getBatchArgs function comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request); - requestDetailsERC20NoConversion.push( - getInputRequestDetailERC20NoConversion(enrichedRequest.request), + if (!isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput)) { + throw new Error(`wrong request currencyInfo type`); + } + ERC20NoConversionRequestDetails.push( + getInputERC20NoConversionRequestDetail(enrichedRequest.request), ); } - if (firstNetwork !== getPnAndNetwork(enrichedRequest.request)[1]) + if (network !== getPnAndNetwork(enrichedRequest.request)[1]) throw new Error('All the requests must have the same network'); } const metaDetails: PaymentTypes.MetaDetail[] = []; - // Add requestDetailsERC20Conversion to metaDetails - if (requestDetailsERC20Conversion.length > 0) { + if (ERC20ConversionRequestDetails.length > 0) { + // Add ERC20 conversion payments metaDetails.push({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - requestDetails: requestDetailsERC20Conversion, + requestDetails: ERC20ConversionRequestDetails, }); } - // Add cryptoDetails to metaDetails - if (requestDetailsERC20NoConversion.length > 0) { - // add ERC20 no-conversion payments + if (ERC20NoConversionRequestDetails.length > 0) { + // Add multi ERC20 no-conversion payments metaDetails.push({ paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - requestDetails: requestDetailsERC20NoConversion, + requestDetails: ERC20NoConversionRequestDetails, }); } const pathsToUSD = getPathsToUSD( - [...requestDetailsERC20Conversion, ...requestDetailsERC20NoConversion], - firstNetwork, + [...ERC20ConversionRequestDetails, ...ERC20NoConversionRequestDetails], + network, skipFeeUSDLimit, ); const proxyContract = BatchConversionPayments__factory.createInterface(); return proxyContract.encodeFunctionData('batchPayments', [ metaDetails, - skipFeeUSDLimit ? [] : pathsToUSD, + pathsToUSD, feeAddress || constants.AddressZero, ]); } @@ -179,7 +182,7 @@ export function encodePayBatchConversionRequest( * Get the ERC20 no conversion input requestDetail from a request, that can be used by the batch contract. * @param request The request to pay. */ -function getInputRequestDetailERC20NoConversion( +function getInputERC20NoConversionRequestDetail( request: ClientTypes.IRequestData, ): PaymentTypes.RequestDetail { validateErc20FeeProxyRequest(request); @@ -202,7 +205,7 @@ function getInputRequestDetailERC20NoConversion( * Get the ERC20 conversion input requestDetail from an enriched request, that can be used by the batch contract. * @param enrichedRequest The enrichedRequest to pay. */ -function getInputRequestDetailERC20Conversion( +function getInputERC20ConversionRequestDetail( enrichedRequest: EnrichedRequest, ): PaymentTypes.RequestDetail { const paymentSettings = enrichedRequest.paymentSettings; @@ -247,18 +250,19 @@ function getPathsToUSD( network: string, skipFeeUSDLimit: boolean, ): string[][] { - const pathsToUSD: Array[] = []; + const pathsToUSD: Array> = []; if (!skipFeeUSDLimit) { const USDCurrency = currencyManager.fromSymbol('USD'); + // token's addresses paid with the batch const tokenAddresses: Array = []; for (const requestDetail of requestDetails) { const tokenAddress = requestDetail.path[requestDetail.path.length - 1]; - // Check token to only unique paths token to USD. - if (!tokenAddresses.includes(tokenAddress)) { + // Create a list of unique paths: token to USD. + if (USDCurrency && !tokenAddresses.includes(tokenAddress)) { tokenAddresses.push(tokenAddress); - const tokenCurrency = currencyManager.fromAddress(tokenAddress); - const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency!, network); + const tokenCurrency = currencyManager.fromAddress(tokenAddress, network); + const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency, network); if (pathToUSD) { pathsToUSD.push(pathToUSD); } diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 19b2ff0dc8..174ea28921 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -333,7 +333,6 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { * 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" * in batchConversionPayment.sol */ 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 feb15fe73c..8f2f2afbe1 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 @@ -11,7 +11,7 @@ import { getErc20Balance } from '../../src/payment/erc20'; import Utils from '@requestnetwork/utils'; import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; -import { currencyManager } from './shared'; +// import { currencyManager } from './shared'; import { approveErc20BatchConversionIfNeeded, getBatchConversionProxyAddress, @@ -19,9 +19,20 @@ import { prepareBatchConversionPaymentTransaction, } from '../../src/payment/batch-conversion-proxy'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; -import { UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; +const currencyManager = new CurrencyManager([ + ...CurrencyManager.getDefaultList(), + { + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', + decimals: 18, + network: 'private', + symbol: 'DAI', + type: RequestLogicTypes.CURRENCY.ERC20, + }, +]); + /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -52,7 +63,7 @@ const alphaPaymentSettings: IConversionPaymentSettings = { // requests setting -const EURExpectedAmount = 100; +const EURExpectedAmount = 55000; // 55 000 € const EURFeeAmount = 2; // amounts used for DAI and FAU requests const expectedAmount = 100000; @@ -182,6 +193,19 @@ describe('erc20-batch-conversion-proxy', () => { FAUTokenAddress, wallet, ); + + // Approve the contract to spent DAI with a conversion request + const approvalTx = await approveErc20BatchConversionIfNeeded( + EURValidRequest, + wallet.address, + batchConvVersion, + wallet.provider, + alphaPaymentSettings, + ); + expect(approvalTx).toBeDefined(); + if (approvalTx) { + await approvalTx.wait(1); + } }); describe(`Conversion:`, () => { @@ -341,78 +365,70 @@ describe('erc20-batch-conversion-proxy', () => { { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ - data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000005f5e10000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000cce41660000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(EURValidRequest, '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( - EURValidRequest, - 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 getErc20Balance( - DAIValidRequest, - wallet.address, - provider, - ); - - // Convert and pay - const tx = await payBatchConversionProxyRequest( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, - request: EURValidRequest, - paymentSettings: alphaPaymentSettings, - }, - ], - batchConvVersion, - wallet, - true, - ); - 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 getErc20Balance(DAIValidRequest, wallet.address, provider); - - // 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); - }); + for (const skipFeeUSDLimit in ['true', 'false']) { + it(`should convert and pay a request in EUR with ERC20, ${ + skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit' + } `, async () => { + // Get the balances to compare after payment + const initialETHFromBalance = await wallet.getBalance(); + const initialDAIFromBalance = await getErc20Balance( + DAIValidRequest, + wallet.address, + provider, + ); + + // Convert and pay + const tx = await payBatchConversionProxyRequest( + [ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + request: EURValidRequest, + paymentSettings: alphaPaymentSettings, + }, + ], + batchConvVersion, + wallet, + skipFeeUSDLimit === 'true', + ); + 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 getErc20Balance(DAIValidRequest, wallet.address, provider); + + // Check each balance + const amountToPay = expectedConversionAmount(EURExpectedAmount); + const feeToPay = expectedConversionAmount(EURFeeAmount); + const totalFeeToPay = + skipFeeUSDLimit === 'true' + ? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR) + : BigNumber.from('1984229702970297029'); // eq to 150$ batch fee (USD limit) + 2$ + const expectedAmountToPay = amountToPay.add(totalFeeToPay); + 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 initial balances const initialETHFromBalance = await wallet.getBalance(); @@ -542,9 +558,7 @@ describe('erc20-batch-conversion-proxy', () => { FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), - ).rejects.toThrowError( - 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', - ); + ).rejects.toThrowError('wrong request currencyInfo type'); }); it("should throw an error if one request's currencyInfo has no value", async () => { From ae46de206a7c7b5f7d88a061d403fa12654f704d Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:04:43 +0200 Subject: [PATCH 118/138] increase DAI funds to tests batch payments with big amounts and be above fee USD limit --- packages/smart-contracts/scripts/test-deploy-main-payments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smart-contracts/scripts/test-deploy-main-payments.ts b/packages/smart-contracts/scripts/test-deploy-main-payments.ts index 5a1d06b50c..0951ddd854 100644 --- a/packages/smart-contracts/scripts/test-deploy-main-payments.ts +++ b/packages/smart-contracts/scripts/test-deploy-main-payments.ts @@ -68,7 +68,7 @@ export default async function deploy(args: any, hre: HardhatRuntimeEnvironment): // Swap-to-pay related contracts // Payment erc20: ALPHA - const erc20AlphaInstance = await erc20Factory.deploy('1000000000000000000000000000000'); + const erc20AlphaInstance = await erc20Factory.deploy('1000000000000000000000000000000000000'); // Mock a swap router const { address: FakeSwapRouterAddress } = await deployOne(args, hre, 'FakeSwapRouter'); // 1 ERC20 = 2 ALPHA From fb8d60c4e178dbaef3b551a36545af80a0b9e3fa Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 18 Oct 2022 11:04:00 +0200 Subject: [PATCH 119/138] naming convention and update with master --- .../src/contracts/BatchConversionPayments.sol | 6 +++--- .../src/contracts/BatchNoConversionPayments.sol | 12 ++++++------ .../lib/artifacts/BatchConversionPayments/0.1.0.json | 6 +++--- .../artifacts/BatchNoConversionPayments/0.1.0.json | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 32b8b16cc5..7fec3f829a 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -42,7 +42,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @param _paymentNativeProxy The native payment proxy address to use. * @param _paymentErc20ConversionProxy The ERC20 Conversion payment proxy address to use. * @param _paymentNativeConversionFeeProxy The native Conversion payment proxy address to use. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( @@ -50,13 +50,13 @@ contract BatchConversionPayments is BatchNoConversionPayments { address _paymentNativeProxy, address _paymentErc20ConversionProxy, address _paymentNativeConversionFeeProxy, - address _chainlinkConversionPathAddress, + address _chainlinkConversionPath, address _owner ) BatchNoConversionPayments( _paymentErc20Proxy, _paymentNativeProxy, - _chainlinkConversionPathAddress, + _chainlinkConversionPath, _owner ) { diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 42cca6bc06..7a921b8a48 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -77,18 +77,18 @@ contract BatchNoConversionPayments is Ownable { /** * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. * @param _paymentNativeProxy The address to the Native fee payment proxy to use. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( address _paymentErc20Proxy, address _paymentNativeProxy, - address _chainlinkConversionPathAddress, + address _chainlinkConversionPath, address _owner ) { paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); paymentNativeProxy = IEthereumFeeProxy(_paymentNativeProxy); - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPath); transferOwnership(_owner); batchFee = 0; } @@ -562,10 +562,10 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Update the conversion path contract used to fetch conversions. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. */ - function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + function setChainlinkConversionPath(address _chainlinkConversionPath) external onlyOwner { + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPath); } /** 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 c04ceade09..48bfae5ed0 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 @@ -24,7 +24,7 @@ }, { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" }, { @@ -648,11 +648,11 @@ "inputs": [ { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" } ], - "name": "setConversionPathAddress", + "name": "setChainlinkConversionPath", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json index 06d163c967..e4438f8741 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json @@ -14,7 +14,7 @@ }, { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" }, { @@ -408,11 +408,11 @@ "inputs": [ { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" } ], - "name": "setConversionPathAddress", + "name": "setChainlinkConversionPath", "outputs": [], "stateMutability": "nonpayable", "type": "function" From fc92e9442db184ebd05e8abb5d49fcd2b808d7c2 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 18 Oct 2022 12:16:15 +0200 Subject: [PATCH 120/138] update with batch fee amount in USD limit --- packages/smart-contracts/hardhat.config.ts | 2 +- .../scripts-create2/constructor-args.ts | 3 +- .../contract-setup/adminTasks.ts | 63 ++++++++++---- .../setupBatchConversionPayments.ts | 85 ++++++++++++------- 4 files changed, 101 insertions(+), 52 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 2d392d5137..4216174c1e 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -162,7 +162,7 @@ export default { xdeploy: { salt: REQUEST_SALT, signer: process.env.ADMIN_PRIVATE_KEY, - networks: process.env.NETWORK ? [process.env.NETWORK] : ['xdai'], + networks: process.env.NETWORK ? [process.env.NETWORK] : ['goerli'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/constructor-args.ts b/packages/smart-contracts/scripts-create2/constructor-args.ts index d48d331127..8f78afef0c 100644 --- a/packages/smart-contracts/scripts-create2/constructor-args.ts +++ b/packages/smart-contracts/scripts-create2/constructor-args.ts @@ -40,7 +40,7 @@ export const getConstructorArgs = (contract: string, network?: string): string[] case 'BatchConversionPayments': { if (!network) { throw new Error( - 'Batch conversion contract requires network parameter to get correct address of erc20FeeProxy, erc20ConversionFeeProxy, ethereumFeeProxy, and ethereumConversionFeeProxy', + 'Batch conversion contract requires network parameter to get correct address of erc20FeeProxy, erc20ConversionFeeProxy, ethereumFeeProxy, ethereumConversionFeeProxy, and chainlinkConversionPath', ); } return [ @@ -48,6 +48,7 @@ export const getConstructorArgs = (contract: string, network?: string): string[] '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000', getAdminWalletAddress(contract), ]; } diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 6e8d4a8b0c..5f7d7e11e5 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -6,9 +6,11 @@ import { BigNumber } from 'ethers'; // Fees: 0.5% export const REQUEST_SWAP_FEES = 5; -// Batch conversion and no conversion fees: temporarily at 0% -const BATCH_NO_CONVERSION_FEE = 0; -const BATCH_CONVERSION_FEE = 0; +// Batch fee: temporarily at 0% +const BATCH_FEE = 0; + +// Batch fee amount in USD Limit: 150 * 1e8 ($150) +const BATCH_FEE_AMOUNT_USD_LIMIT = 150 * 1e8; export const updateChainlinkConversionPath = async ( contract: any, @@ -54,34 +56,33 @@ export const updateRequestSwapFees = async ( export const updateBatchPaymentFees = async (contract: any, gasPrice: BigNumber): Promise => { const currentFees = await contract.batchFee(); - if (currentFees.toNumber() !== BATCH_NO_CONVERSION_FEE) { + if (currentFees.toNumber() !== BATCH_FEE) { // Log is useful to have a direct view on was is being updated console.log( - `Batch: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_NO_CONVERSION_FEE}`, + `Batch: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_FEE}`, ); - await contract.setBatchFee(BATCH_NO_CONVERSION_FEE, { gasPrice: gasPrice }); + await contract.setBatchFee(BATCH_FEE, { gasPrice: gasPrice }); } }; -export const updateBatchConversionPaymentFees = async ( +export const updateBatchPaymentFeeAmountUSDLimit = async ( contract: any, gasPrice: BigNumber, ): Promise => { - const currentFees = await contract.batchConversionFee(); - if (currentFees.toNumber() !== BATCH_CONVERSION_FEE) { + const currentFees = await contract.batchFeeAmountUSDLimit(); + if (currentFees.toNumber() !== BATCH_FEE_AMOUNT_USD_LIMIT) { // Log is useful to have a direct view on was is being updated console.log( - `$Batch conversion: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_CONVERSION_FEE}`, + `Batch: the current fee amount in USD limit: ${currentFees.toString()}, have been replaced by: ${BATCH_FEE_AMOUNT_USD_LIMIT}. ($1 = 1e8)`, ); - await contract.setBatchConversionFee(BATCH_CONVERSION_FEE, { - gasPrice: gasPrice, - }); + await contract.setBatchFeeAmountUSDLimit(BATCH_FEE_AMOUNT_USD_LIMIT, { gasPrice: gasPrice }); } }; export const updatePaymentErc20FeeProxy = async ( contract: any, network: string, + nonce: number, gasPrice: BigNumber, ): Promise => { const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; @@ -89,19 +90,20 @@ export const updatePaymentErc20FeeProxy = async ( const currentAddress = await contract.paymentErc20FeeProxy(); if (currentAddress !== erc20FeeProxyAddress) { await contract.setPaymentErc20FeeProxy(erc20FeeProxyAddress, { + nonce: nonce, gasPrice: gasPrice, }); } }; /** - * Update the address of a payment proxy used by batch conversion contract + * Update the address of a proxy used by batch conversion contract */ -export const updateBatchConversionPaymentProxy = async ( +export const updateBatchConversionProxy = async ( contract: any, network: string, gasPrice: BigNumber, - proxyName: 'eth' | 'ethConversion' | 'erc20' | 'erc20Conversion', + proxyName: 'eth' | 'ethConversion' | 'erc20' | 'erc20Conversion' | 'chainlinkConversionPath', ): Promise => { try { let proxyAddress: string; @@ -119,11 +121,15 @@ export const updateBatchConversionPaymentProxy = async ( proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); batchSetProxy = await contract.setPaymentErc20Proxy; currentAddress = await contract.paymentErc20Proxy(); - } else { - // proxyName === "erc20Conversion" + } else if (proxyName === 'erc20Conversion') { proxyAddress = artifacts.erc20ConversionProxy.getAddress(network); batchSetProxy = await contract.setPaymentErc20ConversionProxy; currentAddress = await contract.paymentErc20ConversionProxy(); + } else { + // (proxyName === 'chainlinkConversionPath') + proxyAddress = artifacts.chainlinkConversionPath.getAddress(network); + batchSetProxy = await contract.setChainlinkConversionPath; + currentAddress = await contract.chainlinkConversionPath(); } if (currentAddress !== proxyAddress) { @@ -139,3 +145,24 @@ export const updateBatchConversionPaymentProxy = async ( console.log(e); } }; + +export const updateNativeAndUSDAddress = async ( + contract: any, + NativeAddress: string, + USDAddress: string, + gasPrice: BigNumber, +): Promise => { + const currentUSDAddress = await contract.USDAddress(); + const currentNativeAddress = await contract.NativeAddress(); + if (currentNativeAddress !== NativeAddress || currentUSDAddress !== USDAddress) { + console.log( + `Batch: the current NativeAddress: ${currentNativeAddress}, have been replaced by: ${NativeAddress}`, + ); + console.log( + `Batch: the current USDAddress: ${currentUSDAddress}, have been replaced by: ${USDAddress}`, + ); + await contract.setNativeAndUSDAddress(NativeAddress, USDAddress, { + gasPrice: gasPrice, + }); + } +}; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index 7fbe1356fb..2ea06f88d9 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -3,10 +3,12 @@ import { HardhatRuntimeEnvironmentExtended } from '../types'; import utils from '@requestnetwork/utils'; import { updateBatchPaymentFees, - updateBatchConversionPaymentProxy, - updateBatchConversionPaymentFees, + updateBatchConversionProxy, + updateBatchPaymentFeeAmountUSDLimit, + updateNativeAndUSDAddress, } from './adminTasks'; - +import { CurrencyManager } from '@requestnetwork/currency'; +import { RequestLogicTypes } from '@requestnetwork/types'; /** * Updates the values of the batch fees of the BatchConversionPayments contract, if needed * @param contractAddress address of the BatchConversionPayments Proxy @@ -21,8 +23,15 @@ export const setupBatchConversionPayments = async ( contractAddress, batchConversionPaymentsArtifact.getContractAbi(), ); + // constants related to chainlink and conversion rate + const currencyManager = CurrencyManager.getDefault(); await Promise.all( hre.config.xdeploy.networks.map(async (network) => { + const NativeAddress = currencyManager.getNativeCurrency( + RequestLogicTypes.CURRENCY.ETH, + network, + )!.hash; + const USDAddress = currencyManager.fromSymbol('USD')!.hash; console.log(`Setup BatchConversionPayments on ${network}`); let provider; if (network === 'celo') { @@ -37,35 +46,47 @@ export const setupBatchConversionPayments = async ( // start from the adminNonce, increase gasPrice if needed const gasCoef = 2; - await updateBatchPaymentFees(batchConversionPaymentConnected, gasPrice.mul(gasCoef)), - await updateBatchConversionPaymentFees( - batchConversionPaymentConnected, - gasPrice.mul(gasCoef), - ), - await updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'erc20', - ), - await updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'eth', - ), - await updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'erc20Conversion', - ), - await updateBatchConversionPaymentProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'ethConversion', - ); + await updateBatchPaymentFees(batchConversionPaymentConnected, gasPrice.mul(gasCoef)); + await updateBatchPaymentFeeAmountUSDLimit( + batchConversionPaymentConnected, + gasPrice.mul(gasCoef), + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'erc20', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'eth', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'erc20Conversion', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'ethConversion', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'chainlinkConversionPath', + ); + await updateNativeAndUSDAddress( + batchConversionPaymentConnected, + NativeAddress, + USDAddress, + gasPrice.mul(gasCoef), + ); }), ); console.log('Setup for setupBatchConversionPayment successfull'); From 1a4efd1ff6679e24ef7bb563fa6de8b937ab778e Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:43:59 +0200 Subject: [PATCH 121/138] deployment batch conversion with fee amount USD limit --- packages/smart-contracts/hardhat.config.ts | 3 +- .../contract-setup/adminTasks.ts | 61 +++++++++------ .../setupBatchConversionPayments.ts | 4 +- .../BatchConversionPayments/index.ts | 74 +++++++++---------- 4 files changed, 77 insertions(+), 65 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 4216174c1e..49905e367b 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -162,7 +162,8 @@ export default { xdeploy: { salt: REQUEST_SALT, signer: process.env.ADMIN_PRIVATE_KEY, - networks: process.env.NETWORK ? [process.env.NETWORK] : ['goerli'], + + networks: process.env.NETWORK ? [process.env.NETWORK] : ['celo'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 5f7d7e11e5..1abad18c99 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -21,10 +21,11 @@ export const updateChainlinkConversionPath = async ( const currentChainlinkAddress = await contract.chainlinkConversionPath(); const chainlinkConversionPathAddress = chainlinkConversionPath.getAddress(network, '0.1.0'); if (currentChainlinkAddress !== chainlinkConversionPathAddress) { - await contract.updateConversionPathAddress(chainlinkConversionPathAddress, { + const tx = await contract.updateConversionPathAddress(chainlinkConversionPathAddress, { nonce: nonce, gasPrice: gasPrice, }); + await tx.wait(); } }; @@ -55,13 +56,14 @@ export const updateRequestSwapFees = async ( }; export const updateBatchPaymentFees = async (contract: any, gasPrice: BigNumber): Promise => { - const currentFees = await contract.batchFee(); - if (currentFees.toNumber() !== BATCH_FEE) { + const currentFees = (await contract.batchFee()) as number; + if (currentFees - BATCH_FEE !== 0) { // Log is useful to have a direct view on was is being updated console.log( `Batch: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_FEE}`, ); - await contract.setBatchFee(BATCH_FEE, { gasPrice: gasPrice }); + const tx = await contract.setBatchFee(BATCH_FEE, { gasPrice: gasPrice }); + await tx.wait(); } }; @@ -69,13 +71,16 @@ export const updateBatchPaymentFeeAmountUSDLimit = async ( contract: any, gasPrice: BigNumber, ): Promise => { - const currentFees = await contract.batchFeeAmountUSDLimit(); - if (currentFees.toNumber() !== BATCH_FEE_AMOUNT_USD_LIMIT) { + const currentFeeAmountUSDLimit = (await contract.batchFeeAmountUSDLimit()) as number; + if (currentFeeAmountUSDLimit - BATCH_FEE_AMOUNT_USD_LIMIT !== 0) { // Log is useful to have a direct view on was is being updated console.log( - `Batch: the current fee amount in USD limit: ${currentFees.toString()}, have been replaced by: ${BATCH_FEE_AMOUNT_USD_LIMIT}. ($1 = 1e8)`, + `Batch: the current fee amount in USD limit: ${currentFeeAmountUSDLimit.toString()}, have been replaced by: ${BATCH_FEE_AMOUNT_USD_LIMIT}. ($1 = 1e8)`, ); - await contract.setBatchFeeAmountUSDLimit(BATCH_FEE_AMOUNT_USD_LIMIT, { gasPrice: gasPrice }); + const tx = await contract.setBatchFeeAmountUSDLimit(BATCH_FEE_AMOUNT_USD_LIMIT, { + gasPrice: gasPrice, + }); + await tx.wait(); } }; @@ -87,8 +92,8 @@ export const updatePaymentErc20FeeProxy = async ( ): Promise => { const erc20FeeProxy = artifacts.erc20FeeProxyArtifact; const erc20FeeProxyAddress = erc20FeeProxy.getAddress(network); - const currentAddress = await contract.paymentErc20FeeProxy(); - if (currentAddress !== erc20FeeProxyAddress) { + const currentAddress = (await contract.paymentErc20FeeProxy()) as string; + if (currentAddress.toLocaleLowerCase() !== erc20FeeProxyAddress.toLocaleLowerCase()) { await contract.setPaymentErc20FeeProxy(erc20FeeProxyAddress, { nonce: nonce, gasPrice: gasPrice, @@ -103,20 +108,25 @@ export const updateBatchConversionProxy = async ( contract: any, network: string, gasPrice: BigNumber, - proxyName: 'eth' | 'ethConversion' | 'erc20' | 'erc20Conversion' | 'chainlinkConversionPath', + proxyName: + | 'native' + | 'nativeConversion' + | 'erc20' + | 'erc20Conversion' + | 'chainlinkConversionPath', ): Promise => { try { let proxyAddress: string; let batchSetProxy: any; let currentAddress: string; - if (proxyName === 'eth') { + if (proxyName === 'native') { proxyAddress = artifacts.ethereumFeeProxyArtifact.getAddress(network); - batchSetProxy = await contract.setPaymentEthProxy; - currentAddress = await contract.paymentEthProxy(); - } else if (proxyName === 'ethConversion') { + batchSetProxy = await contract.setPaymentNativeProxy; + currentAddress = await contract.paymentNativeProxy(); + } else if (proxyName === 'nativeConversion') { proxyAddress = artifacts.ethConversionArtifact.getAddress(network); - batchSetProxy = await contract.setPaymentEthConversionProxy; - currentAddress = await contract.paymentEthConversionProxy(); + batchSetProxy = await contract.setPaymentNativeConversionProxy; + currentAddress = await contract.paymentNativeConversionProxy(); } else if (proxyName === 'erc20') { proxyAddress = artifacts.erc20FeeProxyArtifact.getAddress(network); batchSetProxy = await contract.setPaymentErc20Proxy; @@ -132,13 +142,14 @@ export const updateBatchConversionProxy = async ( currentAddress = await contract.chainlinkConversionPath(); } - if (currentAddress !== proxyAddress) { + if (currentAddress.toLocaleLowerCase() !== proxyAddress.toLocaleLowerCase()) { console.log( `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, ); - await batchSetProxy(proxyAddress, { + const tx = await batchSetProxy(proxyAddress, { gasPrice: gasPrice, }); + await tx.wait(); } } catch (e) { console.log(`Cannot update ${proxyName} proxy, it might not exist on this network`); @@ -152,17 +163,21 @@ export const updateNativeAndUSDAddress = async ( USDAddress: string, gasPrice: BigNumber, ): Promise => { - const currentUSDAddress = await contract.USDAddress(); - const currentNativeAddress = await contract.NativeAddress(); - if (currentNativeAddress !== NativeAddress || currentUSDAddress !== USDAddress) { + const currentUSDAddress = (await contract.USDAddress()).toLocaleLowerCase(); + const currentNativeAddress = (await contract.NativeAddress()).toLocaleLowerCase(); + if ( + currentNativeAddress !== NativeAddress.toLocaleLowerCase() || + currentUSDAddress !== USDAddress.toLocaleLowerCase() + ) { console.log( `Batch: the current NativeAddress: ${currentNativeAddress}, have been replaced by: ${NativeAddress}`, ); console.log( `Batch: the current USDAddress: ${currentUSDAddress}, have been replaced by: ${USDAddress}`, ); - await contract.setNativeAndUSDAddress(NativeAddress, USDAddress, { + const tx = await contract.setNativeAndUSDAddress(NativeAddress, USDAddress, { gasPrice: gasPrice, }); + await tx.wait(); } }; diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index 2ea06f88d9..bcd7e5d219 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -61,7 +61,7 @@ export const setupBatchConversionPayments = async ( batchConversionPaymentConnected, network, gasPrice.mul(gasCoef), - 'eth', + 'native', ); await updateBatchConversionProxy( batchConversionPaymentConnected, @@ -73,7 +73,7 @@ export const setupBatchConversionPayments = async ( batchConversionPaymentConnected, network, gasPrice.mul(gasCoef), - 'ethConversion', + 'nativeConversion', ); await updateBatchConversionProxy( batchConversionPaymentConnected, diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index eb1bae233e..a593c4a601 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -13,52 +13,48 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Tue, 18 Oct 2022 15:45:15 +0200 Subject: [PATCH 122/138] batch contract deployed for tests --- .../BatchConversionPayments/index.ts | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index eb1bae233e..a593c4a601 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -13,52 +13,48 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Wed, 19 Oct 2022 12:56:58 +0200 Subject: [PATCH 123/138] contract deployed WIP verify failing --- packages/smart-contracts/hardhat.config.ts | 3 +- .../scripts-create2/compute-one-address.ts | 8 +++- .../contract-setup/adminTasks.ts | 18 ++++---- .../smart-contracts/scripts-create2/verify.ts | 6 +++ .../BatchConversionPayments/index.ts | 42 +++++++++---------- 5 files changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 49905e367b..2f43a075fa 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -162,8 +162,7 @@ export default { xdeploy: { salt: REQUEST_SALT, signer: process.env.ADMIN_PRIVATE_KEY, - - networks: process.env.NETWORK ? [process.env.NETWORK] : ['celo'], + networks: process.env.NETWORK ? [process.env.NETWORK] : ['matic'], //['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index e3f8ed19fd..78bcda5e14 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -9,6 +9,7 @@ export async function computeCreate2DeploymentAddress( hre: HardhatRuntimeEnvironmentExtended, ): Promise { try { + console.log('HERE'); if (!hre.config.xdeploy.salt) { throw new Error('Missing salt'); } @@ -20,15 +21,19 @@ export async function computeCreate2DeploymentAddress( const provider = new hre.ethers.providers.JsonRpcProvider( 'https://api.avax.network/ext/bc/C/rpc', ); + console.log('HERE'); const RequestDeployer = requestDeployer.connect('avalanche', provider); + console.log('HERE deploymentParams', deploymentParams); + console.log('HERE deploymentParams.contract', deploymentParams.contract); const ContractToDeploy = await hre.ethers.getContractFactory(deploymentParams.contract); + console.log('HERE'); let initcode; if (deploymentParams.constructorArgs) { initcode = await ContractToDeploy.getDeployTransaction(...deploymentParams.constructorArgs); } else { initcode = await ContractToDeploy.getDeployTransaction(); } - + console.log('HERE f-1'); if (!initcode || !initcode.data) { throw new Error('Invalid initcode - check your contract and arguments'); } @@ -37,6 +42,7 @@ export async function computeCreate2DeploymentAddress( hre.ethers.utils.keccak256(initcode.data), hre.config.xdeploy.deployerAddress, ); + console.log('HERE F'); return computedAddress; } catch (e) { throw new Error(e.toString()); diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index 1abad18c99..ecb4d638bd 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -58,12 +58,12 @@ export const updateRequestSwapFees = async ( export const updateBatchPaymentFees = async (contract: any, gasPrice: BigNumber): Promise => { const currentFees = (await contract.batchFee()) as number; if (currentFees - BATCH_FEE !== 0) { + const tx = await contract.setBatchFee(BATCH_FEE, { gasPrice: gasPrice }); + await tx.wait(); // Log is useful to have a direct view on was is being updated console.log( `Batch: the current fees: ${currentFees.toString()}, have been replaced by: ${BATCH_FEE}`, ); - const tx = await contract.setBatchFee(BATCH_FEE, { gasPrice: gasPrice }); - await tx.wait(); } }; @@ -73,14 +73,14 @@ export const updateBatchPaymentFeeAmountUSDLimit = async ( ): Promise => { const currentFeeAmountUSDLimit = (await contract.batchFeeAmountUSDLimit()) as number; if (currentFeeAmountUSDLimit - BATCH_FEE_AMOUNT_USD_LIMIT !== 0) { - // Log is useful to have a direct view on was is being updated - console.log( - `Batch: the current fee amount in USD limit: ${currentFeeAmountUSDLimit.toString()}, have been replaced by: ${BATCH_FEE_AMOUNT_USD_LIMIT}. ($1 = 1e8)`, - ); const tx = await contract.setBatchFeeAmountUSDLimit(BATCH_FEE_AMOUNT_USD_LIMIT, { gasPrice: gasPrice, }); await tx.wait(); + // Log is useful to have a direct view on was is being updated + console.log( + `Batch: the current fee amount in USD limit: ${currentFeeAmountUSDLimit.toString()}, have been replaced by: ${BATCH_FEE_AMOUNT_USD_LIMIT}. ($1 = 1e8)`, + ); } }; @@ -143,13 +143,13 @@ export const updateBatchConversionProxy = async ( } if (currentAddress.toLocaleLowerCase() !== proxyAddress.toLocaleLowerCase()) { - console.log( - `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, - ); const tx = await batchSetProxy(proxyAddress, { gasPrice: gasPrice, }); await tx.wait(); + console.log( + `${proxyName}: the current address ${currentAddress} has been replaced by: ${proxyAddress}`, + ); } } catch (e) { console.log(`Cannot update ${proxyName} proxy, it might not exist on this network`); diff --git a/packages/smart-contracts/scripts-create2/verify.ts b/packages/smart-contracts/scripts-create2/verify.ts index f1b2034f18..78034d7c0a 100644 --- a/packages/smart-contracts/scripts-create2/verify.ts +++ b/packages/smart-contracts/scripts-create2/verify.ts @@ -10,10 +10,13 @@ export const verifyOne = async ( hre: HardhatRuntimeEnvironmentExtended, ): Promise => { try { + console.log('here verifyOne start'); + console.log('contractAddress', contractAddress); await hre.run('verify:verify', { address: contractAddress, constructorArguments: deploymentParams.constructorArgs, }); + console.log('here verifyOne end'); } catch (err) { console.log(err); } @@ -46,6 +49,9 @@ export async function VerifyCreate2FromList(hre: HardhatRuntimeEnvironmentExtend case 'BatchConversionPayments': { const network = hre.config.xdeploy.networks[0]; const constructorArgs = getConstructorArgs(contract, network); + console.log('network', network); + console.log('hre', hre.config.xdeploy); + console.log('hre', hre.config); address = await computeCreate2DeploymentAddress({ contract, constructorArgs }, hre); await verifyOne(address, { contract, constructorArgs }, hre); break; diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index a593c4a601..c9bfc9ec76 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -33,28 +33,28 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Wed, 19 Oct 2022 17:49:48 +0200 Subject: [PATCH 124/138] add Yo PR - function s comments on batch contracts --- .../src/contracts/BatchNoConversionPayments.sol | 5 +++-- .../test/contracts/BatchConversionPayments.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 7a921b8a48..f0ed24a2c2 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -102,7 +102,7 @@ contract BatchNoConversionPayments is Ownable { } /** - * @notice Send a batch of Native (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native token payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. * @param requestDetails List of Native tokens requests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. @@ -155,7 +155,7 @@ contract BatchNoConversionPayments is Ownable { } /** - * @notice Send a batch of Native (or EVM native token) payments with fees and paymentReferences to multiple accounts. + * @notice Send a batch of Native token payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. * @param requestDetails List of Native tokens requests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. @@ -337,6 +337,7 @@ contract BatchNoConversionPayments is Ownable { /** * Top up the contract with enough `requestedToken` to pay `amountAndFee`. + * The contract is NOT topped-up for `batchFeeAmount`. * * It also performs a few checks: * - checks that the batch contract has enough allowance from the payer diff --git a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts index babd8ed66b..5f12cc1315 100644 --- a/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts +++ b/packages/smart-contracts/test/contracts/BatchConversionPayments.test.ts @@ -361,7 +361,7 @@ describe('contract: BatchConversionPayments', async () => { describe('batchPayments', async () => { const testBatchPayment = async (applyLimit: boolean) => { - // Limit is applied if there are paths to USD + // Limit is applied if there are paths to USD: skipFeeUSDLimit const pathsToUSD = applyLimit ? [ [FAU_address, USD_hash], @@ -371,7 +371,7 @@ describe('contract: BatchConversionPayments', async () => { describe(`payment with${ applyLimit ? '' : 'out' - } application of the batch fee limit USD`, async () => { + } application of the batch fee limit USD: skipFeeUSDLimit`, async () => { it(`make 1 ERC20 payment with no conversion, BATCH_ERC20_PAYMENTS`, async () => { const [initialFromFAUBalance, initialToFAUBalance, initialFeeFAUBalance] = await getERC20Balances(fauERC20); From a154712fa0417c1697ecdce89813caa6b13da1b9 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 20 Oct 2022 11:46:48 +0200 Subject: [PATCH 125/138] files cleaned --- packages/smart-contracts/README.md | 3 ++- .../scripts-create2/compute-one-address.ts | 8 +------- packages/smart-contracts/scripts-create2/verify.ts | 6 ------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/smart-contracts/README.md b/packages/smart-contracts/README.md index 4cbfa67c4a..53a718b9b4 100644 --- a/packages/smart-contracts/README.md +++ b/packages/smart-contracts/README.md @@ -157,7 +157,8 @@ This command will output details about each contract deployment on each chain: ### Verify the contracts -Verify and publish the contract code automatically to blockchain explorers, right after smart contracts compilation. You should first set the `ETHERSCAN_API_KEY` environment variable. +Verify and publish the contract code automatically to blockchain explorers, right after smart contracts compilation. +Environment variables needed: `ADMIN_...` key and wallet, `ETHERSCAN_API_KEY`, and `REQUEST_DEPLOYER_LIVE`. ```bash yarn hardhat verify-contract-from-deployer --network diff --git a/packages/smart-contracts/scripts-create2/compute-one-address.ts b/packages/smart-contracts/scripts-create2/compute-one-address.ts index 78bcda5e14..e3f8ed19fd 100644 --- a/packages/smart-contracts/scripts-create2/compute-one-address.ts +++ b/packages/smart-contracts/scripts-create2/compute-one-address.ts @@ -9,7 +9,6 @@ export async function computeCreate2DeploymentAddress( hre: HardhatRuntimeEnvironmentExtended, ): Promise { try { - console.log('HERE'); if (!hre.config.xdeploy.salt) { throw new Error('Missing salt'); } @@ -21,19 +20,15 @@ export async function computeCreate2DeploymentAddress( const provider = new hre.ethers.providers.JsonRpcProvider( 'https://api.avax.network/ext/bc/C/rpc', ); - console.log('HERE'); const RequestDeployer = requestDeployer.connect('avalanche', provider); - console.log('HERE deploymentParams', deploymentParams); - console.log('HERE deploymentParams.contract', deploymentParams.contract); const ContractToDeploy = await hre.ethers.getContractFactory(deploymentParams.contract); - console.log('HERE'); let initcode; if (deploymentParams.constructorArgs) { initcode = await ContractToDeploy.getDeployTransaction(...deploymentParams.constructorArgs); } else { initcode = await ContractToDeploy.getDeployTransaction(); } - console.log('HERE f-1'); + if (!initcode || !initcode.data) { throw new Error('Invalid initcode - check your contract and arguments'); } @@ -42,7 +37,6 @@ export async function computeCreate2DeploymentAddress( hre.ethers.utils.keccak256(initcode.data), hre.config.xdeploy.deployerAddress, ); - console.log('HERE F'); return computedAddress; } catch (e) { throw new Error(e.toString()); diff --git a/packages/smart-contracts/scripts-create2/verify.ts b/packages/smart-contracts/scripts-create2/verify.ts index 78034d7c0a..f1b2034f18 100644 --- a/packages/smart-contracts/scripts-create2/verify.ts +++ b/packages/smart-contracts/scripts-create2/verify.ts @@ -10,13 +10,10 @@ export const verifyOne = async ( hre: HardhatRuntimeEnvironmentExtended, ): Promise => { try { - console.log('here verifyOne start'); - console.log('contractAddress', contractAddress); await hre.run('verify:verify', { address: contractAddress, constructorArguments: deploymentParams.constructorArgs, }); - console.log('here verifyOne end'); } catch (err) { console.log(err); } @@ -49,9 +46,6 @@ export async function VerifyCreate2FromList(hre: HardhatRuntimeEnvironmentExtend case 'BatchConversionPayments': { const network = hre.config.xdeploy.networks[0]; const constructorArgs = getConstructorArgs(contract, network); - console.log('network', network); - console.log('hre', hre.config.xdeploy); - console.log('hre', hre.config); address = await computeCreate2DeploymentAddress({ contract, constructorArgs }, hre); await verifyOne(address, { contract, constructorArgs }, hre); break; From 502741d0f9614e338b39152405c80151aac45120 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:28:11 +0200 Subject: [PATCH 126/138] contract deployed --- packages/smart-contracts/README.md | 2 +- packages/smart-contracts/hardhat.config.ts | 2 +- .../src/lib/artifacts/BatchConversionPayments/index.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/smart-contracts/README.md b/packages/smart-contracts/README.md index 53a718b9b4..b0e558078b 100644 --- a/packages/smart-contracts/README.md +++ b/packages/smart-contracts/README.md @@ -158,7 +158,7 @@ This command will output details about each contract deployment on each chain: ### Verify the contracts Verify and publish the contract code automatically to blockchain explorers, right after smart contracts compilation. -Environment variables needed: `ADMIN_...` key and wallet, `ETHERSCAN_API_KEY`, and `REQUEST_DEPLOYER_LIVE`. +Environment variables needed: `ADMIN_...` key and wallet, `ETHERSCAN_API_KEY`, and `REQUEST_DEPLOYER_LIVE`. If you deploy on mainnet, add `WEB3_PROVIDER_URL`. ```bash yarn hardhat verify-contract-from-deployer --network diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index 2f43a075fa..da8a7d7039 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -162,7 +162,7 @@ export default { xdeploy: { salt: REQUEST_SALT, signer: process.env.ADMIN_PRIVATE_KEY, - networks: process.env.NETWORK ? [process.env.NETWORK] : ['matic'], //['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], + networks: process.env.NETWORK ? [process.env.NETWORK] : ['mainnet'], //['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index c9bfc9ec76..8e24a943a7 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -17,10 +17,10 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Tue, 18 Oct 2022 11:04:00 +0200 Subject: [PATCH 127/138] naming convention and update with master --- .../src/contracts/BatchConversionPayments.sol | 6 +++--- .../src/contracts/BatchNoConversionPayments.sol | 12 ++++++------ .../lib/artifacts/BatchConversionPayments/0.1.0.json | 6 +++--- .../artifacts/BatchNoConversionPayments/0.1.0.json | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 32b8b16cc5..7fec3f829a 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -42,7 +42,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @param _paymentNativeProxy The native payment proxy address to use. * @param _paymentErc20ConversionProxy The ERC20 Conversion payment proxy address to use. * @param _paymentNativeConversionFeeProxy The native Conversion payment proxy address to use. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( @@ -50,13 +50,13 @@ contract BatchConversionPayments is BatchNoConversionPayments { address _paymentNativeProxy, address _paymentErc20ConversionProxy, address _paymentNativeConversionFeeProxy, - address _chainlinkConversionPathAddress, + address _chainlinkConversionPath, address _owner ) BatchNoConversionPayments( _paymentErc20Proxy, _paymentNativeProxy, - _chainlinkConversionPathAddress, + _chainlinkConversionPath, _owner ) { diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index 42cca6bc06..7a921b8a48 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -77,18 +77,18 @@ contract BatchNoConversionPayments is Ownable { /** * @param _paymentErc20Proxy The address to the ERC20 fee payment proxy to use. * @param _paymentNativeProxy The address to the Native fee payment proxy to use. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. * @param _owner Owner of the contract. */ constructor( address _paymentErc20Proxy, address _paymentNativeProxy, - address _chainlinkConversionPathAddress, + address _chainlinkConversionPath, address _owner ) { paymentErc20Proxy = IERC20FeeProxy(_paymentErc20Proxy); paymentNativeProxy = IEthereumFeeProxy(_paymentNativeProxy); - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPath); transferOwnership(_owner); batchFee = 0; } @@ -562,10 +562,10 @@ contract BatchNoConversionPayments is Ownable { /** * @notice Update the conversion path contract used to fetch conversions. - * @param _chainlinkConversionPathAddress The address of the conversion path contract. + * @param _chainlinkConversionPath The address of the conversion path contract. */ - function setConversionPathAddress(address _chainlinkConversionPathAddress) external onlyOwner { - chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPathAddress); + function setChainlinkConversionPath(address _chainlinkConversionPath) external onlyOwner { + chainlinkConversionPath = ChainlinkConversionPath(_chainlinkConversionPath); } /** 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 c04ceade09..48bfae5ed0 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 @@ -24,7 +24,7 @@ }, { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" }, { @@ -648,11 +648,11 @@ "inputs": [ { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" } ], - "name": "setConversionPathAddress", + "name": "setChainlinkConversionPath", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json index 06d163c967..e4438f8741 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json +++ b/packages/smart-contracts/src/lib/artifacts/BatchNoConversionPayments/0.1.0.json @@ -14,7 +14,7 @@ }, { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" }, { @@ -408,11 +408,11 @@ "inputs": [ { "internalType": "address", - "name": "_chainlinkConversionPathAddress", + "name": "_chainlinkConversionPath", "type": "address" } ], - "name": "setConversionPathAddress", + "name": "setChainlinkConversionPath", "outputs": [], "stateMutability": "nonpayable", "type": "function" From ee220e2e9b809fb1e5c1ba03537c0345bc000161 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Mon, 17 Oct 2022 17:47:27 +0200 Subject: [PATCH 128/138] fix(graph retriever): null values (#961) --- .../src/thegraph/info-retriever.ts | 2 +- .../test/erc20/fee-proxy-contract.test.ts | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/payment-detection/src/thegraph/info-retriever.ts b/packages/payment-detection/src/thegraph/info-retriever.ts index 503ae46d18..7f7fadd888 100644 --- a/packages/payment-detection/src/thegraph/info-retriever.ts +++ b/packages/payment-detection/src/thegraph/info-retriever.ts @@ -108,7 +108,7 @@ export class TheGraphInfoRetriever { 'feeAmountInCrypto', 'maxRateTimespan', ), - String, + (val) => (val !== null ? String(val) : undefined), ), // Make sure the checksum is right for addresses. ...mapValues(pick(payment, 'from', 'feeAddress', 'tokenAddress'), (str, key) => 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 f2308527a3..2c423a5261 100644 --- a/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts +++ b/packages/payment-detection/test/erc20/fee-proxy-contract.test.ts @@ -425,6 +425,11 @@ describe('api/erc20/fee-proxy-contract', () => { txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', gasUsed: '73152', gasPrice: '12709127644', + timestamp: 1666002347, + amountInCrypto: null, + feeAddress: '0x35d0e078755cd84d3e0656caab417dee1d7939c7', + feeAmountInCrypto: null, + maxRateTimespan: null, }, ].filter((x) => x.reference.toLowerCase() === reference.toLowerCase()), escrowEvents: [], @@ -434,8 +439,29 @@ describe('api/erc20/fee-proxy-contract', () => { }), }); - const { balance, error } = await erc20FeeProxyContract.getBalance(mockRequest); + const { balance, error, events } = await erc20FeeProxyContract.getBalance(mockRequest); expect(error).toBeUndefined(); expect(balance).toBe('168040800000000000000000'); + expect(events).toMatchObject([ + { + amount: '168040800000000000000000', + name: 'payment', + parameters: { + amountInCrypto: undefined, + block: 15767215, + feeAddress: '0x35d0e078755Cd84D3E0656cAaB417Dee1d7939c7', + feeAmount: '13386000000000000000', + feeAmountInCrypto: undefined, + from: '0x15339d48Fbe31E349A507FD6d48Eb01c45Fdc79A', + gasPrice: '12709127644', + gasUsed: '73152', + maxRateTimespan: undefined, + to: '0x6c9E04997000d6A8a353951231923d776d4Cdff2', + tokenAddress: '0x967da4048cD07aB37855c090aAF366e4ce1b9F48', + txHash: '0x456d67cba236778e91a901e97c71684e82317dc2679d1b5c6bfa6d420d636b7d', + }, + timestamp: 1666002347, + }, + ]); }); }); From 84431e363cd0a53a6617c1ab3671bf9610dccd85 Mon Sep 17 00:00:00 2001 From: Yo <56731761+yomarion@users.noreply.github.com> Date: Tue, 18 Oct 2022 15:17:01 +0200 Subject: [PATCH 129/138] fix: NEAR native timestamp precision (#963) --- packages/payment-detection/test/near/near-native.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/payment-detection/test/near/near-native.test.ts b/packages/payment-detection/test/near/near-native.test.ts index 58ed081cc3..3694bc9297 100644 --- a/packages/payment-detection/test/near/near-native.test.ts +++ b/packages/payment-detection/test/near/near-native.test.ts @@ -64,7 +64,7 @@ describe('Near payments detection', () => { expect(events).toHaveLength(1); expect(events[0].amount).toBe('1000000000000000000000000'); - expect(events[0].timestamp).toBe(1631788427230); + expect(events[0].timestamp).toBe(1631788427); expect(events[0].parameters?.receiptId).toBe('FYVnCvJFoNtK7LE2uAdTFfReFMGiCUHMczLsvEni1Cpf'); expect(events[0].parameters?.txHash).toBeUndefined(); expect(events[0].parameters?.block).toBe(47891257); From 17608de11250b467337c4a371ca959ded34924ae Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Thu, 20 Oct 2022 16:35:44 +0200 Subject: [PATCH 130/138] add batch proxy addresses and clean tests --- .../payment/any-to-erc20-batch-proxy.test.ts | 33 +++++------- .../BatchConversionPayments/index.ts | 50 +++++++++---------- 2 files changed, 37 insertions(+), 46 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 8f2f2afbe1..5d06704a31 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 @@ -245,7 +245,6 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, - true, ), ).rejects.toThrowError( new UnsupportedCurrencyError({ @@ -266,14 +265,13 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, - true, ), ).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, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError(`wrong request currencyInfo type`); }); it('should throw an error if the request has a wrong network', async () => { @@ -295,7 +293,7 @@ describe('erc20-batch-conversion-proxy', () => { }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + 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 () => { @@ -316,7 +314,7 @@ describe('erc20-batch-conversion-proxy', () => { }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-any-to-erc20-proxy request', ); @@ -324,13 +322,13 @@ describe('erc20-batch-conversion-proxy', () => { it("should throw an error if one request's currencyInfo has no value", async () => { EURRequest.currencyInfo.value = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + 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, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('no payment network found'); }); it('should throw an error if there is a wrong version mapping', async () => { @@ -341,7 +339,7 @@ describe('erc20-batch-conversion-proxy', () => { }, }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -447,7 +445,6 @@ describe('erc20-batch-conversion-proxy', () => { }), batchConvVersion, wallet, - true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -503,7 +500,6 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, - true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -557,14 +553,14 @@ describe('erc20-batch-conversion-proxy', () => { it('should throw an error if the request is not erc20', async () => { FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('wrong request currencyInfo type'); }); it("should throw an error if one request's currencyInfo has no value", async () => { FAURequest.currencyInfo.value = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', ); @@ -573,7 +569,7 @@ describe('erc20-batch-conversion-proxy', () => { it("should throw an error if one request's currencyInfo has no network", async () => { FAURequest.currencyInfo.network = ''; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError( 'request cannot be processed, or is not an pn-erc20-fee-proxy-contract request', ); @@ -582,7 +578,7 @@ describe('erc20-batch-conversion-proxy', () => { it('should throw an error if request has no extension', async () => { FAURequest.extensions = [] as any; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('no payment network found'); }); @@ -594,7 +590,7 @@ describe('erc20-batch-conversion-proxy', () => { }, }; await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true), + payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -650,12 +646,7 @@ describe('erc20-batch-conversion-proxy', () => { const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); // Batch payment - const tx = await payBatchConversionProxyRequest( - enrichedRequests, - batchConvVersion, - wallet, - true, - ); + const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index a593c4a601..8e24a943a7 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -17,10 +17,10 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Fri, 21 Oct 2022 12:32:03 +0200 Subject: [PATCH 131/138] set not to skipFeeUSDLimit by default and fix tests with currencyManager update and use right units --- .../src/payment/batch-conversion-proxy.ts | 19 ++++++++++++++----- .../payment/any-to-erc20-batch-proxy.test.ts | 18 +++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index ef0aca1dc3..b1416f3cbd 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, PaymentTypes } from '@requestnetwork/types'; +import { ClientTypes, PaymentTypes, RequestLogicTypes } from '@requestnetwork/types'; import { ITransactionOverrides } from './transaction-overrides'; import { comparePnTypeAndVersion, @@ -30,7 +30,16 @@ import { CurrencyManager, } from '@requestnetwork/currency'; -const currencyManager = CurrencyManager.getDefault(); +const currencyManager = new CurrencyManager([ + ...CurrencyManager.getDefaultList(), + { + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', + decimals: 18, + network: 'private', + symbol: 'DAI', + type: RequestLogicTypes.CURRENCY.ERC20, + }, +]); /** * Processes a transaction to pay a batch of requests with an ERC20 currency @@ -51,7 +60,7 @@ export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], version: string, signerOrProvider: providers.Provider | Signer = getProvider(), - skipFeeUSDLimit = true, + skipFeeUSDLimit = false, overrides?: ITransactionOverrides, ): Promise { const { data, to, value } = prepareBatchConversionPaymentTransaction( @@ -76,7 +85,7 @@ export async function payBatchConversionProxyRequest( export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], version: string, - skipFeeUSDLimit = true, + skipFeeUSDLimit = false, ): IPreparedTransaction { const encodedTx = encodePayBatchConversionRequest(enrichedRequests, skipFeeUSDLimit); const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); @@ -98,7 +107,7 @@ export function prepareBatchConversionPaymentTransaction( */ export function encodePayBatchConversionRequest( enrichedRequests: EnrichedRequest[], - skipFeeUSDLimit = true, + skipFeeUSDLimit = false, ): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); 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 5d06704a31..5c4934a005 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 @@ -11,7 +11,6 @@ import { getErc20Balance } from '../../src/payment/erc20'; import Utils from '@requestnetwork/utils'; import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; -// import { currencyManager } from './shared'; import { approveErc20BatchConversionIfNeeded, getBatchConversionProxyAddress, @@ -63,8 +62,8 @@ const alphaPaymentSettings: IConversionPaymentSettings = { // requests setting -const EURExpectedAmount = 55000; // 55 000 € -const EURFeeAmount = 2; +const EURExpectedAmount = 55000_00; // 55 000 € +const EURFeeAmount = 2_00; // 2€ // amounts used for DAI and FAU requests const expectedAmount = 100000; const feeAmount = 100; @@ -363,14 +362,14 @@ describe('erc20-batch-conversion-proxy', () => { { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ - data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000cce41660000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000001e84800000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + data: '0x92cddb91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000c5fdf4076b8f3a5357c5e395ab970b5b54098fef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b73200000000000000000000000000000000000000000000000000000500918bd80000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000bebc2000000000000000000000000000000000000000000204fce5e3e250261100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000017b4158805772ced11225e77339f90beb5aae968000000000000000000000000775eb53d00dd0acd3ec1696472105d579b9b386b00000000000000000000000038cf23c52bb4b13f051aec09580a2de845a7fa35000000000000000000000000000000000000000000000000000000000000000886dfbccad783599a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', gasPrice: '20000000000', to: getBatchConversionProxyAddress(EURValidRequest, '0.1.0'), value: 0, }); wallet.sendTransaction = originalSendTransaction; }); - for (const skipFeeUSDLimit in ['true', 'false']) { + for (const skipFeeUSDLimit of ['true', 'false']) { it(`should convert and pay a request in EUR with ERC20, ${ skipFeeUSDLimit === 'true' ? 'skipFeeUSDLimit' : 'no skipFeeUSDLimit' } `, async () => { @@ -408,15 +407,15 @@ describe('erc20-batch-conversion-proxy', () => { const feeToPay = expectedConversionAmount(EURFeeAmount); const totalFeeToPay = skipFeeUSDLimit === 'true' - ? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR) - : BigNumber.from('1984229702970297029'); // eq to 150$ batch fee (USD limit) + 2$ + ? amountToPay.add(feeToPay).mul(BATCH_CONV_FEE).div(BATCH_DENOMINATOR).add(feeToPay) + : BigNumber.from('150891089116411368418'); // eq to $150 batch fee (USD limit) + 2€ const expectedAmountToPay = amountToPay.add(totalFeeToPay); expect( BigNumber.from(initialETHFromBalance).sub(ETHFromBalance).toNumber(), ).toBeGreaterThan(0); expect( BigNumber.from(initialDAIFromBalance).sub(BigNumber.from(DAIFromBalance)), - // Calculation of expectedAmountToPay + // Calculation of expectedAmountToPay when there there is no fee USD limit // expectedAmount: 1.00 // feeAmount: + .02 // = 1.02 @@ -435,7 +434,6 @@ describe('erc20-batch-conversion-proxy', () => { wallet.address, provider, ); - alphaPaymentSettings.maxToSpend = alphaPaymentSettings.maxToSpend; // Convert and pay const tx = await payBatchConversionProxyRequest( Array(2).fill({ @@ -445,6 +443,7 @@ describe('erc20-batch-conversion-proxy', () => { }), batchConvVersion, wallet, + true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -500,6 +499,7 @@ describe('erc20-batch-conversion-proxy', () => { ], batchConvVersion, wallet, + true, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); From 1db36164d8e05e6368249431130f6f3a024d7392 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 24 Oct 2022 17:32:06 +0200 Subject: [PATCH 132/138] modify FAKE token and add DAI token within private currencyManager --- .../currency/src/aggregators/private.json | 3 ++ packages/currency/src/currency-utils.ts | 32 ++++++++++++++++--- packages/currency/src/erc20/networks/index.ts | 2 ++ .../currency/src/erc20/networks/private.ts | 11 +++++++ .../conversion-supported-currencies.test.ts | 4 +-- .../currency/test/currencyManager.test.ts | 8 ++--- 6 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 packages/currency/src/erc20/networks/private.ts diff --git a/packages/currency/src/aggregators/private.json b/packages/currency/src/aggregators/private.json index c1829cf411..73894d48b6 100644 --- a/packages/currency/src/aggregators/private.json +++ b/packages/currency/src/aggregators/private.json @@ -16,5 +16,8 @@ }, "0x8acee021a27779d8e98b9650722676b850b25e11": { "0xf5af88e117747e87fc5929f2ff87221b1447652e": 1 + }, + "0x38cf23c52bb4b13f051aec09580a2de845a7fa36": { + "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 } } diff --git a/packages/currency/src/currency-utils.ts b/packages/currency/src/currency-utils.ts index 53894d1e83..c7252deee3 100644 --- a/packages/currency/src/currency-utils.ts +++ b/packages/currency/src/currency-utils.ts @@ -53,19 +53,41 @@ export const isValidNearAddress = (address: string, network?: string): boolean = export const isNativeCurrency = (currency: CurrencyInput): currency is NativeCurrencyInput => { return ( - currency.type === RequestLogicTypes.CURRENCY.BTC || - currency.type === RequestLogicTypes.CURRENCY.ETH + isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.BTC) || + isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ETH) ); }; export const isISO4217Currency = (currency: CurrencyInput): currency is ISO4217CurrencyInput => { - return currency.type === RequestLogicTypes.CURRENCY.ISO4217; + return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ISO4217); }; export const isERC20Currency = (currency: CurrencyInput): currency is ERC20CurrencyInput => { - return currency.type === RequestLogicTypes.CURRENCY.ERC20; + return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ERC20); }; export const isERC777Currency = (currency: CurrencyInput): currency is ERC777CurrencyInput => { - return currency.type === RequestLogicTypes.CURRENCY.ERC777; + return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ERC777); }; + +export const isAgreedCurrencyType = ( + currency: Pick, + currencyAgreed: RequestLogicTypes.CURRENCY, +): boolean => currency.type === currencyAgreed; + +// export const isISO4217CurrencyType = (currency: Pick): boolean => +// currency.type === RequestLogicTypes.CURRENCY.ISO4217; + +// export const isAgreedCurrencyType = (currency: Pick, currencyAgreed: RequestLogicTypes.CURRENCY): boolean => +// currency.type === currencyAgreed; + +// export const isERC20Currency = (currency: CurrencyInput): currency is ERC20CurrencyInput => { +// return isERC20CurrencyType(currency); +// }; + +// export const isERC20CurrencyType = (currency: Pick): boolean => +// currency.type === RequestLogicTypes.CURRENCY.ERC20; + +// export const isERC777Currency = (currency: CurrencyInput): currency is ERC777CurrencyInput => { +// return currency.type === RequestLogicTypes.CURRENCY.ERC777; +// }; diff --git a/packages/currency/src/erc20/networks/index.ts b/packages/currency/src/erc20/networks/index.ts index ee43dbdebe..7bb0469b8f 100644 --- a/packages/currency/src/erc20/networks/index.ts +++ b/packages/currency/src/erc20/networks/index.ts @@ -8,6 +8,7 @@ import { supportedBSCTestERC20 } from './bsctest'; import { supportedBSCERC20 } from './bsc'; import { supportedXDAIERC20 } from './xdai'; import { supportedGoerliERC20 } from './goerli'; +import { supportedPrivateERC20 } from './private'; export const supportedNetworks: Record = { celo: supportedCeloERC20, @@ -20,6 +21,7 @@ export const supportedNetworks: Record = { bsctest: supportedBSCTestERC20, bsc: supportedBSCERC20, xdai: supportedXDAIERC20, + private: supportedPrivateERC20, }; export type { TokenMap }; diff --git a/packages/currency/src/erc20/networks/private.ts b/packages/currency/src/erc20/networks/private.ts new file mode 100644 index 0000000000..0d5d8a3f76 --- /dev/null +++ b/packages/currency/src/erc20/networks/private.ts @@ -0,0 +1,11 @@ +import { TokenMap } from './types'; + +// List of the supported private ERC20 tokens +export const supportedPrivateERC20: TokenMap = { + // DAI Token on private network. + '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35': { + name: '(PoS) Dai Stablecoin', + symbol: 'DAI', + decimals: 18, + }, +}; diff --git a/packages/currency/test/conversion-supported-currencies.test.ts b/packages/currency/test/conversion-supported-currencies.test.ts index b8bde61383..863a8b3d98 100644 --- a/packages/currency/test/conversion-supported-currencies.test.ts +++ b/packages/currency/test/conversion-supported-currencies.test.ts @@ -4,7 +4,7 @@ import { RequestLogicTypes } from '@requestnetwork/types'; const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), { - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa36', decimals: 18, network: 'private', symbol: 'FAKE', @@ -71,7 +71,7 @@ describe('supported currencies with oracles from chainlink', () => { '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', ], - private: ['0x38cf23c52bb4b13f051aec09580a2de845a7fa35'], + private: ['0x38cf23c52bb4b13f051aec09580a2de845a7fa36'], }).forEach(([network, addresses]) => { describe(network, () => { addresses.forEach((address) => { diff --git a/packages/currency/test/currencyManager.test.ts b/packages/currency/test/currencyManager.test.ts index 0512152570..112341b935 100644 --- a/packages/currency/test/currencyManager.test.ts +++ b/packages/currency/test/currencyManager.test.ts @@ -115,21 +115,21 @@ describe('CurrencyManager', () => { { type: RequestLogicTypes.CURRENCY.ERC20, symbol: 'FAKE', - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa36', decimals: 18, network: 'private', }, ]); const fake = currencyManager.from('FAKE') as ERC20Currency; // right case - expect(fake.address).toBe('0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'); + expect(fake.address).toBe('0x38cf23C52bb4B13F051aEc09580A2de845A7Fa36'); // it can match it from right case or wrong - expect(currencyManager.fromAddress('0x38cf23c52bb4b13f051aec09580a2de845a7fa35')?.id).toBe( + expect(currencyManager.fromAddress('0x38cf23c52bb4b13f051aec09580a2de845a7fa36')?.id).toBe( 'FAKE-private', ); - expect(currencyManager.fromAddress('0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35')?.id).toBe( + expect(currencyManager.fromAddress('0x38cf23C52bb4B13F051aEc09580A2de845A7Fa36')?.id).toBe( 'FAKE-private', ); }); From a30f233b5a831aea5b6187ab81b204322b168fb1 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Mon, 24 Oct 2022 18:21:11 +0200 Subject: [PATCH 133/138] Revert "modify FAKE token and add DAI token within private currencyManager" This reverts commit 1db36164d8e05e6368249431130f6f3a024d7392. --- .../currency/src/aggregators/private.json | 3 -- packages/currency/src/currency-utils.ts | 32 +++---------------- packages/currency/src/erc20/networks/index.ts | 2 -- .../currency/src/erc20/networks/private.ts | 11 ------- .../conversion-supported-currencies.test.ts | 4 +-- .../currency/test/currencyManager.test.ts | 8 ++--- 6 files changed, 11 insertions(+), 49 deletions(-) delete mode 100644 packages/currency/src/erc20/networks/private.ts diff --git a/packages/currency/src/aggregators/private.json b/packages/currency/src/aggregators/private.json index 73894d48b6..c1829cf411 100644 --- a/packages/currency/src/aggregators/private.json +++ b/packages/currency/src/aggregators/private.json @@ -16,8 +16,5 @@ }, "0x8acee021a27779d8e98b9650722676b850b25e11": { "0xf5af88e117747e87fc5929f2ff87221b1447652e": 1 - }, - "0x38cf23c52bb4b13f051aec09580a2de845a7fa36": { - "0x775eb53d00dd0acd3ec1696472105d579b9b386b": 1 } } diff --git a/packages/currency/src/currency-utils.ts b/packages/currency/src/currency-utils.ts index c7252deee3..53894d1e83 100644 --- a/packages/currency/src/currency-utils.ts +++ b/packages/currency/src/currency-utils.ts @@ -53,41 +53,19 @@ export const isValidNearAddress = (address: string, network?: string): boolean = export const isNativeCurrency = (currency: CurrencyInput): currency is NativeCurrencyInput => { return ( - isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.BTC) || - isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ETH) + currency.type === RequestLogicTypes.CURRENCY.BTC || + currency.type === RequestLogicTypes.CURRENCY.ETH ); }; export const isISO4217Currency = (currency: CurrencyInput): currency is ISO4217CurrencyInput => { - return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ISO4217); + return currency.type === RequestLogicTypes.CURRENCY.ISO4217; }; export const isERC20Currency = (currency: CurrencyInput): currency is ERC20CurrencyInput => { - return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ERC20); + return currency.type === RequestLogicTypes.CURRENCY.ERC20; }; export const isERC777Currency = (currency: CurrencyInput): currency is ERC777CurrencyInput => { - return isAgreedCurrencyType(currency, RequestLogicTypes.CURRENCY.ERC777); + return currency.type === RequestLogicTypes.CURRENCY.ERC777; }; - -export const isAgreedCurrencyType = ( - currency: Pick, - currencyAgreed: RequestLogicTypes.CURRENCY, -): boolean => currency.type === currencyAgreed; - -// export const isISO4217CurrencyType = (currency: Pick): boolean => -// currency.type === RequestLogicTypes.CURRENCY.ISO4217; - -// export const isAgreedCurrencyType = (currency: Pick, currencyAgreed: RequestLogicTypes.CURRENCY): boolean => -// currency.type === currencyAgreed; - -// export const isERC20Currency = (currency: CurrencyInput): currency is ERC20CurrencyInput => { -// return isERC20CurrencyType(currency); -// }; - -// export const isERC20CurrencyType = (currency: Pick): boolean => -// currency.type === RequestLogicTypes.CURRENCY.ERC20; - -// export const isERC777Currency = (currency: CurrencyInput): currency is ERC777CurrencyInput => { -// return currency.type === RequestLogicTypes.CURRENCY.ERC777; -// }; diff --git a/packages/currency/src/erc20/networks/index.ts b/packages/currency/src/erc20/networks/index.ts index 7bb0469b8f..ee43dbdebe 100644 --- a/packages/currency/src/erc20/networks/index.ts +++ b/packages/currency/src/erc20/networks/index.ts @@ -8,7 +8,6 @@ import { supportedBSCTestERC20 } from './bsctest'; import { supportedBSCERC20 } from './bsc'; import { supportedXDAIERC20 } from './xdai'; import { supportedGoerliERC20 } from './goerli'; -import { supportedPrivateERC20 } from './private'; export const supportedNetworks: Record = { celo: supportedCeloERC20, @@ -21,7 +20,6 @@ export const supportedNetworks: Record = { bsctest: supportedBSCTestERC20, bsc: supportedBSCERC20, xdai: supportedXDAIERC20, - private: supportedPrivateERC20, }; export type { TokenMap }; diff --git a/packages/currency/src/erc20/networks/private.ts b/packages/currency/src/erc20/networks/private.ts deleted file mode 100644 index 0d5d8a3f76..0000000000 --- a/packages/currency/src/erc20/networks/private.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TokenMap } from './types'; - -// List of the supported private ERC20 tokens -export const supportedPrivateERC20: TokenMap = { - // DAI Token on private network. - '0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35': { - name: '(PoS) Dai Stablecoin', - symbol: 'DAI', - decimals: 18, - }, -}; diff --git a/packages/currency/test/conversion-supported-currencies.test.ts b/packages/currency/test/conversion-supported-currencies.test.ts index 863a8b3d98..b8bde61383 100644 --- a/packages/currency/test/conversion-supported-currencies.test.ts +++ b/packages/currency/test/conversion-supported-currencies.test.ts @@ -4,7 +4,7 @@ import { RequestLogicTypes } from '@requestnetwork/types'; const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), { - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa36', + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', decimals: 18, network: 'private', symbol: 'FAKE', @@ -71,7 +71,7 @@ describe('supported currencies with oracles from chainlink', () => { '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063', '0xc2132d05d31c914a87c6611c10748aeb04b58e8f', ], - private: ['0x38cf23c52bb4b13f051aec09580a2de845a7fa36'], + private: ['0x38cf23c52bb4b13f051aec09580a2de845a7fa35'], }).forEach(([network, addresses]) => { describe(network, () => { addresses.forEach((address) => { diff --git a/packages/currency/test/currencyManager.test.ts b/packages/currency/test/currencyManager.test.ts index 112341b935..0512152570 100644 --- a/packages/currency/test/currencyManager.test.ts +++ b/packages/currency/test/currencyManager.test.ts @@ -115,21 +115,21 @@ describe('CurrencyManager', () => { { type: RequestLogicTypes.CURRENCY.ERC20, symbol: 'FAKE', - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa36', + address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', decimals: 18, network: 'private', }, ]); const fake = currencyManager.from('FAKE') as ERC20Currency; // right case - expect(fake.address).toBe('0x38cf23C52bb4B13F051aEc09580A2de845A7Fa36'); + expect(fake.address).toBe('0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35'); // it can match it from right case or wrong - expect(currencyManager.fromAddress('0x38cf23c52bb4b13f051aec09580a2de845a7fa36')?.id).toBe( + expect(currencyManager.fromAddress('0x38cf23c52bb4b13f051aec09580a2de845a7fa35')?.id).toBe( 'FAKE-private', ); - expect(currencyManager.fromAddress('0x38cf23C52bb4B13F051aEc09580A2de845A7Fa36')?.id).toBe( + expect(currencyManager.fromAddress('0x38cF23C52Bb4B13F051Aec09580a2dE845a7FA35')?.id).toBe( 'FAKE-private', ); }); From 1530231e6354c17690fe50707fc4dcb12b7db636 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 25 Oct 2022 12:53:38 +0200 Subject: [PATCH 134/138] update batch payment - currencyManager and version and the associated tests --- .../src/payment/batch-conversion-proxy.ts | 197 ++++++++---------- .../payment-processor/src/payment/index.ts | 6 +- .../payment-processor/src/payment/utils.ts | 19 +- .../payment/any-to-erc20-batch-proxy.test.ts | 170 ++++++++------- 4 files changed, 189 insertions(+), 203 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index b1416f3cbd..39686aabeb 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -23,50 +23,36 @@ import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { BATCH_PAYMENT_NETWORK_ID, RequestDetail } from '@requestnetwork/types/dist/payment-types'; import { IState } from 'types/dist/extension-types'; -import { - CurrencyInput, - isERC20Currency, - isISO4217Currency, - CurrencyManager, -} from '@requestnetwork/currency'; - -const currencyManager = new CurrencyManager([ - ...CurrencyManager.getDefaultList(), - { - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', - decimals: 18, - network: 'private', - symbol: 'DAI', - type: RequestLogicTypes.CURRENCY.ERC20, - }, -]); +import { CurrencyDefinition, CurrencyManager, ICurrencyManager } from '@requestnetwork/currency'; + +const CURRENCY = RequestLogicTypes.CURRENCY; /** * Processes a transaction to pay a batch of requests with an ERC20 currency * that can be 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 EnrichedRequests to pay - * @param version The version of the batch conversion proxy + * @param enrichedRequests List of EnrichedRequests to pay. * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * It can be useful to set it to false if the total amount of the batch is important. * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * @param version The version of the batch conversion proxy. * @param overrides Optionally, override default transaction values, like gas. * @dev We only implement batchPayments using two ERC20 functions: * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. */ export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], - version: string, signerOrProvider: providers.Provider | Signer = getProvider(), skipFeeUSDLimit = false, + version?: string, overrides?: ITransactionOverrides, ): Promise { const { data, to, value } = prepareBatchConversionPaymentTransaction( enrichedRequests, - version, skipFeeUSDLimit, + version, ); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); @@ -76,16 +62,16 @@ export async function payBatchConversionProxyRequest( * Prepares a transaction to pay a batch of requests with an ERC20 currency * that can be different from the request currency (eg. fiat). * It can be used with a Multisig contract. - * @param enrichedRequests List of EnrichedRequests to pay - * @param version The version of the batch conversion proxy + * @param enrichedRequests List of EnrichedRequests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * It can be useful to set it to false if the total amount of the batch is important. * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * @param version The version of the batch conversion proxy. */ export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], - version: string, skipFeeUSDLimit = false, + version?: string, ): IPreparedTransaction { const encodedTx = encodePayBatchConversionRequest(enrichedRequests, skipFeeUSDLimit); const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); @@ -100,18 +86,18 @@ export function prepareBatchConversionPaymentTransaction( * Encodes a transaction to pay a batch of requests with an ERC20 currency * that can be different from the request currency (eg. fiat). * It can be used with a Multisig contract. - * @param enrichedRequests List of EnrichedRequests to pay + * @param enrichedRequests List of EnrichedRequests to pay. * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. * It can be useful to set it to false if the total amount of the batch is important. * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. */ -export function encodePayBatchConversionRequest( +function encodePayBatchConversionRequest( enrichedRequests: EnrichedRequest[], skipFeeUSDLimit = false, ): string { const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); - const network = getPnAndNetwork(enrichedRequests[0].request)[1]; + const { network } = getPnAndNetwork(enrichedRequests[0].request); let firstConversionRequestExtension: IState | undefined; let firstNoConversionRequestExtension: IState | undefined; @@ -120,20 +106,16 @@ export function encodePayBatchConversionRequest( // fill ERC20ConversionRequestDetails and ERC20NoConversionRequestDetails lists for (const enrichedRequest of enrichedRequests) { + const request = enrichedRequest.request; if ( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS ) { firstConversionRequestExtension = - firstConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); - - comparePnTypeAndVersion(firstConversionRequestExtension, enrichedRequest.request); - if ( - !( - isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) || - isISO4217Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput) - ) - ) { + firstConversionRequestExtension ?? getPaymentNetworkExtension(request); + + comparePnTypeAndVersion(firstConversionRequestExtension, request); + if (![CURRENCY.ERC20, CURRENCY.ISO4217].includes(request.currencyInfo.type)) { throw new Error(`wrong request currencyInfo type`); } ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest)); @@ -141,18 +123,16 @@ export function encodePayBatchConversionRequest( enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { firstNoConversionRequestExtension = - firstNoConversionRequestExtension ?? getPaymentNetworkExtension(enrichedRequest.request); + firstNoConversionRequestExtension ?? getPaymentNetworkExtension(request); // isERC20Currency is checked within getBatchArgs function - comparePnTypeAndVersion(firstNoConversionRequestExtension, enrichedRequest.request); - if (!isERC20Currency(enrichedRequest.request.currencyInfo as unknown as CurrencyInput)) { + comparePnTypeAndVersion(firstNoConversionRequestExtension, request); + if (!(request.currencyInfo.type === CURRENCY.ERC20)) { throw new Error(`wrong request currencyInfo type`); } - ERC20NoConversionRequestDetails.push( - getInputERC20NoConversionRequestDetail(enrichedRequest.request), - ); + ERC20NoConversionRequestDetails.push(getInputERC20NoConversionRequestDetail(request)); } - if (network !== getPnAndNetwork(enrichedRequest.request)[1]) + if (network !== getPnAndNetwork(request).network) throw new Error('All the requests must have the same network'); } @@ -173,10 +153,13 @@ export function encodePayBatchConversionRequest( }); } - const pathsToUSD = getPathsToUSD( + const currencyManager = + enrichedRequests[0].paymentSettings.currencyManager ?? CurrencyManager.getDefault(); + const pathsToUSD = getUSDPathsForFeeLimit( [...ERC20ConversionRequestDetails, ...ERC20NoConversionRequestDetails], network, skipFeeUSDLimit, + currencyManager, ); const proxyContract = BatchConversionPayments__factory.createInterface(); @@ -217,12 +200,9 @@ function getInputERC20NoConversionRequestDetail( function getInputERC20ConversionRequestDetail( enrichedRequest: EnrichedRequest, ): PaymentTypes.RequestDetail { - const paymentSettings = enrichedRequest.paymentSettings; - if (!paymentSettings) throw Error('the enrichedRequest has no paymentSettings'); - const { path, requestCurrency } = checkRequestAndGetPathAndCurrency( enrichedRequest.request, - paymentSettings, + enrichedRequest.paymentSettings, ); const { paymentReference, paymentAddress, feeAmount, maxRateTimespan } = getRequestPaymentValues( @@ -241,66 +221,66 @@ function getInputERC20ConversionRequestDetail( path: path, paymentReference: `0x${paymentReference}`, feeAmount: padFeeAmount.toString(), - maxToSpend: paymentSettings.maxToSpend.toString(), + maxToSpend: enrichedRequest.paymentSettings.maxToSpend.toString(), maxRateTimespan: maxRateTimespan || '0', }; } /** * Get the list of conversion paths from tokens to the USD address through currencyManager. + * If there is no path to USD for a token, it goes to the next token. * @param requestDetails List of ERC20 requests to pay. * @param network The network targeted. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. - * It can be useful to set it to false if the total amount of the batch is important. - * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, it skips the path calculation. + * @param currencyManager The currencyManager used to get token conversion paths to USD. */ -function getPathsToUSD( +function getUSDPathsForFeeLimit( requestDetails: RequestDetail[], network: string, skipFeeUSDLimit: boolean, + currencyManager: ICurrencyManager, ): string[][] { - const pathsToUSD: Array> = []; - if (!skipFeeUSDLimit) { - const USDCurrency = currencyManager.fromSymbol('USD'); - - // token's addresses paid with the batch - const tokenAddresses: Array = []; - for (const requestDetail of requestDetails) { - const tokenAddress = requestDetail.path[requestDetail.path.length - 1]; - // Create a list of unique paths: token to USD. - if (USDCurrency && !tokenAddresses.includes(tokenAddress)) { - tokenAddresses.push(tokenAddress); - const tokenCurrency = currencyManager.fromAddress(tokenAddress, network); - const pathToUSD = currencyManager.getConversionPath(tokenCurrency!, USDCurrency, network); - if (pathToUSD) { - pathsToUSD.push(pathToUSD); - } - } - } - } - return pathsToUSD; + if (skipFeeUSDLimit) return []; + + const USDCurrency = currencyManager.fromSymbol('USD'); + if (!USDCurrency) throw 'Cannot find the USD currency information'; + + // get a list of unique token addresses + const tokenAddresses = requestDetails + .map((rd) => rd.path[rd.path.length - 1]) + .filter((value, index, self) => self.indexOf(value) === index); + + // get the token currencies and keep the one that are defined + const tokenCurrencies: Array> = tokenAddresses + .map((token) => currencyManager.fromAddress(token, network)) + .filter((value): value is CurrencyDefinition => !!value); + + // get all the conversion paths to USD when it exists and return it + return tokenCurrencies + .map((t) => currencyManager.getConversionPath(t, USDCurrency, network)) + .filter((value): value is string[] => !!value); } /** - * @param network The network targeted - * @param version The version of the batch conversion proxy + * @param network The network targeted. + * @param version The version of the batch conversion proxy., the last one by default. * @returns */ function getBatchDeploymentInformation( network: string, - version: string, + version?: string, ): { address: string } | null { return { address: batchConversionPaymentsArtifact.getAddress(network, version) }; } /** - * Gets batch conversion contract Address - * @param request The request for an ERC20 payment with/out conversion - * @param version The version of the batch conversion proxy + * Gets batch conversion contract Address. + * @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, - version: string, + version?: string, ): string { return getProxyAddress(request, getBatchDeploymentInformation, version); } @@ -311,35 +291,35 @@ export function getBatchConversionProxyAddress( /** * Processes the approval transaction of the targeted ERC20 with batch conversion proxy. - * @param request The request for an ERC20 payment with/out conversion + * @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 paymentSettings The payment settings are necessary for conversion payment approval. + * @param version The version of the batch conversion proxy., which can be different from request pn version. * @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, + version?: string, overrides?: ITransactionOverrides, ): Promise { if ( !(await hasErc20BatchConversionApproval( request, account, - version, signerOrProvider, paymentSettings, + version, )) ) { return approveErc20BatchConversion( request, - version, getSigner(signerOrProvider), paymentSettings, + version, overrides, ); } @@ -348,18 +328,18 @@ 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 The request for an ERC20 payment with/out conversion + * @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 + * @param paymentSettings The payment settings are necessary for conversion payment approval. + * @param version The version of the batch conversion proxy. */ export async function hasErc20BatchConversionApproval( request: ClientTypes.IRequestData, account: string, - version: string, signerOrProvider: providers.Provider | Signer = getProvider(), paymentSettings?: IConversionPaymentSettings, + version?: string, ): Promise { return checkErc20Allowance( account, @@ -373,24 +353,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 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 request The request for an ERC20 payment with/out conversion. * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param paymentSettings The payment settings are necessary for conversion payment approval. + * @param version The version of the batch conversion proxy., which can be different from request pn version. * @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, + version?: string, overrides?: ITransactionOverrides, ): Promise { const preparedTx = prepareApproveErc20BatchConversion( request, - version, signerOrProvider, paymentSettings, + version, overrides, ); const signer = getSigner(signerOrProvider); @@ -401,24 +381,24 @@ 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 The request for an ERC20 payment with/out conversion - * @param version The version of the batch conversion proxy + * @param request The request for an ERC20 payment with/out conversion. * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param paymentSettings The payment settings are necessary for conversion payment approval. + * @param version The version of the batch conversion proxy. * @param overrides Optionally, override default transaction values, like gas. */ export function prepareApproveErc20BatchConversion( request: ClientTypes.IRequestData, - version: string, signerOrProvider: providers.Provider | Signer = getProvider(), paymentSettings?: IConversionPaymentSettings, + version?: string, overrides?: ITransactionOverrides, ): IPreparedTransaction { const encodedTx = encodeApproveErc20BatchConversion( request, - version, signerOrProvider, paymentSettings, + version, ); return { data: encodedTx, @@ -431,16 +411,16 @@ 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 The request for an ERC20 payment with/out conversion - * @param version The version of the batch conversion proxy + * @param request The request for an ERC20 payment with/out conversion. * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. - * @param paymentSettings The payment settings are necessary for conversion payment approval + * @param paymentSettings The payment settings are necessary for conversion payment approval. + * @param version The version of the batch conversion proxy. */ export function encodeApproveErc20BatchConversion( request: ClientTypes.IRequestData, - version: string, signerOrProvider: providers.Provider | Signer = getProvider(), paymentSettings?: IConversionPaymentSettings, + version?: string, ): string { const proxyAddress = getBatchConversionProxyAddress(request, version); return encodeApproveAnyErc20( @@ -453,12 +433,17 @@ 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 The request for an ERC20 payment with/out conversion + * @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, paymentSettings?: IConversionPaymentSettings, ): string { - return paymentSettings ? paymentSettings.currency!.value : request.currencyInfo.value; + if (paymentSettings) { + if (!paymentSettings.currency) throw 'paymentSetting must have a currency'; + return paymentSettings.currency.value; + } + + return request.currencyInfo.value; } diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index 174ea28921..ba919c78b1 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -329,6 +329,10 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { } }; +// interface mIConversionPaymentSettings extends IConversionPaymentSettings { +// currencyManager: ICurrencyManager; +// } + /** * Input of batch conversion payment processor * It contains requests, paymentSettings, amount and feeAmount. @@ -341,7 +345,7 @@ export interface EnrichedRequest { | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS; request: ClientTypes.IRequestData; - paymentSettings?: IConversionPaymentSettings; + paymentSettings: IConversionPaymentSettings & { currencyManager: ICurrencyManager }; amount?: BigNumberish; feeAmount?: BigNumberish; } diff --git a/packages/payment-processor/src/payment/utils.ts b/packages/payment-processor/src/payment/utils.ts index 7e315803d7..452f473559 100644 --- a/packages/payment-processor/src/payment/utils.ts +++ b/packages/payment-processor/src/payment/utils.ts @@ -141,16 +141,17 @@ export const getProxyNetwork = ( /** * @param request The request to pay - * @return A list that contains the payment network extension and the currency information + * @return An object that contains the payment network extension and the currency information */ -export function getPnAndNetwork( - request: ClientTypes.IRequestData, -): [ExtensionTypes.IState, string] { +export function getPnAndNetwork(request: ClientTypes.IRequestData): { + paymentNetwork: ExtensionTypes.IState; + network: string; +} { const pn = getPaymentNetworkExtension(request); if (!pn) { throw new Error('PaymentNetwork not found'); } - return [pn, getProxyNetwork(pn, request.currencyInfo)]; + return { paymentNetwork: pn, network: getProxyNetwork(pn, request.currencyInfo) }; } /** @@ -163,10 +164,12 @@ export const getProxyAddress = ( getDeploymentInformation: (network: string, version: string) => { address: string } | null, version?: string, ): string => { - const [pn, network] = getPnAndNetwork(request); - const deploymentInfo = getDeploymentInformation(network, version || pn.version); + const { paymentNetwork, network } = getPnAndNetwork(request); + const deploymentInfo = getDeploymentInformation(network, version || paymentNetwork.version); if (!deploymentInfo) { - throw new Error(`No deployment found for network ${network}, version ${version || pn.version}`); + throw new Error( + `No deployment found for network ${network}, version ${version || paymentNetwork.version}`, + ); } return deploymentInfo.address; }; 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 5c4934a005..e0d454ec3a 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 @@ -11,15 +11,15 @@ import { getErc20Balance } from '../../src/payment/erc20'; import Utils from '@requestnetwork/utils'; import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payment/utils'; import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; +import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; +import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; +import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; import { approveErc20BatchConversionIfNeeded, getBatchConversionProxyAddress, payBatchConversionProxyRequest, prepareBatchConversionPaymentTransaction, } from '../../src/payment/batch-conversion-proxy'; -import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; -import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; -import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; const currencyManager = new CurrencyManager([ ...CurrencyManager.getDefaultList(), @@ -40,7 +40,6 @@ 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'; @@ -50,14 +49,22 @@ const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); // Cf. ERC20Alpha in TestERC20.sol -const alphaPaymentSettings: IConversionPaymentSettings = { +const alphaPaymentSettings: IConversionPaymentSettings & { currencyManager: CurrencyManager } = { currency: { type: RequestLogicTypes.CURRENCY.ERC20, value: DAITokenAddress, network: 'private', }, maxToSpend: '10000000000000000000000000000', - currencyManager, + currencyManager: currencyManager, +}; + +const noConvesionPaymentSettings: IConversionPaymentSettings & { + currencyManager: CurrencyManager; +} = { + currency: undefined, + maxToSpend: '0', + currencyManager: currencyManager, }; // requests setting @@ -183,12 +190,12 @@ describe('erc20-batch-conversion-proxy', () => { beforeAll(async () => { // Revoke DAI and FAU approvals await revokeErc20Approval( - getBatchConversionProxyAddress(DAIValidRequest, batchConvVersion), + getBatchConversionProxyAddress(DAIValidRequest), DAITokenAddress, wallet, ); await revokeErc20Approval( - getBatchConversionProxyAddress(FAUValidRequest, batchConvVersion), + getBatchConversionProxyAddress(FAUValidRequest), FAUTokenAddress, wallet, ); @@ -197,7 +204,6 @@ describe('erc20-batch-conversion-proxy', () => { const approvalTx = await approveErc20BatchConversionIfNeeded( EURValidRequest, wallet.address, - batchConvVersion, wallet.provider, alphaPaymentSettings, ); @@ -239,10 +245,9 @@ describe('erc20-batch-conversion-proxy', () => { ...alphaPaymentSettings.currency, value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', }, - } as IConversionPaymentSettings, + } as IConversionPaymentSettings & { currencyManager: CurrencyManager }, }, ], - batchConvVersion, wallet, ), ).rejects.toThrowError( @@ -252,26 +257,25 @@ describe('erc20-batch-conversion-proxy', () => { }), ); }); - it('should throw an error if request has no paymentSettings', async () => { + it('should throw an error if request has no currency within paymentSettings', async () => { await expect( payBatchConversionProxyRequest( [ { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, request: EURRequest, - paymentSettings: undefined, + paymentSettings: noConvesionPaymentSettings, }, ], - batchConvVersion, wallet, ), - ).rejects.toThrowError('the enrichedRequest has no paymentSettings'); + ).rejects.toThrowError('currency must be provided in the 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`); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + `wrong request currencyInfo type`, + ); }); it('should throw an error if the request has a wrong network', async () => { EURRequest.extensions = { @@ -291,9 +295,9 @@ describe('erc20-batch-conversion-proxy', () => { }, }; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('All the requests must have the same network'); + await expect(payBatchConversionProxyRequest(enrichedRequests, 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 = { @@ -312,23 +316,21 @@ describe('erc20-batch-conversion-proxy', () => { }, }; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( + await expect(payBatchConversionProxyRequest(enrichedRequests, 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"); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + "The currency '' is unknown or not supported", + ); }); - it('should throw an error if request has no extension', async () => { + it('should throw an error if a request has no extension', async () => { EURRequest.extensions = [] as any; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('no payment network found'); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + 'no payment network found', + ); }); it('should throw an error if there is a wrong version mapping', async () => { EURRequest.extensions = { @@ -337,9 +339,9 @@ describe('erc20-batch-conversion-proxy', () => { version: '0.3.0', }, }; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('Every payment network type and version must be identical'); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + 'Every payment network type and version must be identical', + ); }); }); @@ -356,9 +358,9 @@ describe('erc20-batch-conversion-proxy', () => { paymentSettings: alphaPaymentSettings, }, ], - batchConvVersion, wallet, true, + undefined, { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ @@ -390,7 +392,6 @@ describe('erc20-batch-conversion-proxy', () => { paymentSettings: alphaPaymentSettings, }, ], - batchConvVersion, wallet, skipFeeUSDLimit === 'true', ); @@ -441,7 +442,6 @@ describe('erc20-batch-conversion-proxy', () => { request: EURValidRequest, paymentSettings: alphaPaymentSettings, }), - batchConvVersion, wallet, true, ); @@ -494,10 +494,9 @@ describe('erc20-batch-conversion-proxy', () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: DAIValidRequest, - paymentSettings: undefined, + paymentSettings: noConvesionPaymentSettings, }, ], - batchConvVersion, wallet, true, ); @@ -541,10 +540,12 @@ describe('erc20-batch-conversion-proxy', () => { { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: DAIValidRequest, + paymentSettings: noConvesionPaymentSettings, }, { paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, request: FAURequest, + paymentSettings: noConvesionPaymentSettings, }, ]; }); @@ -552,34 +553,30 @@ describe('erc20-batch-conversion-proxy', () => { describe('Throw an error', () => { it('should throw an error if the request is not erc20', async () => { FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('wrong request currencyInfo type'); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + 'wrong request currencyInfo type', + ); }); it("should throw an error if one request's currencyInfo has no value", async () => { FAURequest.currencyInfo.value = ''; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( + await expect(payBatchConversionProxyRequest(enrichedRequests, 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 () => { FAURequest.currencyInfo.network = ''; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError( + await expect(payBatchConversionProxyRequest(enrichedRequests, 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 () => { FAURequest.extensions = [] as any; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('no payment network found'); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + 'no payment network found', + ); }); it('should throw an error if there is a wrong version mapping', async () => { @@ -589,9 +586,9 @@ describe('erc20-batch-conversion-proxy', () => { version: '0.3.0', }, }; - await expect( - payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet), - ).rejects.toThrowError('Every payment network type and version must be identical'); + await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + 'Every payment network type and version must be identical', + ); }); }); @@ -600,7 +597,7 @@ describe('erc20-batch-conversion-proxy', () => { const spy = jest.fn(); const originalSendTransaction = wallet.sendTransaction.bind(wallet); wallet.sendTransaction = spy; - await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet, true, { + await payBatchConversionProxyRequest(enrichedRequests, wallet, true, undefined, { gasPrice: '20000000000', }); expect(spy).toHaveBeenCalledWith({ @@ -616,7 +613,6 @@ describe('erc20-batch-conversion-proxy', () => { const FAUApprovalTx = await approveErc20BatchConversionIfNeeded( FAUValidRequest, wallet.address, - batchConvVersion, wallet, ); if (FAUApprovalTx) await FAUApprovalTx.wait(1); @@ -624,7 +620,6 @@ describe('erc20-batch-conversion-proxy', () => { const DAIApprovalTx = await approveErc20BatchConversionIfNeeded( DAIValidRequest, wallet.address, - batchConvVersion, wallet, ); if (DAIApprovalTx) await DAIApprovalTx.wait(1); @@ -646,7 +641,7 @@ describe('erc20-batch-conversion-proxy', () => { const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); // Batch payment - const tx = await payBatchConversionProxyRequest(enrichedRequests, batchConvVersion, wallet); + const tx = await payBatchConversionProxyRequest(enrichedRequests, wallet); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); @@ -691,38 +686,37 @@ describe('erc20-batch-conversion-proxy', () => { describe('prepareBatchPaymentTransaction', () => { it('should consider the version mapping', () => { expect( - prepareBatchConversionPaymentTransaction( - [ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - request: { - ...DAIValidRequest, - extensions: { - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...DAIValidRequest.extensions[ - PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - ], - version: '0.1.0', - }, + prepareBatchConversionPaymentTransaction([ + { + paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + request: { + ...DAIValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...DAIValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', }, - } as any, - } as EnrichedRequest, - { - request: { - ...FAUValidRequest, - extensions: { - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...FAUValidRequest.extensions[ - PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - ], - version: '0.1.0', - }, + }, + } as any, + paymentSettings: noConvesionPaymentSettings, + } as EnrichedRequest, + { + request: { + ...FAUValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...FAUValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', }, - } as any, - } as EnrichedRequest, - ], - batchConvVersion, - ).to, + }, + } as any, + paymentSettings: noConvesionPaymentSettings, + } as unknown as EnrichedRequest, + ]).to, ).toBe(batchConversionPaymentsArtifact.getAddress('private', '0.1.0')); }); }); From ecb2c5836e8c3d3f0ef323e9554a808a9783e30c Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Tue, 25 Oct 2022 13:11:25 +0200 Subject: [PATCH 135/138] convention and typo corrections --- .../src/payment/batch-conversion-proxy.ts | 12 ++++++------ .../lib/artifacts/BatchConversionPayments/index.ts | 2 +- 2 files changed, 7 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 39686aabeb..91ede3ba43 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -21,7 +21,6 @@ import { IPreparedTransaction } from './prepared-transaction'; import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; -import { BATCH_PAYMENT_NETWORK_ID, RequestDetail } from '@requestnetwork/types/dist/payment-types'; import { IState } from 'types/dist/extension-types'; import { CurrencyDefinition, CurrencyManager, ICurrencyManager } from '@requestnetwork/currency'; @@ -109,7 +108,7 @@ function encodePayBatchConversionRequest( const request = enrichedRequest.request; if ( enrichedRequest.paymentNetworkId === - BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS + PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS ) { firstConversionRequestExtension = firstConversionRequestExtension ?? getPaymentNetworkExtension(request); @@ -120,7 +119,8 @@ function encodePayBatchConversionRequest( } ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest)); } else if ( - enrichedRequest.paymentNetworkId === BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS + enrichedRequest.paymentNetworkId === + PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS ) { firstNoConversionRequestExtension = firstNoConversionRequestExtension ?? getPaymentNetworkExtension(request); @@ -140,7 +140,7 @@ function encodePayBatchConversionRequest( if (ERC20ConversionRequestDetails.length > 0) { // Add ERC20 conversion payments metaDetails.push({ - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, requestDetails: ERC20ConversionRequestDetails, }); } @@ -148,7 +148,7 @@ function encodePayBatchConversionRequest( if (ERC20NoConversionRequestDetails.length > 0) { // Add multi ERC20 no-conversion payments metaDetails.push({ - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + paymentNetworkId: PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, requestDetails: ERC20NoConversionRequestDetails, }); } @@ -235,7 +235,7 @@ function getInputERC20ConversionRequestDetail( * @param currencyManager The currencyManager used to get token conversion paths to USD. */ function getUSDPathsForFeeLimit( - requestDetails: RequestDetail[], + requestDetails: PaymentTypes.RequestDetail[], network: string, skipFeeUSDLimit: boolean, currencyManager: ICurrencyManager, diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 8e24a943a7..8d67a47938 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -50,7 +50,7 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Wed, 26 Oct 2022 15:24:13 +0200 Subject: [PATCH 136/138] refacto batch pay inputs - no more batch type and an options with currencyManager and skip fee --- .../src/payment/batch-conversion-proxy.ts | 63 +++-- .../payment-processor/src/payment/index.ts | 13 +- .../payment-processor/src/payment/settings.ts | 11 + .../payment/any-to-erc20-batch-proxy.test.ts | 242 ++++++++++-------- 4 files changed, 175 insertions(+), 154 deletions(-) diff --git a/packages/payment-processor/src/payment/batch-conversion-proxy.ts b/packages/payment-processor/src/payment/batch-conversion-proxy.ts index 91ede3ba43..01991fcc9f 100644 --- a/packages/payment-processor/src/payment/batch-conversion-proxy.ts +++ b/packages/payment-processor/src/payment/batch-conversion-proxy.ts @@ -22,7 +22,8 @@ import { EnrichedRequest, IConversionPaymentSettings } from './index'; import { checkRequestAndGetPathAndCurrency } from './any-to-erc20-proxy'; import { checkErc20Allowance, encodeApproveAnyErc20 } from './erc20'; import { IState } from 'types/dist/extension-types'; -import { CurrencyDefinition, CurrencyManager, ICurrencyManager } from '@requestnetwork/currency'; +import { CurrencyDefinition, ICurrencyManager } from '@requestnetwork/currency'; +import { IConversionSettings, IRequestPaymentOptions } from './settings'; const CURRENCY = RequestLogicTypes.CURRENCY; @@ -33,10 +34,11 @@ const CURRENCY = RequestLogicTypes.CURRENCY; * It can be used with a Multisig contract * @param enrichedRequests List of EnrichedRequests to pay. * @param signerOrProvider The Web3 provider, or signer. Defaults to window.ethereum. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. - * It can be useful to set it to false if the total amount of the batch is important. - * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. - * @param version The version of the batch conversion proxy. + * @param options It contains 3 paramters required to do a batch payments: + * - conversion: It must contains the currencyManager. + * - skipFeeUSDLimit: Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * Setting the value to true skips the USD fee limit, and reduces gas consumption. + * - version: The version of the batch conversion proxy. * @param overrides Optionally, override default transaction values, like gas. * @dev We only implement batchPayments using two ERC20 functions: * batchMultiERC20ConversionPayments, and batchMultiERC20Payments. @@ -44,15 +46,10 @@ const CURRENCY = RequestLogicTypes.CURRENCY; export async function payBatchConversionProxyRequest( enrichedRequests: EnrichedRequest[], signerOrProvider: providers.Provider | Signer = getProvider(), - skipFeeUSDLimit = false, - version?: string, + options: IRequestPaymentOptions, overrides?: ITransactionOverrides, ): Promise { - const { data, to, value } = prepareBatchConversionPaymentTransaction( - enrichedRequests, - skipFeeUSDLimit, - version, - ); + const { data, to, value } = prepareBatchConversionPaymentTransaction(enrichedRequests, options); const signer = getSigner(signerOrProvider); return signer.sendTransaction({ data, to, value, ...overrides }); } @@ -62,18 +59,22 @@ export async function payBatchConversionProxyRequest( * that can be different from the request currency (eg. fiat). * It can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequests to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. - * It can be useful to set it to false if the total amount of the batch is important. - * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. - * @param version The version of the batch conversion proxy. + * @param options It contains 3 paramters required to prepare a batch payments: + * - conversion: It must contains the currencyManager. + * - skipFeeUSDLimit: Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * Setting the value to true skips the USD fee limit, and reduces gas consumption. + * - version: The version of the batch conversion proxy. */ export function prepareBatchConversionPaymentTransaction( enrichedRequests: EnrichedRequest[], - skipFeeUSDLimit = false, - version?: string, + options: IRequestPaymentOptions, ): IPreparedTransaction { - const encodedTx = encodePayBatchConversionRequest(enrichedRequests, skipFeeUSDLimit); - const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, version); + const encodedTx = encodePayBatchConversionRequest( + enrichedRequests, + options.skipFeeUSDLimit, + options.conversion, + ); + const proxyAddress = getBatchConversionProxyAddress(enrichedRequests[0].request, options.version); return { data: encodedTx, to: proxyAddress, @@ -86,14 +87,17 @@ export function prepareBatchConversionPaymentTransaction( * that can be different from the request currency (eg. fiat). * It can be used with a Multisig contract. * @param enrichedRequests List of EnrichedRequests to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. - * It can be useful to set it to false if the total amount of the batch is important. - * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * @param skipFeeUSDLimit Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * Setting the value to true skips the USD fee limit, and reduces gas consumption. */ function encodePayBatchConversionRequest( enrichedRequests: EnrichedRequest[], skipFeeUSDLimit = false, + conversion: IConversionSettings | undefined, ): string { + if (!(conversion && conversion.currencyManager)) { + throw 'the conversion object or the currencyManager is undefined'; + } const { feeAddress } = getRequestPaymentValues(enrichedRequests[0].request); const { network } = getPnAndNetwork(enrichedRequests[0].request); @@ -106,10 +110,8 @@ function encodePayBatchConversionRequest( // fill ERC20ConversionRequestDetails and ERC20NoConversionRequestDetails lists for (const enrichedRequest of enrichedRequests) { const request = enrichedRequest.request; - if ( - enrichedRequest.paymentNetworkId === - PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS - ) { + if (enrichedRequest.paymentNetworkId === PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY) { + enrichedRequest.paymentSettings.currencyManager = conversion.currencyManager; firstConversionRequestExtension = firstConversionRequestExtension ?? getPaymentNetworkExtension(request); @@ -119,8 +121,7 @@ function encodePayBatchConversionRequest( } ERC20ConversionRequestDetails.push(getInputERC20ConversionRequestDetail(enrichedRequest)); } else if ( - enrichedRequest.paymentNetworkId === - PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS + enrichedRequest.paymentNetworkId === PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT ) { firstNoConversionRequestExtension = firstNoConversionRequestExtension ?? getPaymentNetworkExtension(request); @@ -153,13 +154,11 @@ function encodePayBatchConversionRequest( }); } - const currencyManager = - enrichedRequests[0].paymentSettings.currencyManager ?? CurrencyManager.getDefault(); const pathsToUSD = getUSDPathsForFeeLimit( [...ERC20ConversionRequestDetails, ...ERC20NoConversionRequestDetails], network, skipFeeUSDLimit, - currencyManager, + conversion.currencyManager, ); const proxyContract = BatchConversionPayments__factory.createInterface(); diff --git a/packages/payment-processor/src/payment/index.ts b/packages/payment-processor/src/payment/index.ts index ba919c78b1..8629f78e15 100644 --- a/packages/payment-processor/src/payment/index.ts +++ b/packages/payment-processor/src/payment/index.ts @@ -329,23 +329,18 @@ const throwIfNotWeb3 = (request: ClientTypes.IRequestData) => { } }; -// interface mIConversionPaymentSettings extends IConversionPaymentSettings { -// currencyManager: ICurrencyManager; -// } - /** * Input of batch conversion payment processor * It contains requests, paymentSettings, amount and feeAmount. * Currently, these requests must have the same PN, version, and batchFee - * @dev next step: paymentNetworkId could get more values options, see the "ref" - * in batchConversionPayment.sol + * @dev next step: paymentNetworkId could get more values options to pay Native tokens. */ export interface EnrichedRequest { paymentNetworkId: - | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS - | PaymentTypes.BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS; + | PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY + | PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT; request: ClientTypes.IRequestData; - paymentSettings: IConversionPaymentSettings & { currencyManager: ICurrencyManager }; + paymentSettings: IConversionPaymentSettings; amount?: BigNumberish; feeAmount?: BigNumberish; } diff --git a/packages/payment-processor/src/payment/settings.ts b/packages/payment-processor/src/payment/settings.ts index a7014d2349..f50589b3de 100644 --- a/packages/payment-processor/src/payment/settings.ts +++ b/packages/payment-processor/src/payment/settings.ts @@ -52,4 +52,15 @@ export interface IRequestPaymentOptions { conversion?: IConversionSettings; /** Optional, enable to approve only specific amount of token. Defaults to MAX_ALLOWANCE if not set */ approval?: IApprovalSettings; + + /** Optional, specifies if escrow is being used */ + isEscrow?: boolean; + + /** Used, and required, only for batch payment. + * Check the value of batchFeeAmountUSDLimit of the batch proxy deployed. + * Setting the value to true skips the USD fee limit, and reduces gas consumption. + */ + skipFeeUSDLimit?: boolean; + /** Optional, only for batch payment to define the proxy to use. */ + version?: string; } 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 e0d454ec3a..0666fe5bff 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 @@ -13,24 +13,13 @@ import { revokeErc20Approval } from '@requestnetwork/payment-processor/src/payme import { EnrichedRequest, IConversionPaymentSettings } from '../../src/index'; import { batchConversionPaymentsArtifact } from '@requestnetwork/smart-contracts'; import { CurrencyManager, UnsupportedCurrencyError } from '@requestnetwork/currency'; -import { BATCH_PAYMENT_NETWORK_ID } from '@requestnetwork/types/dist/payment-types'; import { approveErc20BatchConversionIfNeeded, getBatchConversionProxyAddress, payBatchConversionProxyRequest, prepareBatchConversionPaymentTransaction, } from '../../src/payment/batch-conversion-proxy'; - -const currencyManager = new CurrencyManager([ - ...CurrencyManager.getDefaultList(), - { - address: '0x38cf23c52bb4b13f051aec09580a2de845a7fa35', - decimals: 18, - network: 'private', - symbol: 'DAI', - type: RequestLogicTypes.CURRENCY.ERC20, - }, -]); +import { IRequestPaymentOptions } from 'payment-processor/src/payment/settings'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -48,23 +37,38 @@ const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const wallet = Wallet.fromMnemonic(mnemonic).connect(provider); -// Cf. ERC20Alpha in TestERC20.sol -const alphaPaymentSettings: IConversionPaymentSettings & { currencyManager: CurrencyManager } = { - currency: { - type: RequestLogicTypes.CURRENCY.ERC20, - value: DAITokenAddress, +const currencyManager = new CurrencyManager([ + ...CurrencyManager.getDefaultList(), + { + address: DAITokenAddress, + decimals: 18, network: 'private', + symbol: 'DAI', + type: RequestLogicTypes.CURRENCY.ERC20, }, +]); + +// Cf. ERC20Alpha in TestERC20.sol +const currency: RequestLogicTypes.ICurrency = { + type: RequestLogicTypes.CURRENCY.ERC20, + value: DAITokenAddress, + network: 'private', +}; +const paymentSettings: IConversionPaymentSettings = { + currency: currency, maxToSpend: '10000000000000000000000000000', currencyManager: currencyManager, }; -const noConvesionPaymentSettings: IConversionPaymentSettings & { - currencyManager: CurrencyManager; -} = { - currency: undefined, - maxToSpend: '0', - currencyManager: currencyManager, +const conversionPaymentSettings = Utils.deepCopy(paymentSettings); +// conversionPaymentSettings.currencyManager = undefined; + +const options: IRequestPaymentOptions = { + conversion: { + currency: currency, + currencyManager: currencyManager, + }, + skipFeeUSDLimit: true, }; // requests setting @@ -205,7 +209,7 @@ describe('erc20-batch-conversion-proxy', () => { EURValidRequest, wallet.address, wallet.provider, - alphaPaymentSettings, + conversionPaymentSettings, ); expect(approvalTx).toBeDefined(); if (approvalTx) { @@ -219,14 +223,14 @@ describe('erc20-batch-conversion-proxy', () => { EURRequest = Utils.deepCopy(EURValidRequest); enrichedRequests = [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, ]; }); @@ -237,18 +241,19 @@ describe('erc20-batch-conversion-proxy', () => { payBatchConversionProxyRequest( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, paymentSettings: { - ...alphaPaymentSettings, + ...conversionPaymentSettings, currency: { - ...alphaPaymentSettings.currency, + ...conversionPaymentSettings.currency, value: '0x775eb53d00dd0acd3ec1696472105d579b9b386b', }, - } as IConversionPaymentSettings & { currencyManager: CurrencyManager }, + } as IConversionPaymentSettings, }, ], wallet, + options, ), ).rejects.toThrowError( new UnsupportedCurrencyError({ @@ -258,24 +263,27 @@ describe('erc20-batch-conversion-proxy', () => { ); }); it('should throw an error if request has no currency within paymentSettings', async () => { + const wrongPaymentSettings = Utils.deepCopy(conversionPaymentSettings); + wrongPaymentSettings.currency = undefined; await expect( payBatchConversionProxyRequest( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURRequest, - paymentSettings: noConvesionPaymentSettings, + paymentSettings: wrongPaymentSettings, }, ], wallet, + options, ), ).rejects.toThrowError('currency must be provided in the paymentSettings'); }); it('should throw an error if the request is ETH', async () => { EURRequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - `wrong request currencyInfo type`, - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError(`wrong request currencyInfo type`); }); it('should throw an error if the request has a wrong network', async () => { EURRequest.extensions = { @@ -295,9 +303,9 @@ describe('erc20-batch-conversion-proxy', () => { }, }; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'All the requests must have the same network', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).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 = { @@ -316,21 +324,23 @@ describe('erc20-batch-conversion-proxy', () => { }, }; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).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, wallet)).rejects.toThrowError( - "The currency '' is unknown or not supported", - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError("The currency '' is unknown or not supported"); }); it('should throw an error if a request has no extension', async () => { EURRequest.extensions = [] as any; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'no payment network found', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError('no payment network found'); }); it('should throw an error if there is a wrong version mapping', async () => { EURRequest.extensions = { @@ -339,9 +349,9 @@ describe('erc20-batch-conversion-proxy', () => { version: '0.3.0', }, }; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'Every payment network type and version must be identical', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -353,14 +363,13 @@ describe('erc20-batch-conversion-proxy', () => { await payBatchConversionProxyRequest( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, ], wallet, - true, - undefined, + options, { gasPrice: '20000000000' }, ); expect(spy).toHaveBeenCalledWith({ @@ -383,18 +392,20 @@ describe('erc20-batch-conversion-proxy', () => { provider, ); + options.skipFeeUSDLimit = skipFeeUSDLimit === 'true'; // Convert and pay const tx = await payBatchConversionProxyRequest( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, ], wallet, - skipFeeUSDLimit === 'true', + options, ); + options.skipFeeUSDLimit = true; const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); expect(tx.hash).toBeDefined(); @@ -438,12 +449,12 @@ describe('erc20-batch-conversion-proxy', () => { // Convert and pay const tx = await payBatchConversionProxyRequest( Array(2).fill({ - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }), wallet, - true, + options, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -482,23 +493,23 @@ describe('erc20-batch-conversion-proxy', () => { const tx = await payBatchConversionProxyRequest( [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_CONVERSION_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ANY_TO_ERC20_PROXY, request: EURValidRequest, - paymentSettings: alphaPaymentSettings, + paymentSettings: conversionPaymentSettings, }, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, request: DAIValidRequest, - paymentSettings: noConvesionPaymentSettings, + paymentSettings: { maxToSpend: '0' }, }, ], wallet, - true, + options, ); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toEqual(1); @@ -538,14 +549,14 @@ describe('erc20-batch-conversion-proxy', () => { FAURequest = Utils.deepCopy(FAUValidRequest); enrichedRequests = [ { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, request: DAIValidRequest, - paymentSettings: noConvesionPaymentSettings, + paymentSettings: { maxToSpend: '0' }, }, { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, request: FAURequest, - paymentSettings: noConvesionPaymentSettings, + paymentSettings: { maxToSpend: '0' }, }, ]; }); @@ -553,30 +564,34 @@ describe('erc20-batch-conversion-proxy', () => { describe('Throw an error', () => { it('should throw an error if the request is not erc20', async () => { FAURequest.currencyInfo.type = RequestLogicTypes.CURRENCY.ETH; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'wrong request currencyInfo type', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError('wrong request currencyInfo type'); }); it("should throw an error if one request's currencyInfo has no value", async () => { FAURequest.currencyInfo.value = ''; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).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 () => { FAURequest.currencyInfo.network = ''; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).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 () => { FAURequest.extensions = [] as any; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'no payment network found', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError('no payment network found'); }); it('should throw an error if there is a wrong version mapping', async () => { @@ -586,9 +601,9 @@ describe('erc20-batch-conversion-proxy', () => { version: '0.3.0', }, }; - await expect(payBatchConversionProxyRequest(enrichedRequests, wallet)).rejects.toThrowError( - 'Every payment network type and version must be identical', - ); + await expect( + payBatchConversionProxyRequest(enrichedRequests, wallet, options), + ).rejects.toThrowError('Every payment network type and version must be identical'); }); }); @@ -597,7 +612,7 @@ describe('erc20-batch-conversion-proxy', () => { const spy = jest.fn(); const originalSendTransaction = wallet.sendTransaction.bind(wallet); wallet.sendTransaction = spy; - await payBatchConversionProxyRequest(enrichedRequests, wallet, true, undefined, { + await payBatchConversionProxyRequest(enrichedRequests, wallet, options, { gasPrice: '20000000000', }); expect(spy).toHaveBeenCalledWith({ @@ -641,7 +656,7 @@ describe('erc20-batch-conversion-proxy', () => { const initialFAUFeeBalance = await getErc20Balance(FAUValidRequest, feeAddress, provider); // Batch payment - const tx = await payBatchConversionProxyRequest(enrichedRequests, wallet); + const tx = await payBatchConversionProxyRequest(enrichedRequests, wallet, options); const confirmedTx = await tx.wait(1); expect(confirmedTx.status).toBe(1); expect(tx.hash).not.toBeUndefined(); @@ -686,37 +701,38 @@ describe('erc20-batch-conversion-proxy', () => { describe('prepareBatchPaymentTransaction', () => { it('should consider the version mapping', () => { expect( - prepareBatchConversionPaymentTransaction([ - { - paymentNetworkId: BATCH_PAYMENT_NETWORK_ID.BATCH_MULTI_ERC20_PAYMENTS, - request: { - ...DAIValidRequest, - extensions: { - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...DAIValidRequest.extensions[ - PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - ], - version: '0.1.0', + prepareBatchConversionPaymentTransaction( + [ + { + paymentNetworkId: PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT, + request: { + ...DAIValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...DAIValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, }, - }, - } as any, - paymentSettings: noConvesionPaymentSettings, - } as EnrichedRequest, - { - request: { - ...FAUValidRequest, - extensions: { - [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { - ...FAUValidRequest.extensions[ - PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT - ], - version: '0.1.0', + } as any, + } as EnrichedRequest, + { + request: { + ...FAUValidRequest, + extensions: { + [PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT]: { + ...FAUValidRequest.extensions[ + PaymentTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT + ], + version: '0.1.0', + }, }, - }, - } as any, - paymentSettings: noConvesionPaymentSettings, - } as unknown as EnrichedRequest, - ]).to, + } as any, + } as unknown as EnrichedRequest, + ], + options, + ).to, ).toBe(batchConversionPaymentsArtifact.getAddress('private', '0.1.0')); }); }); From 5b826c55bcdcbff1c1f58642b568019075027aa0 Mon Sep 17 00:00:00 2001 From: olivier7delf <55892112+olivier7delf@users.noreply.github.com> Date: Wed, 26 Oct 2022 19:49:42 +0200 Subject: [PATCH 137/138] can deploy and setup batch contract in one cmd line --- packages/smart-contracts/hardhat.config.ts | 4 +- .../setupBatchConversionPayments.ts | 128 +++++++++--------- .../src/contracts/BatchConversionPayments.sol | 4 +- .../contracts/BatchNoConversionPayments.sol | 4 +- .../BatchConversionPayments/index.ts | 42 +++--- 5 files changed, 93 insertions(+), 89 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index da8a7d7039..fc0dcc5919 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -162,7 +162,9 @@ export default { xdeploy: { salt: REQUEST_SALT, signer: process.env.ADMIN_PRIVATE_KEY, - networks: process.env.NETWORK ? [process.env.NETWORK] : ['mainnet'], //['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], + networks: process.env.NETWORK + ? [process.env.NETWORK] + : ['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts index bcd7e5d219..b8376df5c4 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/setupBatchConversionPayments.ts @@ -25,69 +25,71 @@ export const setupBatchConversionPayments = async ( ); // constants related to chainlink and conversion rate const currencyManager = CurrencyManager.getDefault(); - await Promise.all( - hre.config.xdeploy.networks.map(async (network) => { - const NativeAddress = currencyManager.getNativeCurrency( - RequestLogicTypes.CURRENCY.ETH, - network, - )!.hash; - const USDAddress = currencyManager.fromSymbol('USD')!.hash; - console.log(`Setup BatchConversionPayments on ${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 = batchConversionPaymentContract.connect(signer); - const gasPrice = await provider.getGasPrice(); + for (const network of hre.config.xdeploy.networks) { + await Promise.all( + [network].map(async (network) => { + const NativeAddress = currencyManager.getNativeCurrency( + RequestLogicTypes.CURRENCY.ETH, + network, + )!.hash; + const USDAddress = currencyManager.fromSymbol('USD')!.hash; + console.log(`Setup BatchConversionPayments on ${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 = batchConversionPaymentContract.connect(signer); + const gasPrice = await provider.getGasPrice(); - // start from the adminNonce, increase gasPrice if needed - const gasCoef = 2; - await updateBatchPaymentFees(batchConversionPaymentConnected, gasPrice.mul(gasCoef)); - await updateBatchPaymentFeeAmountUSDLimit( - batchConversionPaymentConnected, - gasPrice.mul(gasCoef), - ); - await updateBatchConversionProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'erc20', - ); - await updateBatchConversionProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'native', - ); - await updateBatchConversionProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'erc20Conversion', - ); - await updateBatchConversionProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'nativeConversion', - ); - await updateBatchConversionProxy( - batchConversionPaymentConnected, - network, - gasPrice.mul(gasCoef), - 'chainlinkConversionPath', - ); - await updateNativeAndUSDAddress( - batchConversionPaymentConnected, - NativeAddress, - USDAddress, - gasPrice.mul(gasCoef), - ); - }), - ); + // start from the adminNonce, increase gasPrice if needed + const gasCoef = 3; + await updateBatchPaymentFees(batchConversionPaymentConnected, gasPrice.mul(gasCoef)); + await updateBatchPaymentFeeAmountUSDLimit( + batchConversionPaymentConnected, + gasPrice.mul(gasCoef), + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'erc20', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'native', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'erc20Conversion', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'nativeConversion', + ); + await updateBatchConversionProxy( + batchConversionPaymentConnected, + network, + gasPrice.mul(gasCoef), + 'chainlinkConversionPath', + ); + await updateNativeAndUSDAddress( + batchConversionPaymentConnected, + NativeAddress, + USDAddress, + gasPrice.mul(gasCoef), + ); + }), + ); + } console.log('Setup for setupBatchConversionPayment successfull'); }; diff --git a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol index 7fec3f829a..a207c7b335 100644 --- a/packages/smart-contracts/src/contracts/BatchConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchConversionPayments.sol @@ -169,7 +169,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @notice Send a batch of Native conversion payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch is reverted. * @param requestDetails List of native requests denominated in fiat to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduces gas consumption. * @param feeAddress The fee recipient. * @dev It uses NativeConversionProxy (EthereumConversionProxy) to pay an invoice and fees. * Please: @@ -264,7 +264,7 @@ contract BatchConversionPayments is BatchNoConversionPayments { * @notice Send a batch of Native conversion payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch is reverted. * @param requestDetails List of native requests denominated in fiat to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduces gas consumption. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses NativeConversionProxy (EthereumConversionProxy) to pay an invoice and fees. diff --git a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol index f0ed24a2c2..4ef0eb04e0 100644 --- a/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol +++ b/packages/smart-contracts/src/contracts/BatchNoConversionPayments.sol @@ -105,7 +105,7 @@ contract BatchNoConversionPayments is Ownable { * @notice Send a batch of Native token payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. * @param requestDetails List of Native tokens requests to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduces gas consumption. * @param feeAddress The fee recipient. * @dev It uses NativeFeeProxy (EthereumFeeProxy) to pay an invoice and fees with a payment reference. * Make sure: msg.value >= sum(_amouts)+sum(_feeAmounts)+sumBatchFeeAmount @@ -158,7 +158,7 @@ contract BatchNoConversionPayments is Ownable { * @notice Send a batch of Native token payments with fees and paymentReferences to multiple accounts. * If one payment fails, the whole batch reverts. * @param requestDetails List of Native tokens requests to pay. - * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduce gas consumption. + * @param skipFeeUSDLimit Setting the value to true skips the USD fee limit, and reduces gas consumption. * @param batchFeeAmountUSD The batch fee amount in USD already paid. * @param feeAddress The fee recipient. * @dev It uses NativeFeeProxy (EthereumFeeProxy) to pay an invoice and fees with a payment reference. diff --git a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts index 8d67a47938..1c264e2f15 100644 --- a/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts +++ b/packages/smart-contracts/src/lib/artifacts/BatchConversionPayments/index.ts @@ -14,46 +14,46 @@ export const batchConversionPaymentsArtifact = new ContractArtifact Date: Thu, 3 Nov 2022 12:19:26 +0100 Subject: [PATCH 138/138] update batch and re order hardhat order as mainnet is the slower --- packages/smart-contracts/hardhat.config.ts | 2 +- .../scripts-create2/contract-setup/adminTasks.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smart-contracts/hardhat.config.ts b/packages/smart-contracts/hardhat.config.ts index fc0dcc5919..d655f2db4c 100644 --- a/packages/smart-contracts/hardhat.config.ts +++ b/packages/smart-contracts/hardhat.config.ts @@ -164,7 +164,7 @@ export default { signer: process.env.ADMIN_PRIVATE_KEY, networks: process.env.NETWORK ? [process.env.NETWORK] - : ['mainnet', 'matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche'], + : ['matic', 'bsc', 'celo', 'xdai', 'fuse', 'arbitrum-one', 'fantom', 'avalanche', 'mainnet'], gasLimit: undefined, deployerAddress: requestDeployer, }, diff --git a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts index ecb4d638bd..2c887f0e45 100644 --- a/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts +++ b/packages/smart-contracts/scripts-create2/contract-setup/adminTasks.ts @@ -7,7 +7,7 @@ import { BigNumber } from 'ethers'; export const REQUEST_SWAP_FEES = 5; // Batch fee: temporarily at 0% -const BATCH_FEE = 0; +const BATCH_FEE = 30; // Batch fee amount in USD Limit: 150 * 1e8 ($150) const BATCH_FEE_AMOUNT_USD_LIMIT = 150 * 1e8;