From 1a5618b368a3ca49aaf6e4bca3f3210caca3cac1 Mon Sep 17 00:00:00 2001 From: Michael Liu Date: Sat, 8 Apr 2023 16:57:24 -0400 Subject: [PATCH] feat: Allow minting ERC20TransferableReceivables on behalf of other users (#1091) --- .../payment/erc20-transferable-receivable.ts | 4 +- .../erc20-transferable-receivable.test.ts | 136 +++- .../contracts/ERC20TransferableReceivable.sol | 94 ++- .../ERC20TransferableReceivable/0.1.1.json | 655 ++++++++++++++++++ .../ERC20TransferableReceivable.test.ts | 49 +- 5 files changed, 908 insertions(+), 30 deletions(-) create mode 100644 packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json diff --git a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts index c209f89c1c..fd5f2d1657 100644 --- a/packages/payment-processor/src/payment/erc20-transferable-receivable.ts +++ b/packages/payment-processor/src/payment/erc20-transferable-receivable.ts @@ -117,12 +117,12 @@ export function encodeMintErc20TransferableReceivableRequest( const tokenAddress = request.currencyInfo.value; const metadata = Buffer.from(request.requestId).toString('base64'); // metadata is requestId - const { paymentReference } = getRequestPaymentValues(request); + const { paymentReference, paymentAddress } = getRequestPaymentValues(request); const amount = getAmountToPay(request); const receivableContract = ERC20TransferableReceivable__factory.createInterface(); return receivableContract.encodeFunctionData('mint', [ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + paymentAddress, `0x${paymentReference}`, amount, tokenAddress, diff --git a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts index 10563ec6c4..f1cf3f3fca 100644 --- a/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts +++ b/packages/payment-processor/test/payment/erc20-transferable-receivable.test.ts @@ -8,7 +8,8 @@ import { } from '@requestnetwork/types'; import { deepCopy } from '@requestnetwork/utils'; -import { PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; +import { Erc20PaymentNetwork, PaymentReferenceCalculator } from '@requestnetwork/payment-detection'; +import { ERC20TransferableReceivable__factory } from '@requestnetwork/smart-contracts/types'; import { approveErc20, getErc20Balance } from '../../src/payment/erc20'; import { @@ -16,6 +17,7 @@ import { mintErc20TransferableReceivable, payErc20TransferableReceivableRequest, } from '../../src/payment/erc20-transferable-receivable'; +import { getProxyAddress } from '../../src/payment/utils'; /* eslint-disable no-magic-numbers */ /* eslint-disable @typescript-eslint/no-unused-expressions */ @@ -26,6 +28,7 @@ const mnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble const feeAddress = '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef'; const provider = new providers.JsonRpcProvider('http://localhost:8545'); const payeeWallet = Wallet.createRandom().connect(provider); +const thirdPartyWallet = Wallet.createRandom().connect(provider); const wallet = Wallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/1").connect(provider); const paymentAddress = payeeWallet.address; @@ -85,7 +88,17 @@ describe('erc20-transferable-receivable', () => { value: utils.parseEther('1'), }; - const txResponse = await wallet.sendTransaction(tx); + let txResponse = await wallet.sendTransaction(tx); + await txResponse.wait(1); + + // Send funds to thirdPartyWallet + tx = { + to: thirdPartyWallet.address, + // Convert currency unit from ether to wei + value: utils.parseEther('1'), + }; + + txResponse = await wallet.sendTransaction(tx); await txResponse.wait(1); const mintTx = await mintErc20TransferableReceivable(validRequest, payeeWallet, { @@ -218,5 +231,124 @@ describe('erc20-transferable-receivable', () => { BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), ).toBeTruthy(); }); + + it('other wallets can mint receivable for owner', async () => { + // Request without a receivable minted yet + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = '0x01'; + + const mintTx = await mintErc20TransferableReceivable(request, thirdPartyWallet, { + gasLimit: BigNumber.from('20000000'), + }); + let confirmedTx = await mintTx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(mintTx.hash).not.toBeUndefined(); + + // get the balance to compare after payment + const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider); + + const tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, { + gasLimit: BigNumber.from('20000000'), + }); + + confirmedTx = await tx.wait(1); + + const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // ERC20 balance should be lower + expect( + BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), + ).toBeTruthy(); + }); + + it('rejects paying unless minted to correct owner', async () => { + // Request without a receivable minted yet + const request = deepCopy(validRequest) as ClientTypes.IRequestData; + request.requestId = '0x02'; + + let shortReference = PaymentReferenceCalculator.calculate( + request.requestId, + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values + .salt, + paymentAddress, + ); + let metadata = Buffer.from(request.requestId).toString('base64'); + let receivableContract = ERC20TransferableReceivable__factory.createInterface(); + let data = receivableContract.encodeFunctionData('mint', [ + thirdPartyWallet.address, + `0x${shortReference}`, + '100', + erc20ContractAddress, + metadata, + ]); + let tx = await thirdPartyWallet.sendTransaction({ + data, + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }); + let confirmedTx = await tx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + await expect(payErc20TransferableReceivableRequest(request, wallet)).rejects.toThrowError( + 'The receivable for this request has not been minted yet. Please check with the payee.', + ); + + // Mint the receivable for the correct paymentAddress + shortReference = PaymentReferenceCalculator.calculate( + request.requestId, + request.extensions[ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_TRANSFERABLE_RECEIVABLE].values + .salt, + paymentAddress, + ); + metadata = Buffer.from(request.requestId).toString('base64'); + receivableContract = ERC20TransferableReceivable__factory.createInterface(); + data = receivableContract.encodeFunctionData('mint', [ + paymentAddress, + `0x${shortReference}`, + '100', + erc20ContractAddress, + metadata, + ]); + tx = await thirdPartyWallet.sendTransaction({ + data, + to: getProxyAddress( + request, + Erc20PaymentNetwork.ERC20TransferableReceivablePaymentDetector.getDeploymentInformation, + ), + value: 0, + }); + confirmedTx = await tx.wait(1); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // get the balance to compare after payment + const balanceErc20Before = await getErc20Balance(request, payeeWallet.address, provider); + + tx = await payErc20TransferableReceivableRequest(request, wallet, 1, 0, { + gasLimit: BigNumber.from('20000000'), + }); + + confirmedTx = await tx.wait(1); + + const balanceErc20After = await getErc20Balance(request, payeeWallet.address, provider); + + expect(confirmedTx.status).toBe(1); + expect(tx.hash).not.toBeUndefined(); + + // ERC20 balance should be lower + expect( + BigNumber.from(balanceErc20After).eq(BigNumber.from(balanceErc20Before).add(1)), + ).toBeTruthy(); + }); }); }); diff --git a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol index 2c09882298..0d7fc47679 100644 --- a/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol +++ b/packages/smart-contracts/src/contracts/ERC20TransferableReceivable.sol @@ -5,29 +5,60 @@ import '@openzeppelin/contracts/utils/Counters.sol'; import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol'; import '@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol'; +/** + * @title ERC20TransferableReceivable + * @author Request Network + * @dev ERC721 contract for creating and managing unique NFTs representing receivables + * that can be paid with any ERC20 token + */ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStorage { using Counters for Counters.Counter; - // Counter for uniquely identifying payments + /** + * @dev Counter for uniquely identifying payments + */ Counters.Counter private _paymentId; - // Counter for uniquely identifying receivable tokens + /** + * @dev Counter for uniquely identifying receivables + */ Counters.Counter private _receivableTokenId; + /** + * @dev Struct for storing information about a receivable + */ struct ReceivableInfo { address tokenAddress; uint256 amount; uint256 balance; } - mapping(uint256 => ReceivableInfo) public receivableInfoMapping; - // Mapping for looking up receivable token given a paymentReference - // and minter address + /** + * @notice Mapping for looking up a receivable given a paymentReference and minter address + */ mapping(bytes32 => uint256) public receivableTokenIdMapping; + /** + * @notice Mapping for looking up information about a receivable given a receivableTokenId + */ + mapping(uint256 => ReceivableInfo) public receivableInfoMapping; + + /** + * @notice Address of the payment proxy contract that handles the transfer of ERC20 tokens + */ address public paymentProxy; - // Event to declare payments to a receivableTokenId + /** + * @notice Event to declare payments to a receivableTokenId + * @param sender The address of the sender + * @param recipient The address of the recipient of the payment + * @param amount The amount of the payment + * @param paymentProxy The address of the payment proxy contract + * @param receivableTokenId The ID of the receivable being paid + * @param tokenAddress The address of the ERC20 token used to pay the receivable + * @param paymentId The ID of the payment + * @param paymentReference The reference for the payment + */ event TransferableReceivablePayment( address sender, address recipient, @@ -39,8 +70,16 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora bytes indexed paymentReference ); - // Event to declare a transfer with a reference - // This event is emitted from a delegatecall to an ERC20FeeProxy contract + /** + * @notice Event to declare ERC20 token transfers + * @param tokenAddress The address of the ERC20 token being transferred + * @param to The address of the recipient of the transfer + * @param amount The amount of the transfer + * @param paymentReference The reference for the transfer + * @param feeAmount The amount of the transfer fee + * @param feeAddress The address of the fee recipient + * @dev This event is emitted from a delegatecall to an ERC20FeeProxy contract + */ event TransferWithReferenceAndFee( address tokenAddress, address to, @@ -50,6 +89,11 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora address feeAddress ); + /** + * @param name The name of the ERC721 token + * @param symbol The symbol of the ERC721 token + * @param _paymentProxyAddress The address of the payment proxy contract + */ constructor( string memory name, string memory symbol, @@ -58,6 +102,16 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora paymentProxy = _paymentProxyAddress; } + /** + * @notice Pay the owner of the specified receivable with the provided amount of ERC20 tokens. + * @param receivableTokenId The ID of the receivable token to pay. + * @param amount The amount of ERC20 tokens to pay the owner. + * @param paymentReference The reference for the payment. + * @param feeAmount The amount of ERC20 tokens to be paid as a fee. + * @param feeAddress The address to which the fee should be paid. + * @dev This function uses delegatecall to call on a contract which emits + a TransferWithReferenceAndFee event. + */ function payOwner( uint256 receivableTokenId, uint256 amount, @@ -98,7 +152,17 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora ); } + /** + * @notice Mint a new transferable receivable. + * @param owner The address to whom the receivable token will be minted. + * @param paymentReference A reference for the payment. + * @param amount The amount of ERC20 tokens to be paid. + * @param erc20Addr The address of the ERC20 token to be used as payment. + * @param newTokenURI The URI to be set on the minted receivable token. + * @dev Anyone can pay for the mint of a receivable on behalf of a user + */ function mint( + address owner, bytes calldata paymentReference, uint256 amount, address erc20Addr, @@ -106,8 +170,9 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora ) external { require(paymentReference.length > 0, 'Zero paymentReference provided'); require(amount > 0, 'Zero amount provided'); + require(owner != address(0), 'Zero address provided for owner'); require(erc20Addr != address(0), 'Zero address provided'); - bytes32 idKey = keccak256(abi.encodePacked(msg.sender, paymentReference)); + bytes32 idKey = keccak256(abi.encodePacked(owner, paymentReference)); require( receivableTokenIdMapping[idKey] == 0, 'Receivable has already been minted for this user and request' @@ -121,10 +186,15 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora balance: 0 }); - _mint(msg.sender, currentReceivableTokenId); + _mint(owner, currentReceivableTokenId); _setTokenURI(currentReceivableTokenId, newTokenURI); } + /** + * @notice Get an array of all receivable token IDs owned by a specific address. + * @param _owner The address that owns the receivable tokens. + * @return An array of all receivable token IDs owned by the specified address. + */ function getTokenIds(address _owner) public view returns (uint256[] memory) { uint256[] memory _tokensOfOwner = new uint256[](ERC721.balanceOf(_owner)); uint256 i; @@ -136,6 +206,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora } // The following functions are overrides required by Solidity. + /// @dev Overrides ERC721's _beforeTokenTransfer method to include functionality from ERC721Enumerable. function _beforeTokenTransfer( address from, address to, @@ -144,10 +215,12 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora super._beforeTokenTransfer(from, to, tokenId); } + /// @dev Overrides ERC721's _burn method to include functionality from ERC721URIStorage. function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { super._burn(tokenId); } + /// @dev Overrides ERC721's tokenURI method to include functionality from ERC721URIStorage. function tokenURI(uint256 tokenId) public view @@ -157,6 +230,7 @@ contract ERC20TransferableReceivable is ERC721, ERC721Enumerable, ERC721URIStora return super.tokenURI(tokenId); } + /// @dev Overrides ERC721's supportsInterface method to include functionality from ERC721Enumerable. function supportsInterface(bytes4 interfaceId) public view diff --git a/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json new file mode 100644 index 0000000000..aa1b105406 --- /dev/null +++ b/packages/smart-contracts/src/lib/artifacts/ERC20TransferableReceivable/0.1.1.json @@ -0,0 +1,655 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "internalType": "address", + "name": "_paymentProxyAddress", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "TransferWithReferenceAndFee", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "paymentProxy", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "receivableTokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paymentId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + } + ], + "name": "TransferableReceivablePayment", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "getTokenIds", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "erc20Addr", + "type": "address" + }, + { + "internalType": "string", + "name": "newTokenURI", + "type": "string" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "receivableTokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "paymentReference", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "feeAddress", + "type": "address" + } + ], + "name": "payOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paymentProxy", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "receivableInfoMapping", + "outputs": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "receivableTokenIdMapping", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] +} diff --git a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts index 844db3fa26..5a9f012e27 100644 --- a/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts +++ b/packages/smart-contracts/test/contracts/ERC20TransferableReceivable.test.ts @@ -48,27 +48,33 @@ describe('contract: ERC20TransferableReceivable', () => { describe('mint', async function () { it('revert with empty paymentReference', async function () { - await expect(receivable.mint([], 1, testToken.address, '')).to.be.revertedWith( + await expect(receivable.mint(user1Addr, [], 1, testToken.address, '')).to.be.revertedWith( 'Zero paymentReference provided', ); }); it('revert with zero amount', async function () { - await expect(receivable.mint('0x01', 0, testToken.address, '')).to.be.revertedWith( + await expect(receivable.mint(user1Addr, '0x01', 0, testToken.address, '')).to.be.revertedWith( 'Zero amount provided', ); }); it('revert with empty asset address', async function () { - await expect(receivable.mint('0x01', 1, ethers.constants.AddressZero, '')).to.be.revertedWith( - 'Zero address provided', - ); + await expect( + receivable.mint(user1Addr, '0x01', 1, ethers.constants.AddressZero, ''), + ).to.be.revertedWith('Zero address provided'); + }); + + it('revert with empty owner address', async function () { + await expect( + receivable.mint(ethers.constants.AddressZero, '0x01', 1, testToken.address, ''), + ).to.be.revertedWith('Zero address provided for owner'); }); it('revert with duplicated receivableId', async function () { - await receivable.connect(user1).mint('0x01', 1, testToken.address, ''); + await receivable.connect(user1).mint(user1Addr, '0x01', 1, testToken.address, ''); await expect( - receivable.connect(user1).mint('0x01', 2, testToken.address, ''), + receivable.connect(user1).mint(user1Addr, '0x01', 2, testToken.address, ''), ).to.be.revertedWith('Receivable has already been minted for this user and request'); }); @@ -76,7 +82,9 @@ describe('contract: ERC20TransferableReceivable', () => { const receivableId = '0x0134cc5f0224acb0544a9d325f8f2160c53130ba4671849472f2a96a35c93a78d6'; const metadata = ethers.utils.base64.encode(receivableId); const paymentRef = '0x01' as BytesLike; - await receivable.connect(user1).mint(paymentRef, BASE_DECIMAL, testToken.address, metadata); + await receivable + .connect(user1) + .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, metadata); const ids = await receivable.getTokenIds(user1Addr); const tokenId = ids[0]; expect(await receivable.ownerOf(tokenId)).to.equals(user1Addr); @@ -94,7 +102,7 @@ describe('contract: ERC20TransferableReceivable', () => { const paymentRef = '0x01' as BytesLike; await receivable .connect(user1) - .mint(paymentRef, BASE_DECIMAL, testToken.address, receivableId); + .mint(user1Addr, paymentRef, BASE_DECIMAL, testToken.address, receivableId); const ids = await receivable.getTokenIds(user1Addr); const tokenId = ids[0]; @@ -103,12 +111,12 @@ describe('contract: ERC20TransferableReceivable', () => { }); it('list receivables', async function () { - await receivable.connect(user1).mint('0x01', BASE_DECIMAL, testToken.address, '1'); - await receivable.connect(user1).mint('0x02', BASE_DECIMAL, testToken.address, '2'); - await receivable.connect(user1).mint('0x03', BASE_DECIMAL, testToken.address, '3'); + await receivable.connect(user1).mint(user1Addr, '0x01', BASE_DECIMAL, testToken.address, '1'); + await receivable.connect(user1).mint(user1Addr, '0x02', BASE_DECIMAL, testToken.address, '2'); + await receivable.connect(user1).mint(user1Addr, '0x03', BASE_DECIMAL, testToken.address, '3'); await verifyReceivables(user1Addr, [1, 2, 3]); - await receivable.connect(user2).mint('0x04', BASE_DECIMAL, testToken.address, '4'); - await receivable.connect(user2).mint('0x05', BASE_DECIMAL, testToken.address, '5'); + await receivable.connect(user2).mint(user2Addr, '0x04', BASE_DECIMAL, testToken.address, '4'); + await receivable.connect(user2).mint(user2Addr, '0x05', BASE_DECIMAL, testToken.address, '5'); await verifyReceivables(user2Addr, [4, 5]); await receivable.connect(user1).transferFrom(user1Addr, user2Addr, 1); await verifyReceivables(user1Addr, [3, 2]); @@ -141,7 +149,7 @@ describe('contract: ERC20TransferableReceivable', () => { beforeEach(async () => { paymentRef = '0x01' as BytesLike; amount = BN.from(100).mul(BASE_DECIMAL); - await receivable.connect(user1).mint(paymentRef, amount, testToken.address, '1'); + await receivable.connect(user1).mint(user1Addr, paymentRef, amount, testToken.address, '1'); const ids = await receivable.getTokenIds(await user1.getAddress()); tokenId = ids[0]; feeAmount = BN.from(10).mul(BASE_DECIMAL); @@ -183,11 +191,20 @@ describe('contract: ERC20TransferableReceivable', () => { }); it('allow multiple mints per receivable', async function () { - await receivable.connect(user2).mint(paymentRef, amount, testToken.address, '1'); + await receivable.connect(user2).mint(user2Addr, paymentRef, amount, testToken.address, '1'); const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user1Addr, paymentRef]); expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); }); + it('allows user to mint on behalf of another user', async function () { + paymentRef = '0x02' as BytesLike; + await receivable.connect(user1).mint(user2Addr, paymentRef, amount, testToken.address, '1'); + const key = ethers.utils.solidityKeccak256(['address', 'bytes'], [user2Addr, paymentRef]); + const ids = await receivable.getTokenIds(user2Addr); + tokenId = ids[0]; + expect(await receivable.receivableTokenIdMapping(key)).to.equals(tokenId); + }); + it('payment greater than amount', async function () { const beforeBal = await testToken.balanceOf(user1Addr); await expect(