Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refunds #75

Merged
merged 6 commits into from
Dec 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion contracts/common.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
47 changes: 35 additions & 12 deletions contracts/l2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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`
Expand All @@ -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"
Expand All @@ -95,7 +93,7 @@ contract L2 is SignatureChecker {
);

batches[first] = batch;
nextNonceToAuthorize = last + 1;
nextBatchStart = last + 1;
}

function createBatch(uint256 first, uint256 last)
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
}
}
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
18 changes: 18 additions & 0 deletions test/safe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
});