Commit e96164fe by Santiago Palladino Committed by Francisco Giordano

ERC721 full implementation (#803)

* Rename current ERC721 implementation to BaseERC721

* Implement ERC721 optional & approveAll functionality

* Support for new ERC721 interface

- Tests for new features are pending
- ERC721 is abstract, since it requires metadata implementation
- Move some methods into DeprecatedERC721 contract
- Reorganise base vs full implementation
- Pending tokenByIndex

* Add more tests for ERC721

* Implement suggestions by @dekz

* Update comments in ERC721 contracts

* Implement tokensByIndex extension

- Remove restrictions from mock mint and burn calls

* Add default implementation for metadata URI

This allows token implementation to be non-abstract

* Allow operators to call approve on a token

* Remove gas stipend restriction in call to 721 receiver

* Remove deprecated implementation

We only want to keep the interface, for interacting with already deployed contracts

* Add notice to isContract helper on constract constructors

* Change natspec delimiters for consistency

* Minor linting fixes

* Add constant modifier to ERC721_RECEIVED magic value

* Use 4-params safeTransferFrom for implementing the 3-params overload

* Minor text changes in natspec comments

* Use address(0) instead of 0 or 0x0

* Use if-statements instead of boolean one-liners for clarity

:-(

* Keep ownedTokensCount state var in sync in full ERC721 implementation

* Fix incorrect comparison when burning ERC721 tokens with metadata

* Use address(0) instead of 0 in one more place in ERC721

* Throw when querying balance for the zero address

Required by the spec

* Update links to approved version of EIP721

* Use explicit size for uint

* Remove unneeded internal function in ERC721

Also rename addToken and removeToken for added clarity

* Use underscore instead of 'do' prefix for internal methods in ERC721

* Fix failing test due to events reordering in ERC721 safe transfer

* Fix bug introduced in 74db03ba06

* Remove do prefix for internal setTokenUri method

* Allow transfers to self in ERC721
parent 9f52e943
pragma solidity ^0.4.18;
/**
* Utility library of inline functions on addresses
*/
library AddressUtils {
/**
* Returns whether there is code in the target address
* @dev This function will return false if invoked during the constructor of a contract,
* as the code is not actually created until after the constructor finishes.
* @param addr address address to check
* @return whether there is code in the target address
*/
function isContract(address addr) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(addr) }
return size > 0;
}
}
pragma solidity ^0.4.18;
import "../token/ERC721/ERC721BasicToken.sol";
/**
* @title ERC721BasicTokenMock
* This mock just provides a public mint and burn functions for testing purposes
*/
contract ERC721BasicTokenMock is ERC721BasicToken {
function mint(address _to, uint256 _tokenId) public {
super._mint(_to, _tokenId);
}
function burn(uint256 _tokenId) public {
super._burn(ownerOf(_tokenId), _tokenId);
}
}
pragma solidity ^0.4.18;
import "../token/ERC721/ERC721Receiver.sol";
contract ERC721ReceiverMock is ERC721Receiver {
bytes4 retval;
bool reverts;
event Received(address _address, uint256 _tokenId, bytes _data, uint256 _gas);
function ERC721ReceiverMock(bytes4 _retval, bool _reverts) public {
retval = _retval;
reverts = _reverts;
}
function onERC721Received(address _address, uint256 _tokenId, bytes _data) public returns(bytes4) {
require(!reverts);
Received(_address, _tokenId, _data, msg.gas);
return retval;
}
}
...@@ -4,16 +4,23 @@ import "../token/ERC721/ERC721Token.sol"; ...@@ -4,16 +4,23 @@ import "../token/ERC721/ERC721Token.sol";
/** /**
* @title ERC721TokenMock * @title ERC721TokenMock
* This mock just provides a public mint and burn functions for testing purposes. * This mock just provides a public mint and burn functions for testing purposes,
* and a public setter for metadata URI
*/ */
contract ERC721TokenMock is ERC721Token { contract ERC721TokenMock is ERC721Token {
function ERC721TokenMock() ERC721Token() public { } function ERC721TokenMock(string name, string symbol) public
ERC721Token(name, symbol)
{ }
function mint(address _to, uint256 _tokenId) public { function mint(address _to, uint256 _tokenId) public {
super._mint(_to, _tokenId); super._mint(_to, _tokenId);
} }
function burn(uint256 _tokenId) public { function burn(uint256 _tokenId) public {
super._burn(_tokenId); super._burn(ownerOf(_tokenId), _tokenId);
}
function setTokenURI(uint256 _tokenId, string _uri) public {
super._setTokenURI(_tokenId, _uri);
} }
} }
\ No newline at end of file
pragma solidity ^0.4.18;
import "./ERC721.sol";
/**
* @title ERC-721 methods shipped in OpenZeppelin v1.7.0, removed in the latest version of the standard
* @dev Only use this interface for compatibility with previously deployed contracts
* @dev Use ERC721 for interacting with new contracts which are standard-compliant
*/
contract DeprecatedERC721 is ERC721 {
function takeOwnership(uint256 _tokenId) public;
function transfer(address _to, uint256 _tokenId) public;
function tokensOf(address _owner) public view returns (uint256[]);
}
pragma solidity ^0.4.18; pragma solidity ^0.4.18;
import "./ERC721Basic.sol";
/**
* @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721Enumerable is ERC721Basic {
function totalSupply() public view returns (uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view returns (uint256 _tokenId);
function tokenByIndex(uint256 _index) public view returns (uint256);
}
/** /**
* @title ERC721 interface * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
* @dev see https://github.com/ethereum/eips/issues/721 * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/ */
contract ERC721 { contract ERC721Metadata is ERC721Basic {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId); function name() public view returns (string _name);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId); function symbol() public view returns (string _symbol);
function tokenURI(uint256 _tokenId) public view returns (string);
}
function balanceOf(address _owner) public view returns (uint256 _balance); /**
function ownerOf(uint256 _tokenId) public view returns (address _owner); * @title ERC-721 Non-Fungible Token Standard, full implementation interface
function transfer(address _to, uint256 _tokenId) public; * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
function approve(address _to, uint256 _tokenId) public; */
function takeOwnership(uint256 _tokenId) public; contract ERC721 is ERC721Basic, ERC721Enumerable, ERC721Metadata {
} }
pragma solidity ^0.4.18;
/**
* @title ERC721 Non-Fungible Token Standard basic interface
* @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
*/
contract ERC721Basic {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function exists(uint256 _tokenId) public view returns (bool _exists);
function approve(address _to, uint256 _tokenId) public;
function getApproved(uint256 _tokenId) public view returns (address _operator);
function setApprovalForAll(address _operator, bool _approved) public;
function isApprovedForAll(address _owner, address _operator) public view returns (bool);
function transferFrom(address _from, address _to, uint256 _tokenId) public;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) public;
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes _data) public;
}
pragma solidity ^0.4.18;
import "./ERC721Receiver.sol";
contract ERC721Holder is ERC721Receiver {
function onERC721Received(address, uint256, bytes) public returns(bytes4) {
return ERC721_RECEIVED;
}
}
pragma solidity ^0.4.18;
/**
* @title ERC721 token receiver interface
* @dev Interface for any contract that wants to support safeTransfers
* from ERC721 asset contracts.
*/
contract ERC721Receiver {
/**
* @dev Magic value to be returned upon successful reception of an NFT
* Equals to `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`,
* which can be also obtained as `ERC721Receiver(0).onERC721Received.selector`
*/
bytes4 constant ERC721_RECEIVED = 0xf0b9e5ba;
/**
* @notice Handle the receipt of an NFT
* @dev The ERC721 smart contract calls this function on the recipient
* after a `safetransfer`. This function MAY throw to revert and reject the
* transfer. This function MUST use 50,000 gas or less. Return of other
* than the magic value MUST result in the transaction being reverted.
* Note: the contract address is always the message sender.
* @param _from The sending address
* @param _tokenId The NFT identifier which is being transfered
* @param _data Additional data with no specified format
* @return `bytes4(keccak256("onERC721Received(address,uint256,bytes)"))`
*/
function onERC721Received(address _from, uint256 _tokenId, bytes _data) public returns(bytes4);
}
const _ = require('lodash');
const ethjsABI = require('ethjs-abi');
export function findMethod (abi, name, args) {
for (var i = 0; i < abi.length; i++) {
const methodArgs = _.map(abi[i].inputs, 'type').join(',');
if ((abi[i].name === name) && (methodArgs === args)) {
return abi[i];
}
}
}
export default function sendTransaction (target, name, argsTypes, argsValues, opts) {
const abiMethod = findMethod(target.abi, name, argsTypes);
const encodedData = ethjsABI.encodeMethod(abiMethod, argsValues);
return target.sendTransaction(Object.assign({ data: encodedData }, opts));
}
import shouldBehaveLikeERC721BasicToken from './ERC721BasicToken.behaviour';
import shouldMintAndBurnERC721Token from './ERC721MintBurn.behaviour';
const BigNumber = web3.BigNumber;
const ERC721BasicToken = artifacts.require('ERC721BasicTokenMock.sol');
require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();
contract('ERC721BasicToken', function (accounts) {
beforeEach(async function () {
this.token = await ERC721BasicToken.new({ from: accounts[0] });
});
shouldBehaveLikeERC721BasicToken(accounts);
shouldMintAndBurnERC721Token(accounts);
});
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 shouldMintAndBurnERC721Token (accounts) {
const firstTokenId = 1;
const secondTokenId = 2;
const unknownTokenId = 3;
const creator = accounts[0];
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
describe('like a mintable and burnable ERC721Token', function () {
beforeEach(async function () {
await this.token.mint(creator, firstTokenId, { from: creator });
await this.token.mint(creator, secondTokenId, { from: creator });
});
describe('mint', function () {
const to = accounts[1];
const tokenId = unknownTokenId;
let logs = null;
describe('when successful', function () {
beforeEach(async function () {
const result = await this.token.mint(to, tokenId);
logs = result.logs;
});
it('assigns the token to the new owner', async function () {
const owner = await this.token.ownerOf(tokenId);
owner.should.be.equal(to);
});
it('increases the balance of its owner', async function () {
const balance = await this.token.balanceOf(to);
balance.should.be.bignumber.equal(1);
});
it('emits a transfer event', async function () {
logs.length.should.be.equal(1);
logs[0].event.should.be.eq('Transfer');
logs[0].args._from.should.be.equal(ZERO_ADDRESS);
logs[0].args._to.should.be.equal(to);
logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
});
});
describe('when the given owner address is the zero address', function () {
it('reverts', async function () {
await assertRevert(this.token.mint(ZERO_ADDRESS, tokenId));
});
});
describe('when the given token ID was already tracked by this contract', function () {
it('reverts', async function () {
await assertRevert(this.token.mint(accounts[1], firstTokenId));
});
});
});
describe('burn', function () {
const tokenId = firstTokenId;
const sender = creator;
let logs = null;
describe('when successful', function () {
beforeEach(async function () {
const result = await this.token.burn(tokenId, { from: sender });
logs = result.logs;
});
it('burns the given token ID and adjusts the balance of the owner', async function () {
await assertRevert(this.token.ownerOf(tokenId));
const balance = await this.token.balanceOf(sender);
balance.should.be.bignumber.equal(1);
});
it('emits a burn event', async function () {
logs.length.should.be.equal(1);
logs[0].event.should.be.eq('Transfer');
logs[0].args._from.should.be.equal(sender);
logs[0].args._to.should.be.equal(ZERO_ADDRESS);
logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
});
});
describe('when there is a previous approval', function () {
beforeEach(async function () {
await this.token.approve(accounts[1], tokenId, { from: sender });
const result = await this.token.burn(tokenId, { from: sender });
logs = result.logs;
});
it('clears the approval', async function () {
const approvedAccount = await this.token.getApproved(tokenId);
approvedAccount.should.be.equal(ZERO_ADDRESS);
});
it('emits an approval event', async function () {
logs.length.should.be.equal(2);
logs[0].event.should.be.eq('Approval');
logs[0].args._owner.should.be.equal(sender);
logs[0].args._approved.should.be.equal(ZERO_ADDRESS);
logs[0].args._tokenId.should.be.bignumber.equal(tokenId);
logs[1].event.should.be.eq('Transfer');
});
});
describe('when the given token ID was not tracked by this contract', function () {
it('reverts', async function () {
await assertRevert(this.token.burn(unknownTokenId, { from: creator }));
});
});
});
});
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment