diff --git a/contracts/NFTBucket.sol b/contracts/NFTBucket.sol new file mode 100644 index 0000000..d336bfb --- /dev/null +++ b/contracts/NFTBucket.sol @@ -0,0 +1,174 @@ +pragma solidity ^0.6.1; +pragma experimental ABIEncoderV2; + +import "./erc721/IERC721.sol"; +import "./erc721/IERC721Receiver.sol"; +import "./erc721/IERC165.sol"; + +contract NFTBucket is IERC165, IERC721Receiver { + + bool initialized; + + address payable public owner; + + IERC721 public tokenContract; + + uint256 public expirationTime; + + uint256 constant maxTxDelayInBlocks = 10; + + struct Gift { + address recipient; + uint256 tokenID; + bytes32 code; + } + + mapping(address => Gift) public gifts; + + struct Redeem { + uint256 blockNumber; + bytes32 blockHash; + address receiver; + bytes32 code; + } + + uint256 public redeemableSupply; + + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(uint256 blockNumber,bytes32 blockHash,address receiver,bytes32 code)"); + bytes32 DOMAIN_SEPARATOR; + + modifier onlyOwner() { + require(msg.sender == owner, "owner required"); + _; + } + + constructor(address _tokenAddress, uint256 _expirationTime) public { + initialize(_tokenAddress, _expirationTime, msg.sender); + } + + function initialize(address _tokenAddress, uint256 _expirationTime, address _owner) public { + require(initialized == false, "already initialized"); + + require(_expirationTime > block.timestamp, "expiration can't be in the past"); + + tokenContract = IERC721(_tokenAddress); + expirationTime = _expirationTime; + owner = payable(_owner); + + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256("KeycardNFTGift"), + keccak256("1"), + _getChainID(), + address(this) + )); + + initialized = true; + } + + function _getChainID() internal pure returns (uint256) { + uint256 id; + assembly { + id := chainid() + } + + return id; + } + + function redeem(Redeem calldata _redeem, bytes calldata sig) external { + require(_redeem.blockNumber < block.number, "transaction cannot be in the future"); + require(_redeem.blockNumber >= (block.number - maxTxDelayInBlocks), "transaction too old"); + require(_redeem.blockHash == blockhash(_redeem.blockNumber), "invalid block hash"); + + require(block.timestamp < expirationTime, "expired gift"); + + address recipient = recoverSigner(_redeem, sig); + + Gift storage gift = gifts[recipient]; + require(gift.recipient == recipient, "not found"); + + bytes32 codeHash = keccak256(abi.encodePacked(_redeem.code)); + require(codeHash == gift.code, "invalid code"); + + tokenContract.safeTransferFrom(address(this), gift.recipient, gift.tokenID); + } + + function kill() external onlyOwner { + require(block.timestamp >= expirationTime, "not expired yet"); + + tokenContract.setApprovalForAll(owner, true); + assert(tokenContract.isApprovedForAll(address(this), owner)); + + selfdestruct(owner); + } + + function hashRedeem(Redeem memory _redeem) internal pure returns (bytes32) { + return keccak256(abi.encode( + REDEEM_TYPEHASH, + _redeem.blockNumber, + _redeem.blockHash, + _redeem.receiver, + _redeem.code + )); + } + + function recoverSigner(Redeem memory _redeem, bytes memory sig) internal view returns(address) { + require(sig.length == 65, "bad signature length"); + + bytes32 r; + bytes32 s; + uint8 v; + + assembly { + r := mload(add(sig, 32)) + s := mload(add(sig, 64)) + v := byte(0, mload(add(sig, 96))) + } + + if (v < 27) { + v += 27; + } + + require(v == 27 || v == 28, "signature version doesn't match"); + + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + hashRedeem(_redeem) + )); + + return ecrecover(digest, v, r, s); + } + + function supportsInterface(bytes4 interfaceID) external override(IERC165) view returns (bool) { + return interfaceID == _ERC721_RECEIVED; + } + + function onERC721Received(address _operator, address _from, uint256 _tokenID, bytes calldata _data) external override(IERC721Receiver) returns(bytes4) { + require((_operator == owner) || (_from == owner), "only the owner can create gifts"); + + bytes memory d = _data; + bytes32 tmp; + bytes32 code; + + assembly { + // tmp is 12 bytes of padding (taken from the array length) + 20 bytes of address + tmp := mload(add(d, 20)) + code := mload(add(d, 52)) + } + + address recipient = address(uint160(uint256(tmp))); + + Gift storage gift = gifts[recipient]; + require(gift.recipient == address(0), "recipient already used"); + + gift.recipient = recipient; + gift.tokenID = _tokenID; + gift.code = code; + + return 0x150b7a02; //bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")) + } +} diff --git a/contracts/erc721/IERC165.sol b/contracts/erc721/IERC165.sol new file mode 100644 index 0000000..f4e686f --- /dev/null +++ b/contracts/erc721/IERC165.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.6.1; + +interface IERC165 { + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/erc721/IERC721.sol b/contracts/erc721/IERC721.sol new file mode 100644 index 0000000..fa09845 --- /dev/null +++ b/contracts/erc721/IERC721.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.6.1; + +/** + * @dev Required interface of an ERC721 compliant contract. + */ +interface IERC721 { + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + function balanceOf(address owner) external view returns (uint256 balance); + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function safeTransferFrom(address from, address to, uint256 tokenId) external; + + function transferFrom(address from, address to, uint256 tokenId) external; + function approve(address to, uint256 tokenId) external; + function getApproved(uint256 tokenId) external view returns (address operator); + + function setApprovalForAll(address operator, bool _approved) external; + function isApprovedForAll(address owner, address operator) external view returns (bool); + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external; +} \ No newline at end of file diff --git a/contracts/erc721/IERC721Receiver.sol b/contracts/erc721/IERC721Receiver.sol new file mode 100644 index 0000000..f598294 --- /dev/null +++ b/contracts/erc721/IERC721Receiver.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.6.1; + +interface IERC721Receiver { + function onERC721Received(address operator, address from, uint256 tokenId, bytes calldata data) external returns (bytes4); +} \ No newline at end of file diff --git a/contracts/erc721/TestNFT.sol b/contracts/erc721/TestNFT.sol new file mode 100644 index 0000000..c95f359 --- /dev/null +++ b/contracts/erc721/TestNFT.sol @@ -0,0 +1,132 @@ +pragma solidity ^0.6.1; + +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./IERC165.sol"; + +contract TestNFT is IERC165, IERC721 { + bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; + + mapping (uint256 => address) private _tokenOwner; + mapping (uint256 => address) private _tokenApprovals; + mapping (address => uint256) private _ownedTokensCount; + mapping (address => mapping (address => bool)) private _operatorApprovals; + + bytes4 private constant _INTERFACE_ID_ERC721 = 0x80ac58cd; + + function balanceOf(address owner) public override(IERC721) view returns (uint256) { + require(owner != address(0), "ERC721: balance query for the zero address"); + return _ownedTokensCount[owner]; + } + + function ownerOf(uint256 tokenId) public override(IERC721) view returns (address) { + address owner = _tokenOwner[tokenId]; + require(owner != address(0), "ERC721: owner query for nonexistent token"); + + return owner; + } + + function approve(address to, uint256 tokenId) public override(IERC721) { + address owner = ownerOf(tokenId); + require(to != owner, "ERC721: approval to current owner"); + + require(msg.sender == owner || isApprovedForAll(owner, msg.sender), + "ERC721: approve caller is not owner nor approved for all" + ); + + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + function getApproved(uint256 tokenId) public override(IERC721) view returns (address) { + require(_exists(tokenId), "ERC721: approved query for nonexistent token"); + + return _tokenApprovals[tokenId]; + } + + function setApprovalForAll(address to, bool approved) public override(IERC721) { + require(to != msg.sender, "ERC721: approve to caller"); + + _operatorApprovals[msg.sender][to] = approved; + emit ApprovalForAll(msg.sender, to, approved); + } + + function isApprovedForAll(address owner, address operator) public override(IERC721) view returns (bool) { + return _operatorApprovals[owner][operator]; + } + + function transferFrom(address from, address to, uint256 tokenId) public override(IERC721) { + require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved"); + + _transferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public override(IERC721) { + safeTransferFrom(from, to, tokenId, ""); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public override(IERC721) { + transferFrom(from, to, tokenId); + require(_checkOnERC721Received(from, to, tokenId, _data), "ERC721: transfer to non ERC721Receiver implementer"); + } + + function _exists(uint256 tokenId) internal view returns (bool) { + address owner = _tokenOwner[tokenId]; + return owner != address(0); + } + + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + require(_exists(tokenId), "ERC721: operator query for nonexistent token"); + address owner = ownerOf(tokenId); + return (spender == owner || getApproved(tokenId) == spender || isApprovedForAll(owner, spender)); + } + + function mint(address to, uint256 tokenId) public returns (bool) { + require(to != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId), "ERC721: token already minted"); + + _tokenOwner[tokenId] = to; + _ownedTokensCount[to]++; + + emit Transfer(address(0), to, tokenId); + } + + function _transferFrom(address from, address to, uint256 tokenId) internal { + require(ownerOf(tokenId) == from, "ERC721: transfer of token that is not own"); + require(to != address(0), "ERC721: transfer to the zero address"); + + _clearApproval(tokenId); + + _ownedTokensCount[from]--; + _ownedTokensCount[to]++; + + _tokenOwner[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory _data) internal returns (bool) { + if (!isContract(to)) { + return true; + } + + bytes4 retval = IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, _data); + return (retval == _ERC721_RECEIVED); + } + + function _clearApproval(uint256 tokenId) private { + if (_tokenApprovals[tokenId] != address(0)) { + _tokenApprovals[tokenId] = address(0); + } + } + + function isContract(address account) internal view returns (bool) { + uint256 size; + assembly { size := extcodesize(account) } + return size > 0; + } + + function supportsInterface(bytes4 interfaceID) external override(IERC165) view returns (bool) { + return interfaceID == _INTERFACE_ID_ERC721; + } +} \ No newline at end of file