diff --git a/contracts/status/sticker-market/Sticker.sol b/contracts/status/sticker-market/Sticker.sol new file mode 100644 index 0000000..5afee00 --- /dev/null +++ b/contracts/status/sticker-market/Sticker.sol @@ -0,0 +1,23 @@ +pragma solidity >=0.5.0 <0.6.0; + +import "../../common/Controlled.sol"; +import "../../token/UnfungibleToken.sol"; + + +contract Sticker is Controlled, UnfungibleToken { + + uint256 public nextId; + mapping (uint256 => bytes32) public dataHash; + + function generateToken(address _owner, bytes32 _dataHash) external onlyController { + tokenId = nextId++; + dataHash[tokenId] = _dataHash; + _mint(_owner, tokenId); + } + + function destroyToken(address _owner, uint256 _tokenId) external onlyController { + delete dataHash[_tokenId]; + _burn(_owner, _tokenId); + } + +} diff --git a/contracts/status/sticker-market/StickerMarket.sol b/contracts/status/sticker-market/StickerMarket.sol new file mode 100644 index 0000000..a29eb9c --- /dev/null +++ b/contracts/status/sticker-market/StickerMarket.sol @@ -0,0 +1,114 @@ +pragma solidity >=0.5.0 <0.6.0; + +import "./StickerPack.sol"; +import "../../token/ERC20Token.sol"; +import "../../token/ApproveAndCallFallBack.sol"; +import "../../common/Controlled.sol"; + +/** + * @dev + */ +contract StickerMarket is Controlled, ApproveAndCallFallBack { + event Register(uint256 packId, bytes32 dataHash, uint256 dataPrice); + event Unregister(uint256 packId); + event ClaimedTokens(address indexed _token, address indexed _controller, uint256 _amount); + + struct Pack { + bytes32 dataHash; // merkle tree root of "ipfs://stickerdata-json" + uint256 price; + address owner; + } + StickerPack public stickerPack = new StickerPack(); + ERC20Token public snt; + mapping(uint256 => Pack) public packs; + uint256 public nextId; + + //TODO: add list of packs available + /** + * @notice Constructor + * @param _owner Authority address + * @param _snt SNT token + */ + constructor( + ERC20Token _snt, + uint256 _registerPrice + ) + public + { + snt = _snt; + } + + function buy(uint256 _packId) external { + _buy(msg.sender, packs[_packId]); + } + + function receiveApproval(address _from, uint256 _amount, address _token, bytes calldata _data) external { + require(_token == address(snt), "Bad token"); + require(_token == address(msg.sender), "Bad call"); + require(_data.length == 36, "Bad data length"); + uint256 packId = abiDecodeBuy(_data); + Pack memory pack = packs[packId]; + require(pack.price == _amount, "Bad amount"); + _buy(_from, pack); + } + + function register(bytes32 _dataHash, uint256 _price, address _owner) external onlyController { + uint256 id = nextId++; + packs[id] = Pack(_dataHash, _price, _owner); + emit Register(id, _datahash, _price); + } + + function unregister(uint256 _packId) external onlyController { + delete packs[_packId]; + emit Unregister(id); + } + + function migrateMarket(address _newMarket) external onlyController { + stickerPack.changeController(_newMarket); + } + + /** + * @notice This method can be used by the controller to extract mistakenly + * sent tokens to this contract. + * @param _token The address of the token contract that you want to recover + * set to 0 in case you want to extract ether. + */ + function claimTokens(address _token) external onlyController { + if (_token == address(0)) { + address(controller).transfer(address(this).balance); + return; + } + ERC20Token token = ERC20Token(_token); + uint256 balance = token.balanceOf(address(this)); + token.transfer(controller, balance); + emit ClaimedTokens(_token, controller, balance); + } + + function _buy(address _buyer, Pack memory _pack) internal { + require(_pack.dataHash != bytes32(0), "Bad pack"); + require(snt.transferFrom(_from, _pack.owner, _pack.price), "Bad payment"); + stickerPack.generateToken(_from, pack.dataHash); + } + + /** + * @dev Decodes abi encoded data with selector for "buy(uint256)". + * @param _data Abi encoded data. + * @return Decoded registry call. + */ + function abiDecodeBuy( + bytes _data + ) + private + pure + returns( + uint256 packId + ) + { + bytes4 sig; + assembly { + sig := mload(add(_data, add(0x20, 0))) + packId := mload(add(_data, 36)) + } + require(sig == bytes4(keccak256("buy(uint256)")), "Bad method sig"); + } +} \ No newline at end of file diff --git a/contracts/status/sticker-market/StickerPack.sol b/contracts/status/sticker-market/StickerPack.sol new file mode 100644 index 0000000..c5dcfd6 --- /dev/null +++ b/contracts/status/sticker-market/StickerPack.sol @@ -0,0 +1,47 @@ +pragma solidity >=0.5.0 <0.6.0; + +import "../../common/Controlled.sol"; +import "../../token/UnfungibleToken.sol"; +import "./Sticker.sol"; + + +contract StickerPack is Controlled, UnfungibleToken { + + Sticker public sticker = new Sticker(); + uint256 public nextId; + mapping(uint256 => bytes32) public dataHash; + mapping(bytes32 => bool) public unpacked; + + function unpack(uint256 _tokenId, bytes32[] memory _proof, bytes32 _stickerData) external { + address owner = ownerOf(_tokenId); + require(owner == msg.sender, "Unauthorized"); + require(MerkleProof.verifyProof(_proof, dataHash[_tokenId], _stickerData), "Invalid proof"); + bytes32 unpackedHash = keccak256(abi.encodePacked(_tokenId, _stickerData)); + require(!unpacked[unpackedHash], "Duplicate unpacking"); + unpacked[unpackedHash] = true; + sticker.generateToken(owner, _stickerData); + + } + + function repack(uint256 _packId, uint256 _stickerToken) external { + address owner = ownerOf(_packToken); + require(owner == ownerOf(_stickerToken) && owner == msg.sender, "Unauthorized"); + bytes32 stickerData = sticker.dataHash(_stickerToken); + bytes32 unpackedHash = keccak256(abi.encodePacked(_packId, stickerData)); + require(unpacked[unpackedHash],"Bad operation"); + delete unpacked[unpackedHash]; + sticker.destroyToken(owner, _tokenId2); + } + + function generateToken(address _owner, bytes32 _dataHash) external onlyController { + tokenId = nextId++; + dataHash[tokenId] = _dataHash; + _mint(_owner, tokenId); + } + + function isStickerInPack(uint256 _packId, bytes32[] _proof, bytes32 _stickerData) public view returns (bool){ + require(MerkleProof.verifyProof(_proof, dataHash[_tokenId], _stickerData), "Invalid proof"); + bytes32 unpackedHash = keccak256(abi.encodePacked(_tokenId, _stickerData)); + return !unpacked[unpackedHash]; + } +} diff --git a/contracts/token/ApproveAndCallFallBack.sol b/contracts/token/ApproveAndCallFallBack.sol index 0c83ccb..e2ea1da 100644 --- a/contracts/token/ApproveAndCallFallBack.sol +++ b/contracts/token/ApproveAndCallFallBack.sol @@ -1,5 +1,5 @@ pragma solidity >=0.5.0 <0.6.0; -contract ApproveAndCallFallBack { - function receiveApproval(address from, uint256 _amount, address _token, bytes memory _data) public; +interface ApproveAndCallFallBack { + function receiveApproval(address from, uint256 _amount, address _token, bytes calldata _data) external; } diff --git a/contracts/token/ERC721.sol b/contracts/token/ERC721.sol new file mode 100644 index 0000000..f390f59 --- /dev/null +++ b/contracts/token/ERC721.sol @@ -0,0 +1,139 @@ +pragma solidity >=0.5.0 <0.6.0; + +/** + * @title ERC-721 Non-Fungible Token Standard + * @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + * Note: the ERC-165 identifier for this interface is 0x80ac58cd + */ +interface ERC721 /* is ERC165 */ { + /** + * @dev This emits when ownership of any NFT changes by any mechanism. + * This event emits when NFTs are created (`from` == 0) and destroyed + * (`to` == 0). Exception: during contract creation, any number of NFTs + * may be created and assigned without emitting Transfer. At the time of + * any transfer, the approved address for that NFT (if any) is reset to none. + */ + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + + /** + * @dev This emits when the approved address for an NFT is changed or + * reaffirmed. The zero address indicates there is no approved address. + * When a Transfer event emits, this also indicates that the approved + * address for that NFT (if any) is reset to none. + */ + event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId); + + /** + * @dev This emits when an operator is enabled or disabled for an owner. + * The operator can manage all NFTs of the owner. + */ + event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); + + /** + * @notice Count all NFTs assigned to an owner + * @dev NFTs assigned to the zero address are considered invalid, and this + * function throws for queries about the zero address. + * @param _owner An address for whom to query the balance + * @return The number of NFTs owned by `_owner`, possibly zero + */ + function balanceOf(address _owner) external view returns (uint256); + + /** + * @notice Find the owner of an NFT + * @dev NFTs assigned to zero address are considered invalid, and queries + * about them do throw. + * @param _tokenId The identifier for an NFT + * @return The address of the owner of the NFT + */ + function ownerOf(uint256 _tokenId) external view returns (address); + + /** + * @notice Transfers the ownership of an NFT from one address to another address + * @dev Throws unless `msg.sender` is the current owner, an authorized + * operator, or the approved address for this NFT. Throws if `_from` is + * not the current owner. Throws if `_to` is the zero address. Throws if + * `_tokenId` is not a valid NFT. When transfer is complete, this function + * checks if `_to` is a smart contract (code size > 0). If so, it calls + * `onERC721Received` on `_to` and throws if the return value is not + * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. + * @param _from The current owner of the NFT + * @param _to The new owner + * @param _tokenId The NFT to transfer + * @param data Additional data with no specified format, sent in call to `_to` + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) external payable; + + /** + * @notice Transfers the ownership of an NFT from one address to another address + * @dev This works identically to the other function with an extra data parameter, + * except this function just sets data to "" + * @param _from The current owner of the NFT + * @param _to The new owner + * @param _tokenId The NFT to transfer + */ + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; + + /** + * @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE + * TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE + * THEY MAY BE PERMANENTLY LOST + * @dev Throws unless `msg.sender` is the current owner, an authorized + * operator, or the approved address for this NFT. Throws if `_from` is + * not the current owner. Throws if `_to` is the zero address. Throws if + * `_tokenId` is not a valid NFT. + * @param _from The current owner of the NFT + * @param _to The new owner + * @param _tokenId The NFT to transfer + */ + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; + + /** + * @notice Set or reaffirm the approved address for an NFT + * @dev The zero address indicates there is no approved address. + * @dev Throws unless `msg.sender` is the current NFT owner, or an authorized + * operator of the current owner. + * @param _approved The new approved NFT controller + * @param _tokenId The NFT to approve + */ + function approve(address _approved, uint256 _tokenId) external payable; + + /** + * @notice Enable or disable approval for a third party ("operator") to manage + * all of `msg.sender`'s assets. + * @dev Emits the ApprovalForAll event. The contract MUST allow + * multiple operators per owner. + * @param _operator Address to add to the set of authorized operators. + * @param _approved True if the operator is approved, false to revoke approval + */ + function setApprovalForAll(address _operator, bool _approved) external; + + /** + * @notice Get the approved address for a single NFT + * @dev Throws if `_tokenId` is not a valid NFT + * @param _tokenId The NFT to find the approved address for + * @return The approved address for this NFT, or the zero address if there is none + */ + function getApproved(uint256 _tokenId) external view returns (address); + + /** + * @notice Query if an address is an authorized operator for another address + * @param _owner The address that owns the NFTs + * @param _operator The address that acts on behalf of the owner + * @return True if `_operator` is an approved operator for `_owner`, false otherwise + */ + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} + + + +interface ERC165 { + /** + * @notice Query if a contract implements an interface + * @param interfaceID The interface identifier, as specified in ERC-165 + * @dev Interface identification is specified in ERC-165. This function + * uses less than 30,000 gas. + * @return `true` if the contract implements `interfaceID` and + * `interfaceID` is not 0xffffffff, `false` otherwise + */ + function supportsInterface(bytes4 interfaceID) external view returns (bool); +} diff --git a/contracts/token/ERC721Receiver.sol b/contracts/token/ERC721Receiver.sol new file mode 100644 index 0000000..656a0ec --- /dev/null +++ b/contracts/token/ERC721Receiver.sol @@ -0,0 +1,19 @@ + +pragma solidity >=0.5.0 <0.6.0; + +interface ERC721TokenReceiver { + /** + * @notice Handle the receipt of an NFT + * @dev The ERC721 smart contract calls this function on the + * recipient after a `transfer`. This function MAY throw to revert and reject the transfer. Return + * of other than the magic value MUST result in the transaction being reverted. + * @notice The contract address is always the message sender. + * @param _operator The address which called `safeTransferFrom` function + * @param _from The address which previously owned the token + * @param _tokenId The NFT identifier which is being transferred + * @param _data Additional data with no specified format + * @return `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + * unless throwing + */ + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); +} \ No newline at end of file diff --git a/contracts/token/UnfungibleToken.sol b/contracts/token/UnfungibleToken.sol new file mode 100644 index 0000000..676fc56 --- /dev/null +++ b/contracts/token/UnfungibleToken.sol @@ -0,0 +1,286 @@ +pragma solidity >=0.5.0 <0.6.0; + +import "./ERC721.sol"; +import "./ERC721Receiver.sol"; +import "../common/SafeMath.sol"; + +/** + * @title ERC721 Non-Fungible Token Standard basic implementation + * @dev see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md + */ +contract UnfungibleToken is ERC165, ERC721 { + using SafeMath for uint256; + + + // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))` + // which can be also obtained as `IERC721Receiver(0).onERC721Received.selector` + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + // Mapping from token ID to owner + mapping (uint256 => address) private _tokenOwner; + + // Mapping from token ID to approved address + mapping (uint256 => address) private _tokenApprovals; + + // Mapping from owner to number of owned token + mapping (address => uint256) private _ownedTokensCount; + + // Mapping from owner to operator approvals + mapping (address => mapping (address => bool)) private _operatorApprovals; + + bytes4 private constant _InterfaceId_ERC721 = 0x80ac58cd; + /* + * 0x80ac58cd === + * bytes4(keccak256('balanceOf(address)')) ^ + * bytes4(keccak256('ownerOf(uint256)')) ^ + * bytes4(keccak256('approve(address,uint256)')) ^ + * bytes4(keccak256('getApproved(uint256)')) ^ + * bytes4(keccak256('setApprovalForAll(address,bool)')) ^ + * bytes4(keccak256('isApprovedForAll(address,address)')) ^ + * bytes4(keccak256('transferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256)')) ^ + * bytes4(keccak256('safeTransferFrom(address,address,uint256,bytes)')) + */ + + constructor () public { + // register the supported interfaces to conform to ERC721 via ERC165 + _registerInterface(_InterfaceId_ERC721); + } + + /** + * @dev Gets the balance of the specified address + * @param owner address to query the balance of + * @return uint256 representing the amount owned by the passed address + */ + function balanceOf(address owner) public view returns (uint256) { + require(owner != address(0), "Invalid address"); + return _ownedTokensCount[owner]; + } + + /** + * @dev Gets the owner of the specified token ID + * @param tokenId uint256 ID of the token to query the owner of + * @return owner address currently marked as the owner of the given token ID + */ + function ownerOf(uint256 tokenId) public view returns (address) { + address owner = _tokenOwner[tokenId]; + require(owner != address(0), "Invalid address"); + return owner; + } + + /** + * @dev Approves another address to transfer the given token ID + * The zero address indicates there is no approved address. + * There can only be one approved address per token at a given time. + * Can only be called by the token owner or an approved operator. + * @param to address to be approved for the given token ID + * @param tokenId uint256 ID of the token to be approved + */ + function approve(address to, uint256 tokenId) public { + address owner = ownerOf(tokenId); + require(to != owner, "Bad operation"); + require(msg.sender == owner || isApprovedForAll(owner, msg.sender), "Unauthorized"); + + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @dev Gets the approved address for a token ID, or zero if no address set + * Reverts if the token ID does not exist. + * @param tokenId uint256 ID of the token to query the approval of + * @return address currently approved for the given token ID + */ + function getApproved(uint256 tokenId) public view returns (address) { + require(_exists(tokenId), "Bad token id"); + return _tokenApprovals[tokenId]; + } + + /** + * @dev Sets or unsets the approval of a given operator + * An operator is allowed to transfer all tokens of the sender on their behalf + * @param to operator address to set the approval + * @param approved representing the status of the approval to be set + */ + function setApprovalForAll(address to, bool approved) public { + require(to != msg.sender, "Bad operation"); + _operatorApprovals[msg.sender][to] = approved; + emit ApprovalForAll(msg.sender, to, approved); + } + + /** + * @dev Tells whether an operator is approved by a given owner + * @param owner owner address which you want to query the approval of + * @param operator operator address which you want to query the approval of + * @return bool whether the given operator is approved by the given owner + */ + function isApprovedForAll(address owner, address operator) public view returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @dev Transfers the ownership of a given token ID to another address + * Usage of this method is discouraged, use `safeTransferFrom` whenever possible + * Requires the msg sender to be the owner, approved, or operator + * @param from current owner of the token + * @param to address to receive the ownership of the given token ID + * @param tokenId uint256 ID of the token to be transferred + */ + function transferFrom(address from, address to, uint256 tokenId) public { + require(_isApprovedOrOwner(msg.sender, tokenId), "Unauthorized"); + + _transferFrom(from, to, tokenId); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * + * Requires the msg sender to be the owner, approved, or operator + * @param from current owner of the token + * @param to address to receive the ownership of the given token ID + * @param tokenId uint256 ID of the token to be transferred + */ + function safeTransferFrom(address from, address to, uint256 tokenId) public { + // solium-disable-next-line arg-overflow + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @dev Safely transfers the ownership of a given token ID to another address + * If the target address is a contract, it must implement `onERC721Received`, + * which is called upon a safe transfer, and return the magic value + * `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`; otherwise, + * the transfer is reverted. + * Requires the msg sender to be the owner, approved, or operator + * @param from current owner of the token + * @param to address to receive the ownership of the given token ID + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes data to send along with a safe transfer check + */ + function safeTransferFrom(address from, address to, uint256 tokenId, bytes _data) public { + transferFrom(from, to, tokenId); + // solium-disable-next-line arg-overflow + require(_checkOnERC721Received(from, to, tokenId, _data), "Unauthorized"); + } + + /** + * @dev Returns whether the specified token exists + * @param tokenId uint256 ID of the token to query the existence of + * @return whether the token exists + */ + function _exists(uint256 tokenId) internal view returns (bool) { + address owner = _tokenOwner[tokenId]; + return owner != address(0); + } + + /** + * @dev Returns whether the given spender can transfer a given token ID + * @param spender address of the spender to query + * @param tokenId uint256 ID of the token to be transferred + * @return bool whether the msg.sender is approved for the given token ID, + * is an operator of the owner, or is the owner of the token + */ + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + address owner = ownerOf(tokenId); + // Disable solium check because of + // https://github.com/duaraghav8/Solium/issues/175 + // solium-disable-next-line operator-whitespace + return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); + } + + /** + * @dev Internal function to mint a new token + * Reverts if the given token ID already exists + * @param to The address that will own the minted token + * @param tokenId uint256 ID of the token to be minted + */ + function _mint(address to, uint256 tokenId) internal { + require(to != address(0), "Invalid address"); + require(!_exists(tokenId), "Bad operation"); + + _tokenOwner[tokenId] = to; + _ownedTokensCount[to] = _ownedTokensCount[to].add(1); + + emit Transfer(address(0), to, tokenId); + } + + /** + * @dev Internal function to burn a specific token + * Reverts if the token does not exist + * Deprecated, use _burn(uint256) instead. + * @param owner owner of the token to burn + * @param tokenId uint256 ID of the token being burned + */ + function _burn(address owner, uint256 tokenId) internal { + require(ownerOf(tokenId) == owner, "Unauthorized"); + + _clearApproval(tokenId); + + _ownedTokensCount[owner] = _ownedTokensCount[owner].sub(1); + _tokenOwner[tokenId] = address(0); + + emit Transfer(owner, address(0), tokenId); + } + + /** + * @dev Internal function to burn a specific token + * Reverts if the token does not exist + * @param tokenId uint256 ID of the token being burned + */ + function _burn(uint256 tokenId) internal { + _burn(ownerOf(tokenId), tokenId); + } + + /** + * @dev Internal function to transfer ownership of a given token ID to another address. + * As opposed to transferFrom, this imposes no restrictions on msg.sender. + * @param from current owner of the token + * @param to address to receive the ownership of the given token ID + * @param tokenId uint256 ID of the token to be transferred + */ + function _transferFrom(address from, address to, uint256 tokenId) internal { + require(ownerOf(tokenId) == from, "Unauthorized"); + require(to != address(0), "Invalid address"); + + _clearApproval(tokenId); + + _ownedTokensCount[from] = _ownedTokensCount[from].sub(1); + _ownedTokensCount[to] = _ownedTokensCount[to].add(1); + + _tokenOwner[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + /** + * @dev Internal function to invoke `onERC721Received` on a target address + * The call is not executed if the target address is not a contract + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes _data) internal returns (bool) { + if (!to.isContract()) { + return true; + } + + bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data); + return (retval == _ERC721_RECEIVED); + } + + /** + * @dev Private function to clear current approval of a given token ID + * @param tokenId uint256 ID of the token to be transferred + */ + function _clearApproval(uint256 tokenId) private { + if (_tokenApprovals[tokenId] != address(0)) { + _tokenApprovals[tokenId] = address(0); + } + } +}