feat: partial payouts for cancelled requests (#69)

This commit is contained in:
Adam Uhlíř 2023-10-16 11:14:02 +02:00 committed by GitHub
parent 1854dfba99
commit 14e453ac31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 171 additions and 59 deletions

View File

@ -25,6 +25,10 @@ contract Marketplace is Proofs, StateRetrieval {
struct RequestContext {
RequestState state;
uint256 slotsFilled;
/// @notice Tracks how much funds should be returned when Request expires to the Request creator
/// @dev The sum is deducted every time a host fills a Slot by precalculated amount that he should receive if the Request expires
uint256 expiryFundsWithdraw;
uint256 startedAt;
uint256 endsAt;
}
@ -32,7 +36,12 @@ contract Marketplace is Proofs, StateRetrieval {
struct Slot {
SlotState state;
RequestId requestId;
/// @notice Timestamp that signals when slot was filled
/// @dev Used for partial payouts when Requests expires and Hosts are paid out only the time they host the content.
uint256 filledAt;
uint256 slotIndex;
/// @notice Tracks the current amount of host's collateral that is to be payed out at the end of Slot's lifespan.
/// @dev When Slot is filled, the collateral is collected in amount of request.ask.collateral
/// @dev When Host is slashed for missing a proof the slashed amount is reflected in this variable
@ -80,6 +89,7 @@ contract Marketplace is Proofs, StateRetrieval {
_addToMyRequests(request.client, id);
uint256 amount = request.price();
_requestContexts[id].expiryFundsWithdraw = amount;
_marketplaceTotals.received += amount;
_transferFrom(msg.sender, amount);
@ -106,8 +116,10 @@ contract Marketplace is Proofs, StateRetrieval {
slot.host = msg.sender;
slot.state = SlotState.Filled;
slot.filledAt = block.timestamp;
RequestContext storage context = _requestContexts[requestId];
context.slotsFilled += 1;
context.expiryFundsWithdraw -= _expiryPayoutAmount(requestId, block.timestamp);
// Collect collateral
uint256 collateralAmount = request.ask.collateral;
@ -133,6 +145,8 @@ contract Marketplace is Proofs, StateRetrieval {
if (state == SlotState.Finished) {
_payoutSlot(slot.requestId, slotId);
} else if (state == SlotState.Cancelled) {
_payoutCancelledSlot(slot.requestId, slotId);
} else if (state == SlotState.Failed) {
_removeFromMySlots(msg.sender, slotId);
} else if (state == SlotState.Filled) {
@ -207,6 +221,19 @@ contract Marketplace is Proofs, StateRetrieval {
assert(token.transfer(slot.host, amount));
}
function _payoutCancelledSlot(
RequestId requestId,
SlotId slotId
) private requestIsKnown(requestId) {
Slot storage slot = _slots[slotId];
_removeFromMySlots(slot.host, slotId);
uint256 amount = _expiryPayoutAmount(requestId, slot.filledAt) + slot.currentCollateral;
_marketplaceTotals.sent += amount;
slot.state = SlotState.Paid;
assert(token.transfer(slot.host, amount));
}
/// @notice Withdraws storage request funds back to the client that deposited them.
/// @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address.
/// @param requestId the id of the request
@ -224,10 +251,7 @@ contract Marketplace is Proofs, StateRetrieval {
emit RequestCancelled(requestId);
// TODO: To be changed once we start paying out hosts for the time they
// fill a slot. The amount that we paid to hosts will then have to be
// deducted from the price.
uint256 amount = request.price();
uint256 amount = context.expiryFundsWithdraw;
_marketplaceTotals.sent += amount;
assert(token.transfer(msg.sender, amount));
}
@ -268,6 +292,14 @@ contract Marketplace is Proofs, StateRetrieval {
}
}
/// @notice Calculates the amount that should be payed out to a host if a request expires based on when the host fills the slot
function _expiryPayoutAmount(RequestId requestId, uint256 startingTimestamp) private view returns (uint256) {
Request storage request = _requests[requestId];
require(startingTimestamp < request.expiry, "Start not before expiry");
return (request.expiry - startingTimestamp) * request.ask.reward;
}
function getHost(SlotId slotId) public view returns (address) {
return _slots[slotId].host;
}
@ -300,7 +332,7 @@ contract Marketplace is Proofs, StateRetrieval {
return SlotState.Paid;
}
if (reqState == RequestState.Cancelled) {
return SlotState.Finished;
return SlotState.Cancelled;
}
if (reqState == RequestState.Finished) {
return SlotState.Finished;

View File

@ -51,7 +51,8 @@ enum SlotState {
Filled, // host has filled slot
Finished, // successfully completed
Failed, // the request has failed
Paid // host has been paid
Paid, // host has been paid
Cancelled // when request was cancelled then slot is cancelled as well
}
library Requests {

View File

@ -24,11 +24,13 @@ const {
revert,
mine,
ensureMinimumBlockHeight,
advanceTime,
advanceTimeTo,
advanceTimeForNextBlock,
advanceTimeToForNextBlock,
currentTime,
} = require("./evm")
const ACCOUNT_STARTING_BALANCE = 1_000_000_000
describe("Marketplace constructor", function () {
let Marketplace, token, config
@ -90,8 +92,8 @@ describe("Marketplace", function () {
const TestToken = await ethers.getContractFactory("TestToken")
token = await TestToken.deploy()
for (account of [client, host1, host2, host3]) {
await token.mint(account.address, 1_000_000_000)
for (let account of [client, host1, host2, host3]) {
await token.mint(account.address, ACCOUNT_STARTING_BALANCE)
}
const Marketplace = await ethers.getContractFactory("TestMarketplace")
@ -320,6 +322,7 @@ describe("Marketplace", function () {
it("sets request end time to the past once cancelled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request)
await mine()
const now = await currentTime()
await expect(await marketplace.requestEnd(requestId(request))).to.be.eq(
now - 1
@ -329,6 +332,7 @@ describe("Marketplace", function () {
it("checks that request end time is in the past once finished", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request))
await mine()
const now = await currentTime()
// in the process of calling currentTime and requestEnd,
// block.timestamp has advanced by 1, so the expected proof end time will
@ -403,12 +407,17 @@ describe("Marketplace", function () {
})
it("pays the host when contract was cancelled", async function () {
// Lets move the time into middle of the expiry window
const fillTimestamp = await currentTime() + Math.floor((request.expiry - await currentTime()) / 2) - 1
await advanceTimeToForNextBlock(fillTimestamp)
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request)
const startBalance = await token.balanceOf(host.address)
await marketplace.freeSlot(slotId(slot))
const endBalance = await token.balanceOf(host.address)
expect(endBalance).to.be.gt(startBalance)
const expectedPartialPayout = (request.expiry - fillTimestamp) * request.ask.reward
expect(endBalance - ACCOUNT_STARTING_BALANCE).to.be.equal(expectedPartialPayout)
})
it("does not pay when the contract hasn't ended", async function () {
@ -542,6 +551,19 @@ describe("Marketplace", function () {
const endBalance = await token.balanceOf(client.address)
expect(endBalance - startBalance).to.equal(price(request))
})
it("withdraws to the client for cancelled requests lowered by hosts payout", async function () {
const fillTimestamp = await currentTime() + Math.floor((request.expiry - await currentTime()) / 2)
await advanceTimeToForNextBlock(fillTimestamp)
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request)
const expectedPartialHostPayout = (request.expiry - fillTimestamp) * request.ask.reward
switchAccount(client)
await marketplace.withdrawFunds(slot.request)
const endBalance = await token.balanceOf(client.address)
expect(ACCOUNT_STARTING_BALANCE - endBalance).to.equal(expectedPartialHostPayout)
})
})
describe("request state", function () {
@ -561,6 +583,7 @@ describe("Marketplace", function () {
it("changes to 'Cancelled' once request is cancelled", async function () {
await waitUntilCancelled(request)
await mine()
expect(await marketplace.requestState(slot.request)).to.equal(Cancelled)
})
@ -579,6 +602,7 @@ describe("Marketplace", function () {
it("changes to 'Failed' once too many slots are freed", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request)
await mine()
expect(await marketplace.requestState(slot.request)).to.equal(Failed)
})
@ -601,6 +625,7 @@ describe("Marketplace", function () {
it("changes to 'Finished' when the request ends", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, requestId(request))
await mine()
expect(await marketplace.requestState(slot.request)).to.equal(Finished)
})
@ -613,7 +638,7 @@ describe("Marketplace", function () {
})
describe("slot state", function () {
const { Free, Filled, Finished, Failed, Paid } = SlotState
const { Free, Filled, Finished, Failed, Paid, Cancelled } = SlotState
let period, periodEnd
beforeEach(async function () {
@ -628,14 +653,16 @@ describe("Marketplace", function () {
})
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await mine()
while (
!(
(await marketplace.isProofRequired(id)) &&
(await marketplace.getPointer(id)) < 250
)
) {
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
}
@ -651,13 +678,15 @@ describe("Marketplace", function () {
it("changes to 'Finished' when request finishes", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFinished(marketplace, slot.request)
await mine()
expect(await marketplace.slotState(slotId(slot))).to.equal(Finished)
})
it("changes to 'Finished' when request is cancelled", async function () {
it("changes to 'Cancelled' when request is cancelled", async function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilCancelled(request)
expect(await marketplace.slotState(slotId(slot))).to.equal(Finished)
await mine()
expect(await marketplace.slotState(slotId(slot))).to.equal(Cancelled)
})
it("changes to 'Free' when host frees the slot", async function () {
@ -671,7 +700,8 @@ describe("Marketplace", function () {
while ((await marketplace.slotState(slotId(slot))) === Filled) {
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
}
expect(await marketplace.slotState(slotId(slot))).to.equal(Free)
@ -680,6 +710,7 @@ describe("Marketplace", function () {
it("changes to 'Failed' when request fails", async function () {
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilSlotFailed(marketplace, request, slot)
await mine()
expect(await marketplace.slotState(slotId(slot))).to.equal(Failed)
})
@ -712,14 +743,15 @@ describe("Marketplace", function () {
}
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
while (
!(
(await marketplace.isProofRequired(id)) &&
(await marketplace.getPointer(id)) < 250
)
) {
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
}
@ -734,7 +766,8 @@ describe("Marketplace", function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilProofWillBeRequired(id)
await expect(await marketplace.willProofBeRequired(id)).to.be.true
await advanceTimeTo(request.expiry + 1)
await waitUntilCancelled(request)
await mine()
await expect(await marketplace.willProofBeRequired(id)).to.be.false
})
@ -743,7 +776,8 @@ describe("Marketplace", function () {
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
await expect(await marketplace.isProofRequired(id)).to.be.true
await advanceTimeTo(request.expiry + 1)
await waitUntilCancelled(request)
await mine()
await expect(await marketplace.isProofRequired(id)).to.be.false
})
@ -751,9 +785,11 @@ describe("Marketplace", function () {
const id = slotId(slot)
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
await mine()
const challenge1 = await marketplace.getChallenge(id)
expect(BigNumber.from(challenge1).gt(0))
await advanceTimeTo(request.expiry + 1)
await waitUntilCancelled(request)
await mine()
const challenge2 = await marketplace.getChallenge(id)
expect(BigNumber.from(challenge2).isZero())
})
@ -762,9 +798,11 @@ describe("Marketplace", function () {
const id = slotId(slot)
await marketplace.fillSlot(slot.request, slot.index, proof)
await waitUntilProofIsRequired(id)
await mine()
const challenge1 = await marketplace.getChallenge(id)
expect(BigNumber.from(challenge1).gt(0))
await advanceTimeTo(request.expiry + 1)
await waitUntilCancelled(request)
await mine()
const challenge2 = await marketplace.getChallenge(id)
expect(BigNumber.from(challenge2).isZero())
})
@ -785,14 +823,16 @@ describe("Marketplace", function () {
})
async function waitUntilProofIsRequired(id) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await mine()
while (
!(
(await marketplace.isProofRequired(id)) &&
(await marketplace.getPointer(id)) < 250
)
) {
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
}
@ -813,7 +853,7 @@ describe("Marketplace", function () {
for (let i = 0; i < slashCriterion; i++) {
await waitUntilProofIsRequired(id)
let missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await advanceTimeForNextBlock(period+1)
await marketplace.markProofAsMissing(id, missedPeriod)
}
const expectedBalance =
@ -841,7 +881,7 @@ describe("Marketplace", function () {
)
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await advanceTimeForNextBlock(period+1)
await marketplace.markProofAsMissing(slotId(slot), missedPeriod)
}
expect(await marketplace.slotState(slotId(slot))).to.equal(SlotState.Free)
@ -865,7 +905,7 @@ describe("Marketplace", function () {
)
await waitUntilProofIsRequired(slotId(slot))
const missedPeriod = periodOf(await currentTime())
await advanceTime(period)
await advanceTimeForNextBlock(period+1)
expect(await marketplace.missingProofs(slotId(slot))).to.equal(
missedProofs
)
@ -896,6 +936,7 @@ describe("Marketplace", function () {
it("keeps request in list when cancelled", async function () {
await marketplace.requestStorage(request)
await waitUntilCancelled(request)
await mine()
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
})
@ -911,6 +952,7 @@ describe("Marketplace", function () {
switchAccount(host)
await waitUntilStarted(marketplace, request, proof, token)
await waitUntilFailed(marketplace, request)
await mine()
switchAccount(client)
expect(await marketplace.myRequests()).to.deep.equal([requestId(request)])
})
@ -963,6 +1005,7 @@ describe("Marketplace", function () {
await token.approve(marketplace.address, request.ask.collateral)
await marketplace.fillSlot(slot.request, slot1.index, proof)
await waitUntilCancelled(request)
await mine()
expect(await marketplace.mySlots()).to.have.members([
slotId(slot),
slotId(slot1),

View File

@ -7,8 +7,8 @@ const {
mine,
ensureMinimumBlockHeight,
currentTime,
advanceTime,
advanceTimeTo,
advanceTimeForNextBlock,
advanceTimeToForNextBlock,
} = require("./evm")
const { periodic } = require("./time")
const { SlotState } = require("./requests")
@ -44,13 +44,15 @@ describe("Proofs", function () {
const samples = 256 // 256 samples avoids bias due to pointer downtime
await proofs.startRequiringProofs(slotId, probability)
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
let amount = 0
for (let i = 0; i < samples; i++) {
if (await proofs.isProofRequired(slotId)) {
amount += 1
}
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
const p = 1 / probability // expected probability
@ -62,7 +64,8 @@ describe("Proofs", function () {
it("supports probability 1 (proofs are always required)", async function () {
await proofs.startRequiringProofs(slotId, 1)
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
while ((await proofs.getPointer(slotId)) < downtime) {
await mine()
}
@ -75,7 +78,8 @@ describe("Proofs", function () {
await proofs.startRequiringProofs(slotId, probability)
while (Math.floor((await currentTime()) / period) == startPeriod) {
expect(await proofs.isProofRequired(slotId)).to.be.false
await advanceTime(Math.floor(period / 10))
await advanceTimeForNextBlock(Math.floor(period / 10))
await mine()
}
})
@ -92,12 +96,14 @@ describe("Proofs", function () {
req1 = await proofs.isProofRequired(id1)
req2 = await proofs.isProofRequired(id2)
req3 = await proofs.isProofRequired(id3)
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
})
it("moves pointer one block at a time", async function () {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await mine()
for (let i = 0; i < 256; i++) {
let previous = await proofs.getPointer(slotId)
await mine()
@ -117,7 +123,7 @@ describe("Proofs", function () {
beforeEach(async function () {
await proofs.setSlotState(slotId, SlotState.Filled)
await proofs.startRequiringProofs(slotId, probability)
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await waitUntilProofWillBeRequired()
})
@ -154,14 +160,17 @@ describe("Proofs", function () {
})
async function waitUntilProofIsRequired(slotId) {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await mine()
while (
!(
(await proofs.isProofRequired(slotId)) &&
(await proofs.getPointer(slotId)) < 250
)
) {
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
}
@ -199,7 +208,7 @@ describe("Proofs", function () {
})
it("fails proof submission when already submitted", async function () {
await advanceTimeTo(periodEnd(periodOf(await currentTime())))
await advanceTimeToForNextBlock(periodEnd(periodOf(await currentTime())))
await proofs.submitProof(slotId, proof)
await expect(proofs.submitProof(slotId, proof)).to.be.revertedWith(
"Proof already submitted"
@ -210,7 +219,8 @@ describe("Proofs", function () {
expect(await proofs.missingProofs(slotId)).to.equal(0)
await waitUntilProofIsRequired(slotId)
let missedPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(missedPeriod))
await advanceTimeToForNextBlock(periodEnd(missedPeriod))
await mine()
await proofs.markProofAsMissing(slotId, missedPeriod)
expect(await proofs.missingProofs(slotId)).to.equal(1)
})
@ -226,7 +236,7 @@ describe("Proofs", function () {
it("does not mark a proof as missing after timeout", async function () {
await waitUntilProofIsRequired(slotId)
let currentPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(currentPeriod) + timeout)
await advanceTimeToForNextBlock(periodEnd(currentPeriod) + timeout)
await expect(
proofs.markProofAsMissing(slotId, currentPeriod)
).to.be.revertedWith("Validation timed out")
@ -236,7 +246,8 @@ describe("Proofs", function () {
await waitUntilProofIsRequired(slotId)
let submittedPeriod = periodOf(await currentTime())
await proofs.submitProof(slotId, proof)
await advanceTimeTo(periodEnd(submittedPeriod))
await advanceTimeToForNextBlock(periodEnd(submittedPeriod))
await mine()
await expect(
proofs.markProofAsMissing(slotId, submittedPeriod)
).to.be.revertedWith("Proof was submitted, not missing")
@ -244,10 +255,12 @@ describe("Proofs", function () {
it("does not mark proof as missing when not required", async function () {
while (await proofs.isProofRequired(slotId)) {
await advanceTime(period)
await advanceTimeForNextBlock(period)
await mine()
}
let currentPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(currentPeriod))
await advanceTimeToForNextBlock(periodEnd(currentPeriod))
await mine()
await expect(
proofs.markProofAsMissing(slotId, currentPeriod)
).to.be.revertedWith("Proof was not required")
@ -256,7 +269,8 @@ describe("Proofs", function () {
it("does not mark proof as missing twice", async function () {
await waitUntilProofIsRequired(slotId)
let missedPeriod = periodOf(await currentTime())
await advanceTimeTo(periodEnd(missedPeriod))
await advanceTimeToForNextBlock(periodEnd(missedPeriod))
await mine()
await proofs.markProofAsMissing(slotId, missedPeriod)
await expect(
proofs.markProofAsMissing(slotId, missedPeriod)

View File

@ -14,6 +14,13 @@ async function revert() {
await ethers.provider.send("evm_setNextBlockTimestamp", [time + 1])
}
/**
* Mines new block.
*
* This call increases the block's timestamp by 1!
*
* @returns {Promise<void>}
*/
async function mine() {
await ethers.provider.send("evm_mine")
}
@ -29,16 +36,30 @@ async function currentTime() {
return block.timestamp
}
async function advanceTime(seconds) {
/**
* Function that advances time by adding seconds to current timestamp for **next block**.
*
* If you need the timestamp to be already applied for current block then mine a new block with `mine()` after this call.
* This is mainly needed when doing assertions on top of view calls that does not create transactions and mine new block.
*
* @param timestamp
* @returns {Promise<void>}
*/
async function advanceTimeForNextBlock(seconds) {
await ethers.provider.send("evm_increaseTime", [seconds])
await mine()
}
async function advanceTimeTo(timestamp) {
if ((await currentTime()) !== timestamp) {
await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp])
await mine()
}
/**
* Function that sets specific timestamp for **next block**.
*
* If you need the timestamp to be already applied for current block then mine a new block with `mine()` after this call.
* This is mainly needed when doing assertions on top of view calls that does not create transactions and mine new block.
*
* @param timestamp
* @returns {Promise<void>}
*/
async function advanceTimeToForNextBlock(timestamp) {
await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp])
}
module.exports = {
@ -47,6 +68,6 @@ module.exports = {
mine,
ensureMinimumBlockHeight,
currentTime,
advanceTime,
advanceTimeTo,
advanceTimeForNextBlock,
advanceTimeToForNextBlock,
}

View File

@ -1,9 +1,9 @@
const { advanceTimeTo } = require("./evm")
const { advanceTimeToForNextBlock, currentTime } = require("./evm")
const { slotId, requestId } = require("./ids")
const {price} = require("./price");
async function waitUntilCancelled(request) {
await advanceTimeTo(request.expiry + 1)
await advanceTimeToForNextBlock(request.expiry + 1)
}
async function waitUntilStarted(contract, request, proof, token) {
@ -16,7 +16,7 @@ async function waitUntilStarted(contract, request, proof, token) {
async function waitUntilFinished(contract, requestId) {
const end = (await contract.requestEnd(requestId)).toNumber()
await advanceTimeTo(end + 1)
await advanceTimeToForNextBlock(end + 1)
}
async function waitUntilFailed(contract, request) {

View File

@ -14,6 +14,7 @@ const SlotState = {
Finished: 2,
Failed: 3,
Paid: 4,
Cancelled: 5,
}
const enableRequestAssertions = function () {