diff --git a/contracts/common.sol b/contracts/common.sol index e7ea059..290ba88 100644 --- a/contracts/common.sol +++ b/contracts/common.sol @@ -11,7 +11,7 @@ struct Ticket { /// The amount of funds to send. uint256 value; /// The timestamp when the ticket was registered - uint256 timestamp; + uint256 createdAt; } struct TicketsWithIndex { diff --git a/contracts/l2.sol b/contracts/l2.sol index 833fdc0..ad9f56c 100644 --- a/contracts/l2.sol +++ b/contracts/l2.sol @@ -16,12 +16,10 @@ struct L2Deposit { } // Authorized: all tickets in this batch are authorized but not claimed -// Claimed: all tickets in this batch are claimed -// Refunded: all tickets in this batch have been refunded, due to inactivity or provable fraud. +// Withdrawn: all tickets in this batch are withdrawn (either claimed or refunded) enum BatchStatus { Authorized, - Claimed, - Refunded + Withdrawn } struct Batch { @@ -37,10 +35,10 @@ uint256 constant safetyDelay = 60; contract L2 is SignatureChecker { Ticket[] public tickets; - // `batches` is used to record the fact that tickets with nonce between startingNonce and startingNonce + numTickets-1 are authorized, claimed or refunded. + // `batches` is used to record the fact that tickets with nonce between startingNonce and startingNonce + numTickets-1 are authorized or withdrawn. // Indexed by nonce mapping(uint256 => Batch) batches; - uint256 nextNonceToAuthorize = 0; + uint256 nextBatchStart = 0; function depositOnL2(L2Deposit calldata deposit) public payable { uint256 amountAvailable = deposit.trustedAmount; @@ -64,7 +62,7 @@ contract L2 is SignatureChecker { Ticket memory ticket = Ticket({ l1Recipient: deposit.l1Recipient, value: deposit.depositAmount, - timestamp: block.timestamp + createdAt: block.timestamp }); // ticket's nonce is now its index in `tickets` @@ -81,9 +79,9 @@ contract L2 is SignatureChecker { TicketsWithIndex memory ticketsWithIndex ) = createBatch(first, last); bytes32 message = keccak256(abi.encode(ticketsWithIndex)); - uint256 earliestTimestamp = tickets[first].timestamp; + uint256 earliestTimestamp = tickets[first].createdAt; - require(nextNonceToAuthorize == first, "Batches must be gapless"); + require(nextBatchStart == first, "Batches must be gapless"); require( recoverSigner(message, signature) == lpAddress, "Must be signed by liquidity provider" @@ -95,7 +93,7 @@ contract L2 is SignatureChecker { ); batches[first] = batch; - nextNonceToAuthorize = last + 1; + nextBatchStart = last + 1; } function createBatch(uint256 first, uint256 last) @@ -131,7 +129,7 @@ contract L2 is SignatureChecker { "safetyDelay must have passed since authorization timestamp" ); - batch.status = BatchStatus.Claimed; + batch.status = BatchStatus.Withdrawn; batches[first] = batch; (bool sent, ) = lpAddress.call{value: batch.total}(""); require(sent, "Failed to send Ether"); @@ -187,6 +185,31 @@ contract L2 is SignatureChecker { require(sent, "Failed to send Ether"); } - batches[honestStartNonce].status = BatchStatus.Refunded; + batches[honestStartNonce].status = BatchStatus.Withdrawn; + } + + /** + * @notice Refund all tickets with nonce >= nextBatchStart and nonce <= lastNonce + * @param lastNonce Nonce of the "expired" ticket aka a ticket that is past AuthorizationWindow + */ + function refund(uint256 lastNonce) public { + require( + block.timestamp > tickets[lastNonce].createdAt + maxAuthDelay, + "maxAuthDelay must have passed since deposit" + ); + + require( + nextBatchStart <= lastNonce, + "The nonce must not be a part of a batch" + ); + (Batch memory batch, ) = createBatch(nextBatchStart, lastNonce); + batches[nextBatchStart] = batch; + batch.status = BatchStatus.Withdrawn; + nextBatchStart = lastNonce + 1; + + (bool sent, ) = tickets[lastNonce].l1Recipient.call{ + value: tickets[lastNonce].value + }(""); + require(sent, "Failed to send Ether"); } } diff --git a/src/constants.ts b/src/constants.ts index c322be0..682a43b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,5 @@ export const SIGNED_SWAPS_ABI_TYPE = [ - "tuple(uint256 startIndex, tuple(address l1Recipient, uint256 value, uint256 timestamp)[]) ", + "tuple(uint256 startIndex, tuple(address l1Recipient, uint256 value, uint256 createdAt)[]) ", ]; export const USE_ERC20 = process.env.USE_ERC20 === "true"; diff --git a/test/safe.test.ts b/test/safe.test.ts index 64e2bad..24e5772 100644 --- a/test/safe.test.ts +++ b/test/safe.test.ts @@ -192,3 +192,21 @@ it("Handles a fraud proofs", async () => { ), ); }); + +it("Able to get a ticket refunded", async () => { + await deposit(0, 10); + await expect(customerL2.refund(0, { gasLimit })).to.be.rejectedWith( + "maxAuthDelay must have passed since deposit", + ); + await ethers.provider.send("evm_increaseTime", [61]); + await waitForTx(customerL2.refund(0, { gasLimit })); + await waitForTx(customerL2.refund(1, { gasLimit })); + await expect(customerL2.refund(1, { gasLimit })).to.be.rejectedWith( + "The nonce must not be a part of a batch", + ); + + await deposit(2, 8); + await ethers.provider.send("evm_increaseTime", [61]); + // Refund 3rd and 4th deposit + await waitForTx(customerL2.refund(2, { gasLimit })); +});