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

ERC827-like methods for ERC721Holdings #2

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion contracts/ERC721Holdings.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pragma solidity ^0.4.23;

import "./ERC721HoldingsBasic.sol";
import "./ERC721HoldingsExecuteCalls.sol";


/**
Expand Down Expand Up @@ -29,4 +30,4 @@ contract ERC721HoldingsMetadata is ERC721HoldingsBasic {
* @title Holdings of ERC-721 Non-Fungible Token Standard, full implementation interface
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721Holdings is ERC721HoldingsBasic, ERC721HoldingsEnumerable, ERC721HoldingsMetadata {}
contract ERC721Holdings is ERC721HoldingsBasic, ERC721HoldingsEnumerable, ERC721HoldingsMetadata, ERC721HoldingsExecuteCalls {}
12 changes: 12 additions & 0 deletions contracts/ERC721HoldingsExecuteCalls.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pragma solidity ^0.4.23;

import "./ERC721HoldingsBasic.sol";


contract ERC721HoldingsExecuteCalls is ERC721HoldingsBasic {
function approveAndCall(address _spender, uint256 _tokenId, bytes _data)
public payable returns (bool);

function transferFromAndCall(address _from, uint256 _to, address _toOrigin, uint256 _tokenId, bytes _data)
public payable returns (bool);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add newline at EOF

36 changes: 36 additions & 0 deletions contracts/ERC721HoldingsExecuteCallsToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
pragma solidity ^0.4.23;

import "./ERC721HoldingsBasicToken.sol";


contract ERC721HoldingsExecuteCallsToken is ERC721HoldingsBasicToken {

function approveAndCall(address _spender, uint256 _tokenId, bytes _data)
public payable returns (bool)
{
super.approve(_spender, _tokenId);

// solium-disable-next-line security/no-call-value
require(_spender.call.value(msg.value)(_data));

return true;
}

function transferFromAndCall(
address _from,
uint256 _to,
address _toOrigin,
uint256 _tokenId,
bytes _data)
public payable returns (bool)
{
address _holderOwner = _ownerOf(_to, _toOrigin);
require(_holderOwner != address(this));

super.transferFrom(_from, _to, _toOrigin, _tokenId);

// solium-disable-next-line security/no-call-value
require(_holderOwner.call.value(msg.value)(_data));
return true;
}
}
3 changes: 2 additions & 1 deletion contracts/ERC721HoldingsToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ pragma solidity ^0.4.23;

import "./ERC721Holdings.sol";
import "./ERC721HoldingsBasicToken.sol";
import "./ERC721HoldingsExecuteCallsToken.sol";


/**
* @title Full ERC721Holdings Token
* This implementation includes all the required and some optional functionality of the ERC721Holdings standard
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721HoldingsToken is ERC721Holdings, ERC721HoldingsBasicToken {
contract ERC721HoldingsToken is ERC721Holdings, ERC721HoldingsBasicToken, ERC721HoldingsExecuteCallsToken {
// Token name
string internal name_;

Expand Down
8 changes: 4 additions & 4 deletions contracts/mocks/ERC721HoldingsBasicTokenMock.sol
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
pragma solidity ^0.4.23;

import "../ERC721HoldingsBasicToken.sol";
import "../ERC721HoldingsToken.sol";


/**
* @title ERC721HoldingsBasicTokenMock
* @title ERC721HoldingsTokenMock
* This mock just provides a public mint and burn functions for testing purposes
*/
contract ERC721HoldingsBasicTokenMock is ERC721HoldingsBasicToken {
contract ERC721HoldingsTokenMock is ERC721HoldingsToken {

constructor (address _nftAddress) ERC721HoldingsBasicToken(_nftAddress) public {
constructor (address _nftAddress) ERC721HoldingsToken("Test", "TST", _nftAddress) public {
require(_nftAddress != address(0));
tokens = ERC721(_nftAddress);
}
Expand Down
31 changes: 31 additions & 0 deletions contracts/mocks/MessageHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
pragma solidity ^0.4.21;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be consistent and use 'pragma solidity ^0.4.23;'



contract MessageHelper {

event Show(bytes32 b32, uint256 number, string text);
event Buy(bytes32 b32, uint256 number, string text, uint256 value);

function showMessage( bytes32 message, uint256 number, string text ) public returns (bool) {
emit Show(message, number, text);
return true;
}

function buyMessage( bytes32 message, uint256 number, string text ) public payable returns (bool) {
emit Buy(message, number, text, msg.value);
return true;
}

function fail() public pure {
require(false);
}

function call(address to, bytes data) public returns (bool) {
// solium-disable-next-line security/no-low-level-calls
if (to.call(data))
return true;
else
return false;
}

}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add newline to EOF

21 changes: 0 additions & 21 deletions test/ERC721HoldingsBasicToken.test.js

This file was deleted.

200 changes: 200 additions & 0 deletions test/ERC721HoldingsExecuteCalls.behaviour.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
const Message = artifacts.require('mocks/MessageHelper.sol');

import assertRevert from './helpers/assertRevert';
const BigNumber = web3.BigNumber;

require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();

export default function shouldExecuteCallsERC721HoldingsToken (accounts) {
const firstTokenId = 1;
const secondTokenId = 2;
const unknownTokenId = 3;

const firstAvatarId = 1;
const secondAvatarId = 2;
const thirdAvatarId = 3;

const creator = accounts[0];
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

describe('like an execute calls ERC721HoldingsToken', function () {
beforeEach(async function () {
this.message = await Message.new({ from: creator });

await this.avatars.mint(creator, firstAvatarId, { from: creator });
await this.avatars.mint(this.message.contract.address, secondAvatarId, { from: creator });
await this.avatars.mint(this.token.contract.address, thirdAvatarId, { from: creator });

await this.token.mint(firstAvatarId, this.avatars.address, firstTokenId, { from: creator });
await this.token.mint(firstAvatarId, this.avatars.address, secondTokenId, { from: creator });
});

describe('Test Execute Calls methods', function () {
it('should allow payment through approve', async function () {
const extraData = this.message.contract.buyMessage.getData(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used encodeCall helper in r8-app tests.

It would look something like this:

const data = encodeCall('buyMessage', ['bytes32', 'uint256', 'string'], [web3.toHex(123456), 666, 'Transfer Done']);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using the web3 v0.20 API and this will change with web3 v1.0 release.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a FYI comment, no need to change anything for now.

web3.toHex(123456), 666, 'Transfer Done'
);

const transaction = await this.token.approveAndCall(
this.message.contract.address, firstTokenId, extraData, { from: creator, value: 1 }
);

assert.equal(2, transaction.receipt.logs.length);

const appproved = await this.token.getApproved(firstTokenId);
this.message.contract.address.should.be.equal(appproved);

const balance = await web3.eth.getBalance(this.message.contract.address);
new BigNumber(1).should.be.bignumber.equal(balance);
});

it('should allow payment through transferFrom', async function () {
const extraData = this.message.contract.buyMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approve(accounts[1], firstTokenId, { from: creator });

const appproved = await this.token.getApproved(firstTokenId);
accounts[1].should.be.equal(appproved);

const transaction = await this.token.transferFromAndCall(
accounts[0], secondAvatarId, this.avatars.contract.address, firstTokenId, extraData, { from: accounts[1], value: 1 }
);

assert.equal(3, transaction.receipt.logs.length);

const tokenBalance = await this.token.balanceOf(secondAvatarId, this.avatars.address);
new BigNumber(1).should.be.bignumber.equal(tokenBalance);

const ethBalance = await web3.eth.getBalance(this.message.contract.address);
new BigNumber(1).should.be.bignumber.equal(ethBalance);
});

it('should revert funds of failure inside approve (with data)', async function () {
// showMessage is not payable, so it fails when called with msg.value
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approveAndCall(
this.message.contract.address, firstTokenId, extraData, { from: creator, value: 1 }
).should.be.rejectedWith('revert');

// approval should not have gone through so approved address is still 0x0
const appproved = await this.token.getApproved(firstTokenId);
ZERO_ADDRESS.should.be.equal(appproved);

const ethBalance = await web3.eth.getBalance(this.message.contract.address);
new BigNumber(0).should.be.bignumber.equal(ethBalance);
});

it('should revert funds of failure inside transferFrom (with data)', async function () {
// showMessage is not payable, so it fails when called with msg.value
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approve(accounts[1], firstTokenId, { from: creator });

await this.token.transferFromAndCall(
accounts[0], secondAvatarId, this.avatars.contract.address, firstTokenId, extraData, { from: accounts[1], value: 1 }
).should.be.rejectedWith('revert');;

// transferFrom should not have gone through so approved address is still accounts[1]
const appproved = await this.token.getApproved(firstTokenId);
accounts[1].should.be.equal(appproved);

const tokenBalance = await this.token.balanceOf(secondAvatarId, this.avatars.address);
new BigNumber(0).should.be.bignumber.equal(tokenBalance);

const ethBalance = await web3.eth.getBalance(this.message.contract.address);
new BigNumber(0).should.be.bignumber.equal(ethBalance);
});

it('should return correct allowance after approve (with data) and show the event on receiver contract', async function () {
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

const transaction = await this.token.approveAndCall(
this.message.contract.address, firstTokenId, extraData, { from: creator }
);

assert.equal(2, transaction.receipt.logs.length);

const appproved = await this.token.getApproved(firstTokenId);
this.message.contract.address.should.be.equal(appproved);
});

it('should return correct balances after transferFrom (with data) and show the event on receiver contract', async function () {
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approve(accounts[1], firstTokenId, { from: accounts[0] });

const appproved = await this.token.getApproved(firstTokenId);
accounts[1].should.be.equal(appproved);

const transaction = await this.token.transferFromAndCall(
accounts[0], secondAvatarId, this.avatars.contract.address, firstTokenId, extraData, { from: accounts[1] }
);

assert.equal(3, transaction.receipt.logs.length);

const tokenBalance = await this.token.balanceOf(secondAvatarId, this.avatars.address);
new BigNumber(1).should.be.bignumber.equal(tokenBalance);
});

it('should fail inside approve (with data)', async function () {
const extraData = this.message.contract.fail.getData();

await this.token.approveAndCall(this.message.contract.address, firstTokenId, extraData)
.should.be.rejectedWith('revert');

// approval should not have gone through so approved is still ZERO_ADDRESS
const appproved = await this.token.getApproved(firstTokenId);
ZERO_ADDRESS.should.be.equal(appproved);
});

it('should fail inside transferFrom (with data)', async function () {
const extraData = this.message.contract.fail.getData();

await this.token.approve(accounts[1], firstAvatarId, { from: creator });
await this.token.transferFromAndCall(creator, secondAvatarId, this.avatars.address, firstTokenId, extraData, { from: creator })
.should.be.rejectedWith('revert');

// transferFrom should have failed so balance is still 0 but approved is accounts[1]
const appproved = await this.token.getApproved(firstTokenId);
accounts[1].should.be.equal(appproved);

const tokenBalance = await this.token.balanceOf(secondAvatarId, this.avatars.address);
new BigNumber(0).should.be.bignumber.equal(tokenBalance);
});

it('should fail approve (with data) when using token contract address as receiver', async function () {
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approveAndCall(this.token.contract.address, firstAvatarId, extraData, { from: accounts[0] })
.should.be.rejectedWith('revert');
});

it('should fail transferFrom (with data) when using token contract address as receiver', async function () {
const extraData = this.message.contract.showMessage.getData(
web3.toHex(123456), 666, 'Transfer Done'
);

await this.token.approve(accounts[1], firstTokenId, { from: accounts[0] });

await this.token.transferFromAndCall(accounts[0], thirdAvatarId, this.avatars.address, firstTokenId, extraData, { from: accounts[1] })
.should.be.rejectedWith('revert');
});
});
});
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add newline to EOF

2 changes: 1 addition & 1 deletion test/ERC721HoldingsMintBurn.behaviour.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function shouldMintAndBurnERC721HoldingsToken (accounts) {
const creator = accounts[0];
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

describe('like a mintable and burnable ERC721Token', function () {
describe('like a mintable and burnable ERC721HoldingsToken', function () {
beforeEach(async function () {
await this.avatars.mint(creator, firstAvatarId, { from: creator });
await this.avatars.mint(creator, secondAvatarId, { from: creator });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require('chai')
.use(require('chai-bignumber')(BigNumber))
.should();

export default function shouldBehaveLikeERC721HoldingsBasicToken (accounts) {
export default function shouldBehaveLikeERC721HoldingsToken (accounts) {
const firstTokenId = 1;
const secondTokenId = 2;
const unknownTokenId = 3;
Expand All @@ -20,7 +20,7 @@ export default function shouldBehaveLikeERC721HoldingsBasicToken (accounts) {
const creator = accounts[0];
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

describe('like a ERC721HoldingsBasicToken', function () {
describe('like a ERC721HoldingsToken', function () {
beforeEach(async function () {
await this.avatars.mint(creator, firstAvatarId, { from: creator });
await this.avatars.mint(creator, secondAvatarId, { from: creator });
Expand Down
Loading