Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(erc777): <- add custom function call when minting #1

Merged
merged 7 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions contracts/interfaces/IPReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pragma solidity ^0.6.2;

/**
* @title IPReceiver
* @author pNetwork
*
* @dev Interface for contracts excpecting cross-chain data
*/
interface IPReceiver {
/*
* @dev Function called when userData.length > 0 when minting the pToken
oliviera9 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param userData
*/
function receiveUserData(bytes calldata userData) external;
}
14 changes: 14 additions & 0 deletions contracts/pToken.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pragma solidity ^0.6.2;

import {IPReceiver} from "./interfaces/IPReceiver.sol";
import "./ERC777GSN.sol";
import "./ERC777WithAdminOperatorUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
Expand All @@ -14,6 +15,8 @@ contract PToken is
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes4 public ORIGIN_CHAIN_ID;

event ReceiveUserDataFailed();

event Redeem(
address indexed redeemer,
uint256 value,
Expand Down Expand Up @@ -76,6 +79,17 @@ contract PToken is
"Recipient cannot be the token contract address!"
);
_mint(recipient, value, userData, operatorData);
if (userData.length > 0) {
gskapka marked this conversation as resolved.
Show resolved Hide resolved
// pNetwork aims to deliver cross chain messages successfully regardless of what the user may do with them.
// We do not want this mint transaction reverting if their receiveUserData function reverts,
// and thus we swallow any such errors, emitting a `ReceiveUserDataFailed` event instead.
// The low-level call is used because in the solidity version this contract was written in,
// a try/catch block fails to catch the revert caused if the receiver is not in fact a contract.
// This way, a user also has the option include userData even when minting to an externally owned account.
oliviera9 marked this conversation as resolved.
Show resolved Hide resolved
bytes memory data = abi.encodeWithSelector(IPReceiver.receiveUserData.selector, userData);
(bool success, ) = recipient.call(data);
if (!success) emit ReceiveUserDataFailed();
}
return true;
}

Expand Down
14 changes: 14 additions & 0 deletions contracts/pTokenNoGSN.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pragma solidity ^0.6.2;

import {IPReceiver} from "./interfaces/IPReceiver.sol";
import "./ERC777WithAdminOperatorUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
Expand All @@ -18,6 +19,8 @@ contract PTokenNoGSN is
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes4 public ORIGIN_CHAIN_ID;

event ReceiveUserDataFailed();

event Redeem(
address indexed redeemer,
uint256 value,
Expand Down Expand Up @@ -79,6 +82,17 @@ contract PTokenNoGSN is
"Recipient cannot be the token contract address!"
);
_mint(recipient, value, userData, operatorData);
if (userData.length > 0) {
// pNetwork aims to deliver cross chain messages successfully regardless of what the user may do with them.
// We do not want this mint transaction reverting if their receiveUserData function reverts,
// and thus we swallow any such errors, emitting a `ReceiveUserDataFailed` event instead.
// The low-level call is used because in the solidity version this contract was written in,
// a try/catch block fails to catch the revert caused if the receiver is not in fact a contract.
// This way, a user also has the option include userData even when minting to an externally owned account.
gskapka marked this conversation as resolved.
Show resolved Hide resolved
bytes memory data = abi.encodeWithSelector(IPReceiver.receiveUserData.selector, userData);
(bool success, ) = recipient.call(data);
if (!success) emit ReceiveUserDataFailed();
}
return true;
}

Expand Down
20 changes: 20 additions & 0 deletions contracts/test-contracts/PReceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pragma solidity ^0.6.2;

import {IPReceiver} from "../interfaces/IPReceiver.sol";

contract PReceiver is IPReceiver {
event UserData(bytes data);

function receiveUserData(bytes calldata userData) external override {
emit UserData(userData);
}
}

contract PReceiverReverting is IPReceiver {
oliviera9 marked this conversation as resolved.
Show resolved Hide resolved
function receiveUserData(bytes calldata) external override {
require(false, "Revert!");
}
}

contract NotImplementingReceiveUserDataFxn {
}
14 changes: 4 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ptokens-erc777-smart-contract",
"version": "3.11.1",
"version": "3.12.0",
"description": "The pToken ERC777 smart-contract & CLI",
"main": "cli.js",
"scripts": {
Expand Down
74 changes: 72 additions & 2 deletions test/05-ptoken.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,14 @@ USE_GSN.map(_useGSN =>
}
})

it('`mint()` w/ data should mint tokens & emit correct events', async () => {
it('`mint()` w/ data should mint tokens to a non-contract address & emit correct events', async () => {
oliviera9 marked this conversation as resolved.
Show resolved Hide resolved
oliviera9 marked this conversation as resolved.
Show resolved Hide resolved
const data = '0xdead'
const expectedNumEvents = 2
const operatorData = '0xb33f'
const recipient = OTHERS[0].address
const recipientBalanceBefore = await getTokenBalance(recipient, CONTRACT)
const tx = await CONTRACT['mint(address,uint256,bytes,bytes)'](recipient, AMOUNT, data, operatorData)
const tx =
await CONTRACT['mint(address,uint256,bytes,bytes)'](recipient, AMOUNT, data, operatorData)
const { events } = await tx.wait()
const recipientBalanceAfter = await getTokenBalance(recipient, CONTRACT)
assert(recipientBalanceBefore.eq(BigNumber.from(0)))
Expand All @@ -161,6 +162,75 @@ USE_GSN.map(_useGSN =>
assertMintEvent(events, recipient, OWNER.address, AMOUNT, data, operatorData)
})

it('`mint()` w/ data should mint & call receiveUserData & emit correct events', async () => {
const data = '0xdead'
const expectedNumEvents = 3
const operatorData = '0xb33f'
const recipientContract = await ethers
.getContractFactory('contracts/test-contracts/PReceiver.sol:PReceiver')
.then(_factory => upgrades.deployProxy(_factory))

const recipientBalanceBefore = await getTokenBalance(recipientContract.address, CONTRACT)
const tx =
await CONTRACT['mint(address,uint256,bytes,bytes)'](recipientContract.address, AMOUNT, data, operatorData)
const { events } = await tx.wait()
const recipientBalanceAfter = await getTokenBalance(recipientContract.address, CONTRACT)
assert(recipientBalanceBefore.eq(BigNumber.from(0)))
assert(recipientBalanceAfter.eq(BigNumber.from(AMOUNT)))
assert.strictEqual(events.length, expectedNumEvents)
assertTransferEvent(events, ZERO_ADDRESS, recipientContract.address, AMOUNT)
assertMintEvent(events, recipientContract.address, OWNER.address, AMOUNT, data, operatorData)
const userDataEvent = recipientContract.interface.parseLog(events.at(-1))
assert.strictEqual(userDataEvent.name, 'UserData')
assert.strictEqual(userDataEvent.args.data, data)
})

// eslint-disable-next-line max-len
it('`mint()` w/ `userData` should mint tokens, emit correct events & not revert despite `receiveUserData` hook being not implemented',
async () => {
const data = '0xdead'
const expectedNumEvents = 3
const operatorData = '0xb33f'
const recipientContract = await ethers
.getContractFactory('contracts/test-contracts/PReceiver.sol:NotImplementingReceiveUserDataFxn')
.then(_factory => upgrades.deployProxy(_factory))

const recipientBalanceBefore = await getTokenBalance(recipientContract.address, CONTRACT)
const tx =
await CONTRACT['mint(address,uint256,bytes,bytes)'](recipientContract.address, AMOUNT, data, operatorData)
const { events } = await tx.wait()
const recipientBalanceAfter = await getTokenBalance(recipientContract.address, CONTRACT)
assert(recipientBalanceBefore.eq(BigNumber.from(0)))
assert(recipientBalanceAfter.eq(BigNumber.from(AMOUNT)))
assert.strictEqual(events.length, expectedNumEvents)
assertTransferEvent(events, ZERO_ADDRESS, recipientContract.address, AMOUNT)
assertMintEvent(events, recipientContract.address, OWNER.address, AMOUNT, data, operatorData)
assert.strictEqual(events.at(-1).event, 'ReceiveUserDataFailed')
})

// eslint-disable-next-line max-len
it('`mint()` w/ `userData` should mint tokens, emit correct events & not revert despite `receiveUserData` hook reverting',
async () => {
const data = '0xdead'
const expectedNumEvents = 3
const operatorData = '0xb33f'
const recipientContract = await ethers
.getContractFactory('contracts/test-contracts/PReceiver.sol:PReceiverReverting')
.then(_factory => upgrades.deployProxy(_factory))

const recipientBalanceBefore = await getTokenBalance(recipientContract.address, CONTRACT)
const tx =
await CONTRACT['mint(address,uint256,bytes,bytes)'](recipientContract.address, AMOUNT, data, operatorData)
const { events } = await tx.wait()
const recipientBalanceAfter = await getTokenBalance(recipientContract.address, CONTRACT)
assert(recipientBalanceBefore.eq(BigNumber.from(0)))
assert(recipientBalanceAfter.eq(BigNumber.from(AMOUNT)))
assert.strictEqual(events.length, expectedNumEvents)
assertTransferEvent(events, ZERO_ADDRESS, recipientContract.address, AMOUNT)
assertMintEvent(events, recipientContract.address, OWNER.address, AMOUNT, data, operatorData)
assert.strictEqual(events.at(-1).event, 'ReceiveUserDataFailed')
})

it('Should revert when minting tokens with the contract address as the recipient', async () => {
const recipient = CONTRACT.address
try {
Expand Down
Loading