diff --git a/contracts/GiftBucket.sol b/contracts/GiftBucket.sol index db26803..d5834af 100644 --- a/contracts/GiftBucket.sol +++ b/contracts/GiftBucket.sol @@ -2,6 +2,7 @@ pragma solidity ^0.6.1; pragma experimental ABIEncoderV2; import "./erc20/IERC20.sol"; +import "./RedeemUtil.sol"; contract GiftBucket { @@ -23,17 +24,9 @@ contract GiftBucket { mapping(address => Gift) public gifts; - struct Redeem { - uint256 blockNumber; - bytes32 blockHash; - address receiver; - bytes32 code; - } - uint256 public redeemableSupply; 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() { @@ -48,7 +41,7 @@ contract GiftBucket { 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"); + RedeemUtil.validateExpiryDate(_expirationTime); tokenContract = IERC20(_tokenAddress); expirationTime = _expirationTime; @@ -58,22 +51,13 @@ contract GiftBucket { EIP712DOMAIN_TYPEHASH, keccak256("KeycardGift"), keccak256("1"), - _getChainID(), + RedeemUtil.getChainID(), address(this) )); initialized = true; } - function _getChainID() internal pure returns (uint256) { - uint256 id; - assembly { - id := chainid() - } - - return id; - } - function totalSupply() public view returns(uint256) { return tokenContract.balanceOf(address(this)); } @@ -102,20 +86,15 @@ contract GiftBucket { redeemableSupply += amount; } - 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"); + function redeem(RedeemUtil.Redeem calldata _redeem, bytes calldata _sig) external { + RedeemUtil.validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, 0); - require(block.timestamp < expirationTime, "expired gift"); - - address recipient = recoverSigner(_redeem, sig); + address recipient = RedeemUtil.recoverSigner(DOMAIN_SEPARATOR, _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"); + RedeemUtil.validateCode(_redeem, gift.code); uint256 amount = gift.amount; require(redeemableSupply >= amount, "not enough redeemable supply"); @@ -127,49 +106,11 @@ contract GiftBucket { } function kill() external onlyOwner { - require(block.timestamp >= expirationTime, "not expired yet"); + RedeemUtil.validateExpired(expirationTime); bool success = tokenContract.transfer(owner, this.totalSupply()); assert(success); 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); - } } diff --git a/contracts/NFTBucket.sol b/contracts/NFTBucket.sol index 83bad7a..3a669a7 100644 --- a/contracts/NFTBucket.sol +++ b/contracts/NFTBucket.sol @@ -4,9 +4,9 @@ pragma experimental ABIEncoderV2; import "./erc721/IERC721.sol"; import "./erc721/IERC721Receiver.sol"; import "./erc721/IERC165.sol"; +import "./RedeemUtil.sol"; contract NFTBucket is IERC165, IERC721Receiver { - bool initialized; address payable public owner; @@ -25,17 +25,9 @@ contract NFTBucket is IERC165, IERC721Receiver { mapping(address => Gift) public gifts; - struct Redeem { - uint256 blockNumber; - bytes32 blockHash; - address receiver; - bytes32 code; - } - bytes4 private constant _ERC721_RECEIVED = 0x150b7a02; //bytes4(keccak256("onERC721Received(address,address,uint256,bytes)")) 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() { @@ -50,7 +42,7 @@ contract NFTBucket is IERC165, IERC721Receiver { 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"); + RedeemUtil.validateExpiryDate(_expirationTime); tokenContract = IERC721(_tokenAddress); expirationTime = _expirationTime; @@ -60,42 +52,28 @@ contract NFTBucket is IERC165, IERC721Receiver { EIP712DOMAIN_TYPEHASH, keccak256("KeycardNFTGift"), keccak256("1"), - _getChainID(), + RedeemUtil.getChainID(), address(this) )); initialized = true; } - function _getChainID() internal pure returns (uint256) { - uint256 id; - assembly { - id := chainid() - } + function redeem(RedeemUtil.Redeem calldata _redeem, bytes calldata _sig) external { + RedeemUtil.validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, 0); - 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); + address recipient = RedeemUtil.recoverSigner(DOMAIN_SEPARATOR, _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"); + RedeemUtil.validateCode(_redeem, gift.code); tokenContract.safeTransferFrom(address(this), _redeem.receiver, gift.tokenID); } function kill() external onlyOwner { - require(block.timestamp >= expirationTime, "not expired yet"); + RedeemUtil.validateExpired(expirationTime); tokenContract.setApprovalForAll(owner, true); assert(tokenContract.isApprovedForAll(address(this), owner)); @@ -103,44 +81,6 @@ contract NFTBucket is IERC165, IERC721Receiver { 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; } diff --git a/contracts/RedeemUtil.sol b/contracts/RedeemUtil.sol new file mode 100644 index 0000000..980a9e6 --- /dev/null +++ b/contracts/RedeemUtil.sol @@ -0,0 +1,82 @@ +pragma solidity ^0.6.1; +pragma experimental ABIEncoderV2; + +library RedeemUtil { + struct Redeem { + uint256 blockNumber; + bytes32 blockHash; + address receiver; + bytes32 code; + } + + bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(uint256 blockNumber,bytes32 blockHash,address receiver,bytes32 code)"); + + function getChainID() internal pure returns (uint256) { + uint256 id; + assembly { + id := chainid() + } + + return id; + } + + function validateExpiryDate(uint256 _expirationTime) internal view { + require(_expirationTime > block.timestamp, "expiration can't be in the past"); + } + + function validateExpired(uint256 _expirationTime) internal view { + require(block.timestamp >= _expirationTime, "not expired yet"); + } + + function validateRedeem(Redeem memory _redeem, uint256 _maxTxDelayInBlocks, uint256 _expirationTime, uint256 _startTime) internal view { + 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"); + require(block.timestamp > _startTime, "reedeming not yet started"); + } + + 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(bytes32 _domainSeparator, Redeem memory _redeem, bytes memory _sig) internal pure 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", + _domainSeparator, + hashRedeem(_redeem) + )); + + return ecrecover(digest, v, r, s); + } + + function validateCode(Redeem memory _redeem, bytes32 _code) internal pure { + bytes32 codeHash = keccak256(abi.encodePacked(_redeem.code)); + require(codeHash == _code, "invalid code"); + } +} \ No newline at end of file