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

Initial ERC1155 implementation with some tests #1803

Merged
merged 19 commits into from Nov 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions contracts/mocks/ERC1155Mock.sol
@@ -0,0 +1,33 @@
pragma solidity ^0.5.0;

import "../token/ERC1155/ERC1155.sol";

/**
* @title ERC1155Mock
* This mock just publicizes internal functions for testing purposes
*/
contract ERC1155Mock is ERC1155 {
function mint(address to, uint256 id, uint256 value, bytes memory data) public {
_mint(to, id, value, data);
}

function mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) public {
_mintBatch(to, ids, values, data);
}

function burn(address owner, uint256 id, uint256 value) public {
_burn(owner, id, value);
}

function burnBatch(address owner, uint256[] memory ids, uint256[] memory values) public {
_burnBatch(owner, ids, values);
}

function doSafeTransferAcceptanceCheck(address operator, address from, address to, uint256 id, uint256 value, bytes memory data) public {
_doSafeTransferAcceptanceCheck(operator, from, to, id, value, data);
}

function doSafeBatchTransferAcceptanceCheck(address operator, address from, address to, uint256[] memory ids, uint256[] memory values, bytes memory data) public {
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, values, data);
}
}
58 changes: 58 additions & 0 deletions contracts/mocks/ERC1155ReceiverMock.sol
@@ -0,0 +1,58 @@
pragma solidity ^0.5.0;

import "../token/ERC1155/IERC1155Receiver.sol";
import "./ERC165Mock.sol";

contract ERC1155ReceiverMock is IERC1155Receiver, ERC165Mock {
bytes4 private _recRetval;
bool private _recReverts;
bytes4 private _batRetval;
bool private _batReverts;

event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas);
event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas);

constructor (
bytes4 recRetval,
bool recReverts,
bytes4 batRetval,
bool batReverts
)
public
{
_recRetval = recRetval;
_recReverts = recReverts;
_batRetval = batRetval;
_batReverts = batReverts;
}

function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
)
external
returns(bytes4)
{
require(!_recReverts, "ERC1155ReceiverMock: reverting on receive");
emit Received(operator, from, id, value, data, gasleft());
return _recRetval;
}

function onERC1155BatchReceived(
address operator,
address from,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
)
external
returns(bytes4)
{
require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive");
emit BatchReceived(operator, from, ids, values, data, gasleft());
return _batRetval;
}
}
295 changes: 295 additions & 0 deletions contracts/token/ERC1155/ERC1155.sol
@@ -0,0 +1,295 @@
pragma solidity ^0.5.0;

import "./IERC1155.sol";
import "./IERC1155Receiver.sol";
import "../../math/SafeMath.sol";
import "../../utils/Address.sol";
import "../../introspection/ERC165.sol";

/**
* @title Standard ERC1155 token
*
* @dev Implementation of the basic standard multi-token.
* See https://eips.ethereum.org/EIPS/eip-1155
* Originally based on code by Enjin: https://github.com/enjin/erc-1155
*/
contract ERC1155 is ERC165, IERC1155
nventuro marked this conversation as resolved.
Show resolved Hide resolved
nventuro marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Some of the contracts are missing, such as the one for the metadata and the one for the URI functions. Are those going to be implemented in another stage?

Copy link
Contributor Author

@cag cag Oct 4, 2019

Choose a reason for hiding this comment

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

I did not seek to implement those contracts, since we don't know how these features really should work because we don't use them, and didn't plan on using them. These should be added by somebody with more knowledge/use for those features.

{
using SafeMath for uint256;
using Address for address;

// Mapping from token ID to account balances
mapping (uint256 => mapping(address => uint256)) private _balances;

// Mapping from account to operator approvals
mapping (address => mapping(address => bool)) private _operatorApprovals;

constructor()
public
{
_registerInterface(
nventuro marked this conversation as resolved.
Show resolved Hide resolved
ERC1155(0).safeTransferFrom.selector ^
ERC1155(0).safeBatchTransferFrom.selector ^
ERC1155(0).balanceOf.selector ^
ERC1155(0).balanceOfBatch.selector ^
ERC1155(0).setApprovalForAll.selector ^
ERC1155(0).isApprovedForAll.selector
);
}

/**
@dev Get the specified address' balance for token with specified ID.

Attempting to query the zero account for a balance will result in a revert.

@param account The address of the token holder
@param id ID of the token
@return The account's balance of the token type requested
*/
function balanceOf(address account, uint256 id) public view returns (uint256) {
require(account != address(0), "ERC1155: balance query for the zero address");
return _balances[id][account];
}

/**
@dev Get the balance of multiple account/token pairs.

If any of the query accounts is the zero account, this query will revert.

@param accounts The addresses of the token holders
@param ids IDs of the tokens
@return Balances for each account and token id pair
*/
function balanceOfBatch(
address[] memory accounts,
uint256[] memory ids
)
public
view
returns (uint256[] memory)
{
require(accounts.length == ids.length, "ERC1155: accounts and IDs must have same lengths");

uint256[] memory batchBalances = new uint256[](accounts.length);

for (uint256 i = 0; i < accounts.length; ++i) {
require(accounts[i] != address(0), "ERC1155: some address in batch balance query is zero");
batchBalances[i] = _balances[ids[i]][accounts[i]];
}

return batchBalances;
}

/**
* @dev Sets or unsets the approval of a given operator.
*
* An operator is allowed to transfer all tokens of the sender on their behalf.
*
* Because an account already has operator privileges for itself, this function will revert
* if the account attempts to set the approval status for itself.
*
* @param operator address to set the approval
* @param approved representing the status of the approval to be set
*/
function setApprovalForAll(address operator, bool approved) external {
cag marked this conversation as resolved.
Show resolved Hide resolved
require(msg.sender != operator, "ERC1155: cannot set approval status for self");
_operatorApprovals[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}

/**
@notice Queries the approval status of an operator for a given account.
@param account The account of the Tokens
@param operator Address of authorized operator
@return True if the operator is approved, false if not
*/
function isApprovedForAll(address account, address operator) public view returns (bool) {
return _operatorApprovals[account][operator];
}

/**
@dev Transfers `value` amount of an `id` from the `from` address to the `to` address specified.
Caller must be approved to manage the tokens being transferred out of the `from` account.
If `to` is a smart contract, will call `onERC1155Received` on `to` and act appropriately.
@param from Source address
@param to Target address
@param id ID of the token type
@param value Transfer amount
@param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
*/
function safeTransferFrom(
cag marked this conversation as resolved.
Show resolved Hide resolved
address from,
address to,
uint256 id,
uint256 value,
bytes calldata data
)
external
{
require(to != address(0), "ERC1155: target address must be non-zero");
require(
from == msg.sender || isApprovedForAll(from, msg.sender) == true,
"ERC1155: need operator approval for 3rd party transfers"
);

_balances[id][from] = _balances[id][from].sub(value, "ERC1155: insufficient balance for transfer");
_balances[id][to] = _balances[id][to].add(value);

emit TransferSingle(msg.sender, from, to, id, value);

_doSafeTransferAcceptanceCheck(msg.sender, from, to, id, value, data);
}

/**
@dev Transfers `values` amount(s) of `ids` from the `from` address to the
`to` address specified. Caller must be approved to manage the tokens being
transferred out of the `from` account. If `to` is a smart contract, will
call `onERC1155BatchReceived` on `to` and act appropriately.
@param from Source address
@param to Target address
@param ids IDs of each token type
@param values Transfer amounts per token type
@param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
*/
function safeBatchTransferFrom(
address from,
address to,
uint256[] calldata ids,
uint256[] calldata values,
bytes calldata data
)
external
{
require(ids.length == values.length, "ERC1155: IDs and values must have same lengths");
require(to != address(0), "ERC1155: target address must be non-zero");
require(
from == msg.sender || isApprovedForAll(from, msg.sender) == true,
"ERC1155: need operator approval for 3rd party transfers"
);

for (uint256 i = 0; i < ids.length; ++i) {
uint256 id = ids[i];
uint256 value = values[i];

_balances[id][from] = _balances[id][from].sub(
value,
"ERC1155: insufficient balance of some token type for transfer"
);
_balances[id][to] = _balances[id][to].add(value);
}

emit TransferBatch(msg.sender, from, to, ids, values);

_doSafeBatchTransferAcceptanceCheck(msg.sender, from, to, ids, values, data);
}

/**
* @dev Internal function to mint an amount of a token with the given ID
* @param to The address that will own the minted token
* @param id ID of the token to be minted
* @param value Amount of the token to be minted
* @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
*/
function _mint(address to, uint256 id, uint256 value, bytes memory data) internal {
cag marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

There isn't a method to broadcast a new token id creation. The EIP states (Minting/creating and burning/destroying rules:): "To broadcast the existence of a token ID with no initial balance, the contract SHOULD emit the TransferSingle event from 0x0 to 0x0, with the token creator as _operator, and a _value of 0" but the transfer and _mint functions don't allow to send tokens to the zero address, therefore it can not be triggered that event with those parameters.

Consider adding a new function for creating a new type of token. Take into account that if it is used a _broadcast function to let the rest know that a new id has been created, then the function should check into the storage - either a totalSupply or a idExists mapping - to see if that id was already created. Otherwise, the creator could trigger several TransferSingle events with the 'new-id-created parameters'.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If possible, I'd like to punt on this, because this will require additional storage in the contract, and if implemented, probably should prevent the use of counterfactual IDs (as you've mentioned below via reverting transactions on unminted IDs).

Since we (Gnosis) actually need to use counterfactual IDs in our implementation, we have not implemented this.

I can see how this may be useful for "game item" scenarios though: something that this standard was originally designed for, but because we don't want to exclude our own use case, I think this functionality would fit better in an derived contract.

require(to != address(0), "ERC1155: mint to the zero address");

_balances[id][to] = _balances[id][to].add(value);
emit TransferSingle(msg.sender, address(0), to, id, value);

_doSafeTransferAcceptanceCheck(msg.sender, address(0), to, id, value, data);
}

/**
* @dev Internal function to batch mint amounts of tokens with the given IDs
* @param to The address that will own the minted token
* @param ids IDs of the tokens to be minted
* @param values Amounts of the tokens to be minted
* @param data Data forwarded to `onERC1155Received` if `to` is a contract receiver
*/
function _mintBatch(address to, uint256[] memory ids, uint256[] memory values, bytes memory data) internal {
require(to != address(0), "ERC1155: batch mint to the zero address");
require(ids.length == values.length, "ERC1155: minted IDs and values must have same lengths");

for(uint i = 0; i < ids.length; i++) {
_balances[ids[i]][to] = values[i].add(_balances[ids[i]][to]);
}

emit TransferBatch(msg.sender, address(0), to, ids, values);

_doSafeBatchTransferAcceptanceCheck(msg.sender, address(0), to, ids, values, data);
}

/**
* @dev Internal function to burn an amount of a token with the given ID
* @param account Account which owns the token to be burnt
* @param id ID of the token to be burnt
* @param value Amount of the token to be burnt
*/
function _burn(address account, uint256 id, uint256 value) internal {
require(account != address(0), "ERC1155: attempting to burn tokens on zero account");

_balances[id][account] = _balances[id][account].sub(
value,
"ERC1155: attempting to burn more than balance"
);
emit TransferSingle(msg.sender, account, address(0), id, value);
}

/**
* @dev Internal function to batch burn an amounts of tokens with the given IDs
* @param account Account which owns the token to be burnt
* @param ids IDs of the tokens to be burnt
* @param values Amounts of the tokens to be burnt
*/
function _burnBatch(address account, uint256[] memory ids, uint256[] memory values) internal {
require(account != address(0), "ERC1155: attempting to burn batch of tokens on zero account");
require(ids.length == values.length, "ERC1155: burnt IDs and values must have same lengths");

for(uint i = 0; i < ids.length; i++) {
_balances[ids[i]][account] = _balances[ids[i]][account].sub(
values[i],
"ERC1155: attempting to burn more than balance for some token"
);
}

emit TransferBatch(msg.sender, account, address(0), ids, values);
}

function _doSafeTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256 id,
uint256 value,
bytes memory data
)
internal
{
if(to.isContract()) {
require(
IERC1155Receiver(to).onERC1155Received(operator, from, id, value, data) ==
IERC1155Receiver(to).onERC1155Received.selector,
"ERC1155: got unknown value from onERC1155Received"
);
}
}

function _doSafeBatchTransferAcceptanceCheck(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory values,
bytes memory data
)
internal
{
if(to.isContract()) {
require(
IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, values, data) ==
IERC1155Receiver(to).onERC1155BatchReceived.selector,
"ERC1155: got unknown value from onERC1155BatchReceived"
);
}
}
}