diff --git a/contracts/RedeemUtil.sol b/contracts/Bucket.sol similarity index 51% rename from contracts/RedeemUtil.sol rename to contracts/Bucket.sol index f17eccd..affbf67 100644 --- a/contracts/RedeemUtil.sol +++ b/contracts/Bucket.sol @@ -1,7 +1,24 @@ pragma solidity ^0.6.1; pragma experimental ABIEncoderV2; -library RedeemUtil { +abstract contract Bucket { + bool initialized; + address payable public owner; + address public tokenAddress; + uint256 public expirationTime; + uint256 public startTime; + + uint256 constant maxTxDelayInBlocks = 10; + bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(uint256 blockNumber,bytes32 blockHash,address receiver,bytes32 code)"); + bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 DOMAIN_SEPARATOR; + + struct Gift { + address recipient; + bytes32 code; + uint256 data; + } + struct Redeem { uint256 blockNumber; bytes32 blockHash; @@ -9,7 +26,66 @@ library RedeemUtil { bytes32 code; } - bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(uint256 blockNumber,bytes32 blockHash,address receiver,bytes32 code)"); + mapping(address => Gift) public gifts; + + modifier onlyOwner() { + require(msg.sender == owner, "owner required"); + _; + } + + constructor(bytes memory _eip712DomainName, address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public { + initialize(_eip712DomainName, _tokenAddress, _startTime, _expirationTime, msg.sender); + } + + function initialize(bytes memory _eip712DomainName, address _tokenAddress, uint256 _startTime, uint256 _expirationTime, address _owner) public { + require(initialized == false, "already initialized"); + + validateExpiryDate(_expirationTime); + + tokenAddress = _tokenAddress; + startTime = _startTime; + expirationTime = _expirationTime; + owner = payable(_owner); + + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(_eip712DomainName), + keccak256("1"), + getChainID(), + address(this) + )); + + initialized = true; + } + + function transferRedeemable(uint256 data, Redeem memory redeem) virtual internal; + + function transferRedeemablesToOwner() virtual internal; + + function redeem(Redeem calldata _redeem, bytes calldata _sig) external { + validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, startTime); + + address recipient = recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig); + + Gift storage gift = gifts[recipient]; + require(gift.recipient == recipient, "not found"); + + validateCode(_redeem, gift.code); + + uint256 data = gift.data; + + gift.recipient = address(0); + gift.code = 0; + gift.data = 0; + + transferRedeemable(data, _redeem); + } + + function kill() external onlyOwner { + validateExpired(expirationTime); + transferRedeemablesToOwner(); + selfdestruct(owner); + } function getChainID() internal pure returns (uint256) { uint256 id; diff --git a/contracts/GiftBucket.sol b/contracts/GiftBucket.sol index 102d3ca..aeda192 100644 --- a/contracts/GiftBucket.sol +++ b/contracts/GiftBucket.sol @@ -1,67 +1,19 @@ pragma solidity ^0.6.1; pragma experimental ABIEncoderV2; +import "./Bucket.sol"; import "./erc20/IERC20.sol"; -import "./RedeemUtil.sol"; - -contract GiftBucket { - - bool initialized; - - address payable public owner; - - IERC20 public tokenContract; - - uint256 public expirationTime; - uint256 public startTime; - - uint256 constant maxTxDelayInBlocks = 10; - - struct Gift { - address recipient; - uint256 amount; - bytes32 code; - } - - mapping(address => Gift) public gifts; +contract GiftBucket is Bucket { uint256 public redeemableSupply; - bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - bytes32 DOMAIN_SEPARATOR; - - modifier onlyOwner() { - require(msg.sender == owner, "owner required"); - _; - } - - constructor(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public { - initialize(_tokenAddress, _startTime, _expirationTime, msg.sender); - } - - function initialize(address _tokenAddress, uint256 _startTime, uint256 _expirationTime, address _owner) public { - require(initialized == false, "already initialized"); - - RedeemUtil.validateExpiryDate(_expirationTime); - - tokenContract = IERC20(_tokenAddress); - startTime = _startTime; - expirationTime = _expirationTime; - owner = payable(_owner); - - DOMAIN_SEPARATOR = keccak256(abi.encode( - EIP712DOMAIN_TYPEHASH, - keccak256("KeycardGift"), - keccak256("1"), - RedeemUtil.getChainID(), - address(this) - )); - - initialized = true; - } + constructor( + address _tokenAddress, + uint256 _startTime, + uint256 _expirationTime) Bucket("KeycardGift", _tokenAddress, _startTime, _expirationTime) public {} function totalSupply() public view returns(uint256) { - return tokenContract.balanceOf(address(this)); + return IERC20(tokenAddress).balanceOf(address(this)); } function availableSupply() public view returns(uint256) { @@ -81,41 +33,21 @@ contract GiftBucket { require(gift.recipient == address(0), "recipient already used"); gift.recipient = recipient; - gift.amount = amount; gift.code = code; + gift.data = amount; require(redeemableSupply + amount > redeemableSupply, "addition overflow"); redeemableSupply += amount; } - function redeem(RedeemUtil.Redeem calldata _redeem, bytes calldata _sig) external { - RedeemUtil.validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, startTime); - - address recipient = RedeemUtil.recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig); - - Gift storage gift = gifts[recipient]; - require(gift.recipient == recipient, "not found"); - - RedeemUtil.validateCode(_redeem, gift.code); - - uint256 amount = gift.amount; - require(redeemableSupply >= amount, "not enough redeemable supply"); - - gift.recipient = address(0); - gift.amount = 0; - gift.code = 0; - - redeemableSupply -= amount; - - tokenContract.transfer(_redeem.receiver, amount); + function transferRedeemable(uint256 data, Redeem memory redeem) override internal { + require(redeemableSupply >= data, "not enough redeemable supply"); + redeemableSupply -= data; + IERC20(tokenAddress).transfer(redeem.receiver, data); } - function kill() external onlyOwner { - RedeemUtil.validateExpired(expirationTime); - - bool success = tokenContract.transfer(owner, this.totalSupply()); + function transferRedeemablesToOwner() override internal { + bool success = IERC20(tokenAddress).transfer(owner, this.totalSupply()); assert(success); - - selfdestruct(owner); } } diff --git a/contracts/GiftBucketFactory.sol b/contracts/GiftBucketFactory.sol index 8593fb1..77513ee 100644 --- a/contracts/GiftBucketFactory.sol +++ b/contracts/GiftBucketFactory.sol @@ -13,7 +13,7 @@ contract GiftBucketFactory { } function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) { - address p = address(new Proxy(abi.encodeWithSelector(0x9e3d87cd, _tokenAddress, _startTime, _expirationTime, msg.sender), address(GiftBucketImplementation))); + address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardGift", _tokenAddress, _startTime, _expirationTime, msg.sender), address(GiftBucketImplementation))); emit BucketCreated(msg.sender, p); return p; } diff --git a/contracts/NFTBucket.sol b/contracts/NFTBucket.sol index 218342e..8c61c1d 100644 --- a/contracts/NFTBucket.sol +++ b/contracts/NFTBucket.sol @@ -1,91 +1,26 @@ pragma solidity ^0.6.1; pragma experimental ABIEncoderV2; +import "./Bucket.sol"; 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; - - IERC721 public tokenContract; - - uint256 public expirationTime; - uint256 public startTime; - - uint256 constant maxTxDelayInBlocks = 10; - - struct Gift { - address recipient; - uint256 tokenID; - bytes32 code; - } - - mapping(address => Gift) public gifts; +contract NFTBucket is Bucket, IERC165, IERC721Receiver { 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 DOMAIN_SEPARATOR; + constructor( + address _tokenAddress, + uint256 _startTime, + uint256 _expirationTime) Bucket("KeycardNFTGift", _tokenAddress, _startTime, _expirationTime) public {} - modifier onlyOwner() { - require(msg.sender == owner, "owner required"); - _; + function transferRedeemable(uint256 data, Redeem memory redeem) override internal { + IERC721(tokenAddress).safeTransferFrom(address(this), redeem.receiver, data); } - constructor(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public { - initialize(_tokenAddress, _startTime, _expirationTime, msg.sender); - } - - function initialize(address _tokenAddress, uint256 _startTime, uint256 _expirationTime, address _owner) public { - require(initialized == false, "already initialized"); - - RedeemUtil.validateExpiryDate(_expirationTime); - - tokenContract = IERC721(_tokenAddress); - startTime = _startTime; - expirationTime = _expirationTime; - owner = payable(_owner); - - DOMAIN_SEPARATOR = keccak256(abi.encode( - EIP712DOMAIN_TYPEHASH, - keccak256("KeycardNFTGift"), - keccak256("1"), - RedeemUtil.getChainID(), - address(this) - )); - - initialized = true; - } - - function redeem(RedeemUtil.Redeem calldata _redeem, bytes calldata _sig) external { - RedeemUtil.validateRedeem(_redeem, maxTxDelayInBlocks, expirationTime, startTime); - - address recipient = RedeemUtil.recoverSigner(DOMAIN_SEPARATOR, _redeem, _sig); - - Gift storage gift = gifts[recipient]; - require(gift.recipient == recipient, "not found"); - - RedeemUtil.validateCode(_redeem, gift.code); - - uint256 tokenID = gift.tokenID; - gift.recipient = address(0); - gift.tokenID = 0; - gift.code = 0; - - tokenContract.safeTransferFrom(address(this), _redeem.receiver, tokenID); - } - - function kill() external onlyOwner { - RedeemUtil.validateExpired(expirationTime); - - tokenContract.setApprovalForAll(owner, true); - assert(tokenContract.isApprovedForAll(address(this), owner)); - - selfdestruct(owner); + function transferRedeemablesToOwner() override internal { + IERC721(tokenAddress).setApprovalForAll(owner, true); + assert(IERC721(tokenAddress).isApprovedForAll(address(this), owner)); } function supportsInterface(bytes4 interfaceID) external override(IERC165) view returns (bool) { @@ -93,7 +28,7 @@ contract NFTBucket is IERC165, IERC721Receiver { } function onERC721Received(address _operator, address _from, uint256 _tokenID, bytes calldata _data) external override(IERC721Receiver) returns(bytes4) { - require(msg.sender == address(tokenContract), "only the NFT contract can call this"); + require(msg.sender == tokenAddress, "only the NFT contract can call this"); require((_operator == owner) || (_from == owner), "only the owner can create gifts"); require(_data.length == 52, "invalid data field"); @@ -113,8 +48,8 @@ contract NFTBucket is IERC165, IERC721Receiver { require(gift.recipient == address(0), "recipient already used"); gift.recipient = recipient; - gift.tokenID = _tokenID; gift.code = code; + gift.data = _tokenID; return _ERC721_RECEIVED; } diff --git a/contracts/NFTBucketFactory.sol b/contracts/NFTBucketFactory.sol index 55add82..7d979f4 100644 --- a/contracts/NFTBucketFactory.sol +++ b/contracts/NFTBucketFactory.sol @@ -13,7 +13,7 @@ contract NFTBucketFactory { } function create(address _tokenAddress, uint256 _startTime, uint256 _expirationTime) public returns (address) { - address p = address(new Proxy(abi.encodeWithSelector(0x9e3d87cd, _tokenAddress, _startTime, _expirationTime, msg.sender), address(NFTBucketImplementation))); + address p = address(new Proxy(abi.encodeWithSelector(0x4e9464ed, "KeycardNFTGift", _tokenAddress, _startTime, _expirationTime, msg.sender), address(NFTBucketImplementation))); emit BucketCreated(msg.sender, p); return p; } diff --git a/test/contract_spec.js b/test/contract_spec.js index 64a72cf..8aeb1af 100644 --- a/test/contract_spec.js +++ b/test/contract_spec.js @@ -264,7 +264,7 @@ contract("GiftBucket", function () { let initialRedeemableSupply = await GiftBucket.methods.redeemableSupply().call(); let gift = await GiftBucket.methods.gifts(recipient).call(); - const amount = parseInt(gift.amount); + const amount = parseInt(gift.data); const message = { blockNumber: blockNumber, @@ -293,7 +293,6 @@ contract("GiftBucket", function () { let expectedRedeemableSupply = initialRedeemableSupply - amount; let redeemableSupply = await GiftBucket.methods.redeemableSupply().call(); assert.equal(parseInt(redeemableSupply), expectedRedeemableSupply, `redeemableSupply after redeem should be ${expectedRedeemableSupply} instead of ${redeemableSupply}`); - } it("cannot redeem before start date", async function() { diff --git a/test/nft_contract_spec.js b/test/nft_contract_spec.js index c4e3e6d..ffd92c9 100644 --- a/test/nft_contract_spec.js +++ b/test/nft_contract_spec.js @@ -165,7 +165,7 @@ contract("NFTBucket", function () { async function checkGift(recipient, tokenID) { let gift = await NFTBucket.methods.gifts(recipient).call(); assert.equal(gift.recipient, recipient, "gift not found"); - assert.equal(parseInt(gift.tokenID), tokenID, "token ID does not match"); + assert.equal(parseInt(gift.data), tokenID, "token ID does not match"); let tokenOwner = await TestNFT.methods.ownerOf(tokenID).call(); assert.equal(tokenOwner, NFTBucket._address, "token owner is wrong"); } @@ -209,7 +209,7 @@ contract("NFTBucket", function () { async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) { let gift = await NFTBucket.methods.gifts(recipient).call(); - const tokenID = gift.tokenID; + const tokenID = gift.data; const message = { blockNumber: blockNumber,