Skip to content

Commit

Permalink
feat: Allow minting ERC20TransferableReceivables on behalf of other u…
Browse files Browse the repository at this point in the history
…sers (#1091)
  • Loading branch information
mliu committed Apr 8, 2023
1 parent 750908c commit 1a5618b
Show file tree
Hide file tree
Showing 5 changed files with 908 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ 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 {
getReceivableTokenIdForRequest,
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 */
Expand All @@ -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;

Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -98,16 +152,27 @@ 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,
string memory newTokenURI
) 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'
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 1a5618b

Please sign in to comment.