diff --git a/app/js/actions/redeem.ts b/app/js/actions/redeem.ts index acab5d4..5f7050e 100644 --- a/app/js/actions/redeem.ts +++ b/app/js/actions/redeem.ts @@ -132,6 +132,9 @@ interface SignRedeemResponse { async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: string, message: RedeemMessage): Promise { const chainId = await config.web3!.eth.net.getId(); + let block = await config.web3!.eth.getBlock("latest"); + let finalMessage = {blockNumber: block.number, blockHash: block.hash, receiver: message.receiver, code: message.code}; + const domain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, @@ -140,6 +143,8 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s ]; const redeem = [ + { name: "blockNumber", type: "uint256" }, + { name: "blockHash", type: "bytes32" }, { name: "receiver", type: "address" }, { name: "code", type: "bytes32" }, ]; @@ -158,7 +163,7 @@ async function signRedeem(web3Type: Web3Type, contractAddress: string, signer: s }, primaryType: ("Redeem" as const), domain: domainData, - message: message + message: finalMessage }; if (web3Type === Web3Type.Status) { diff --git a/contracts/GiftBucket.sol b/contracts/GiftBucket.sol index 9eb484b..ef17966 100644 --- a/contracts/GiftBucket.sol +++ b/contracts/GiftBucket.sol @@ -13,6 +13,8 @@ contract GiftBucket { uint256 public expirationTime; + uint256 constant maxTxDelayInBlocks = 10; + struct Gift { address recipient; uint256 amount; @@ -22,6 +24,8 @@ contract GiftBucket { mapping(address => Gift) public gifts; struct Redeem { + uint256 blockNumber; + bytes32 blockHash; address receiver; bytes32 code; } @@ -29,7 +33,7 @@ contract GiftBucket { uint256 public redeemableSupply; bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(address receiver,bytes32 code)"); + bytes32 constant REDEEM_TYPEHASH = keccak256("Redeem(uint256 blockNumber,bytes32 blockHash,address receiver,bytes32 code)"); bytes32 DOMAIN_SEPARATOR; modifier onlyOwner() { @@ -99,6 +103,10 @@ contract GiftBucket { } 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); @@ -130,6 +138,8 @@ contract GiftBucket { function hashRedeem(Redeem memory _redeem) internal pure returns (bytes32) { return keccak256(abi.encode( REDEEM_TYPEHASH, + _redeem.blockNumber, + _redeem.blockHash, _redeem.receiver, _redeem.code )); diff --git a/test/contract_spec.js b/test/contract_spec.js index 47b0094..f57d8fb 100644 --- a/test/contract_spec.js +++ b/test/contract_spec.js @@ -54,6 +54,8 @@ async function signRedeem(contractAddress, signer, message) { ]; const redeem = [ + { name: "blockNumber", type: "uint256" }, + { name: "blockHash", type: "bytes32" }, { name: "receiver", type: "address" }, { name: "code", type: "bytes32" }, ]; @@ -256,7 +258,7 @@ contract("GiftBucket", function () { } }); - async function testRedeem(receiver, recipient, signer, relayer, redeemCode) { + async function testRedeem(receiver, recipient, signer, relayer, redeemCode, blockNumber, blockHash) { let initialBucketBalance = await TestToken.methods.balanceOf(GiftBucket._address).call(); let initialUserBalance = await TestToken.methods.balanceOf(user).call(); let initialRedeemableSupply = await GiftBucket.methods.redeemableSupply().call(); @@ -265,6 +267,8 @@ contract("GiftBucket", function () { const amount = parseInt(gift.amount); const message = { + blockNumber: blockNumber, + blockHash: blockHash, receiver: receiver, code: redeemCode, }; @@ -293,9 +297,11 @@ contract("GiftBucket", function () { } it("cannot redeem after expiration date", async function() { + const block = await web3.eth.getBlock("latest"); await mineAt(EXPIRATION_TIME); + try { - await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE); + await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE, block.number, block.hash); assert.fail("redeem should have failed"); } catch(e) { assert.match(e.message, /expired/); @@ -303,9 +309,10 @@ contract("GiftBucket", function () { }); it("cannot redeem with invalid code", async function() { + const block = await web3.eth.getBlock("latest"); await mineAt(NOW); try { - await testRedeem(user, keycard_1, keycard_1, relayer, web3.utils.sha3("bad-code")); + await testRedeem(user, keycard_1, keycard_1, relayer, web3.utils.sha3("bad-code"), block.number, block.hash); assert.fail("redeem should have failed"); } catch(e) { assert.match(e.message, /invalid code/); @@ -313,18 +320,53 @@ contract("GiftBucket", function () { }); it("cannot redeem with invalid recipient", async function() { + const block = await web3.eth.getBlock("latest"); await mineAt(NOW); try { - await testRedeem(user, keycard_1, keycard_2, relayer, REDEEM_CODE); + await testRedeem(user, keycard_1, keycard_2, relayer, REDEEM_CODE, block.number, block.hash); assert.fail("redeem should have failed"); } catch(e) { assert.match(e.message, /not found/); } }); - it("can redeem before expiration date", async function() { + it("cannot redeem with a block in the future", async function() { + const block = await web3.eth.getBlock("latest"); await mineAt(NOW); - await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE); + try { + await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE, (block.number + 2), "0x0000000000000000000000000000000000000000000000000000000000000000"); + } catch (e) { + assert.match(e.message, /future/); + } + }); + + it("cannot redeem with an old block", async function() { + const currentBlock = await web3.eth.getBlock("latest"); + const block = await web3.eth.getBlock(currentBlock.number - 10); + + await mineAt(NOW); + try { + await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE, block.number, block.hash); + } catch (e) { + assert.match(e.message, /too old/); + } + }); + + it("cannot redeem with an invalid hash", async function() { + const block = await web3.eth.getBlock("latest"); + + await mineAt(NOW); + try { + await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE, block.number, "0x0000000000000000000000000000000000000000000000000000000000000000"); + } catch (e) { + assert.match(e.message, /invalid block hash/); + } + }); + + it("can redeem before expiration date", async function() { + const block = await web3.eth.getBlock("latest"); + await mineAt(NOW); + await testRedeem(user, keycard_1, keycard_1, relayer, REDEEM_CODE, block.number, block.hash); }); async function testKill() {