nim-codex/tests/contracts/testMarket.nim

557 lines
23 KiB
Nim

import std/options
import std/importutils
import pkg/chronos
import pkg/ethers/erc20
import codex/contracts
import ../ethertest
import ./examples
import ./time
import ./deployment
privateAccess(OnChainMarket) # enable access to private fields
# to see supportive information in the test output
# use `-d:"chronicles_enabled_topics:testMarket:DEBUG` option
# when compiling the test file
logScope:
topics = "testMarket"
ethersuite "On-Chain Market":
let proof = Groth16Proof.example
var market: OnChainMarket
var marketplace: Marketplace
var token: Erc20Token
var request: StorageRequest
var slotIndex: UInt256
var periodicity: Periodicity
var host: Signer
var otherHost: Signer
var hostRewardRecipient: Address
proc expectedPayout(r: StorageRequest, startTimestamp: UInt256, endTimestamp: UInt256): UInt256 =
return (endTimestamp - startTimestamp) * r.ask.reward
proc switchAccount(account: Signer) =
marketplace = marketplace.connect(account)
token = token.connect(account)
market = OnChainMarket.new(marketplace, market.rewardRecipient)
setup:
let address = Marketplace.address(dummyVerifier = true)
marketplace = Marketplace.new(address, ethProvider.getSigner())
let config = await marketplace.configuration()
hostRewardRecipient = accounts[2]
market = OnChainMarket.new(marketplace)
let tokenAddress = await marketplace.token()
token = Erc20Token.new(tokenAddress, ethProvider.getSigner())
periodicity = Periodicity(seconds: config.proofs.period)
request = StorageRequest.example
request.client = accounts[0]
host = ethProvider.getSigner(accounts[1])
otherHost = ethProvider.getSigner(accounts[3])
slotIndex = (request.ask.slots div 2).u256
proc advanceToNextPeriod() {.async.} =
let currentPeriod = periodicity.periodOf(await ethProvider.currentTime())
await ethProvider.advanceTimeTo(periodicity.periodEnd(currentPeriod) + 1)
proc advanceToCancelledRequest(request: StorageRequest) {.async.} =
let expiry = (await market.requestExpiresAt(request.id)) + 1
await ethProvider.advanceTimeTo(expiry.u256)
proc waitUntilProofRequired(slotId: SlotId) {.async.} =
await advanceToNextPeriod()
while not (
(await marketplace.isProofRequired(slotId)) and
(await marketplace.getPointer(slotId)) < 250
):
await advanceToNextPeriod()
test "caches marketplace configuration":
check isNone market.configuration
discard await market.periodicity()
check isSome market.configuration
test "fails to instantiate when contract does not have a signer":
let storageWithoutSigner = marketplace.connect(ethProvider)
expect AssertionDefect:
discard OnChainMarket.new(storageWithoutSigner)
test "knows signer address":
check (await market.getSigner()) == (await ethProvider.getSigner().getAddress())
test "can retrieve proof periodicity":
let periodicity = await market.periodicity()
let config = await marketplace.configuration()
let periodLength = config.proofs.period
check periodicity.seconds == periodLength
test "can retrieve proof timeout":
let proofTimeout = await market.proofTimeout()
let config = await marketplace.configuration()
check proofTimeout == config.proofs.timeout
test "supports marketplace requests":
await market.requestStorage(request)
test "can retrieve previously submitted requests":
check (await market.getRequest(request.id)) == none StorageRequest
await market.requestStorage(request)
let r = await market.getRequest(request.id)
check (r) == some request
test "withdraws funds to client":
let clientAddress = request.client
await market.requestStorage(request)
await advanceToCancelledRequest(request)
let startBalanceClient = await token.balanceOf(clientAddress)
await market.withdrawFunds(request.id)
let endBalanceClient = await token.balanceOf(clientAddress)
check endBalanceClient == (startBalanceClient + request.price)
test "supports request subscriptions":
var receivedIds: seq[RequestId]
var receivedAsks: seq[StorageAsk]
proc onRequest(id: RequestId, ask: StorageAsk, expiry: UInt256) =
receivedIds.add(id)
receivedAsks.add(ask)
let subscription = await market.subscribeRequests(onRequest)
await market.requestStorage(request)
check eventually receivedIds == @[request.id] and receivedAsks == @[request.ask]
await subscription.unsubscribe()
test "supports filling of slots":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
test "can retrieve host that filled slot":
await market.requestStorage(request)
check (await market.getHost(request.id, slotIndex)) == none Address
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check (await market.getHost(request.id, slotIndex)) == some accounts[0]
test "supports freeing a slot":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await market.freeSlot(slotId(request.id, slotIndex))
check (await market.getHost(request.id, slotIndex)) == none Address
test "supports checking whether proof is required now":
check (await market.isProofRequired(slotId(request.id, slotIndex))) == false
test "supports checking whether proof is required soon":
check (await market.willProofBeRequired(slotId(request.id, slotIndex))) == false
test "submits proofs":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await advanceToNextPeriod()
await market.submitProof(slotId(request.id, slotIndex), proof)
test "marks a proof as missing":
let slotId = slotId(request, slotIndex)
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
await market.markProofAsMissing(slotId, missingPeriod)
check (await marketplace.missingProofs(slotId)) == 1
test "can check whether a proof can be marked as missing":
let slotId = slotId(request, slotIndex)
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
check (await market.canProofBeMarkedAsMissing(slotId, missingPeriod)) == true
test "supports slot filled subscriptions":
await market.requestStorage(request)
var receivedIds: seq[RequestId]
var receivedSlotIndices: seq[UInt256]
proc onSlotFilled(id: RequestId, slotIndex: UInt256) =
receivedIds.add(id)
receivedSlotIndices.add(slotIndex)
let subscription = await market.subscribeSlotFilled(onSlotFilled)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check eventually receivedIds == @[request.id] and receivedSlotIndices == @[slotIndex]
await subscription.unsubscribe()
test "subscribes only to a certain slot":
var otherSlot = slotIndex - 1
await market.requestStorage(request)
var receivedSlotIndices: seq[UInt256]
proc onSlotFilled(requestId: RequestId, slotIndex: UInt256) =
receivedSlotIndices.add(slotIndex)
let subscription = await market.subscribeSlotFilled(request.id, slotIndex, onSlotFilled)
await market.reserveSlot(request.id, otherSlot)
await market.fillSlot(request.id, otherSlot, proof, request.ask.collateral)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
check eventually receivedSlotIndices == @[slotIndex]
await subscription.unsubscribe()
test "supports slot freed subscriptions":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
var receivedRequestIds: seq[RequestId] = @[]
var receivedIdxs: seq[UInt256] = @[]
proc onSlotFreed(requestId: RequestId, idx: UInt256) =
receivedRequestIds.add(requestId)
receivedIdxs.add(idx)
let subscription = await market.subscribeSlotFreed(onSlotFreed)
await market.freeSlot(slotId(request.id, slotIndex))
check eventually receivedRequestIds == @[request.id] and receivedIdxs == @[slotIndex]
await subscription.unsubscribe()
test "supports slot reservations full subscriptions":
let account2 = ethProvider.getSigner(accounts[2])
let account3 = ethProvider.getSigner(accounts[3])
await market.requestStorage(request)
var receivedRequestIds: seq[RequestId] = @[]
var receivedIdxs: seq[UInt256] = @[]
proc onSlotReservationsFull(requestId: RequestId, idx: UInt256) =
receivedRequestIds.add(requestId)
receivedIdxs.add(idx)
let subscription =
await market.subscribeSlotReservationsFull(onSlotReservationsFull)
await market.reserveSlot(request.id, slotIndex)
switchAccount(account2)
await market.reserveSlot(request.id, slotIndex)
switchAccount(account3)
await market.reserveSlot(request.id, slotIndex)
check eventually receivedRequestIds == @[request.id] and receivedIdxs == @[slotIndex]
await subscription.unsubscribe()
test "support fulfillment subscriptions":
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onFulfillment(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
for slotIndex in 0..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
check eventually receivedIds == @[request.id]
await subscription.unsubscribe()
test "subscribes only to fulfillment of a certain request":
var otherRequest = StorageRequest.example
otherRequest.client = accounts[0]
await market.requestStorage(request)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
proc onFulfillment(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeFulfillment(request.id, onFulfillment)
for slotIndex in 0..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
for slotIndex in 0..<otherRequest.ask.slots:
await market.reserveSlot(otherRequest.id, slotIndex.u256)
await market.fillSlot(otherRequest.id, slotIndex.u256, proof, otherRequest.ask.collateral)
check eventually receivedIds == @[request.id]
await subscription.unsubscribe()
test "support request cancelled subscriptions":
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onRequestCancelled(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
await advanceToCancelledRequest(request)
await market.withdrawFunds(request.id)
check eventually receivedIds == @[request.id]
await subscription.unsubscribe()
test "support request failed subscriptions":
await market.requestStorage(request)
var receivedIds: seq[RequestId]
proc onRequestFailed(id: RequestId) =
receivedIds.add(id)
let subscription = await market.subscribeRequestFailed(request.id, onRequestFailed)
for slotIndex in 0..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
for slotIndex in 0..request.ask.maxSlotLoss:
let slotId = request.slotId(slotIndex.u256)
while true:
let slotState = await marketplace.slotState(slotId)
if slotState == SlotState.Repair or slotState == SlotState.Failed:
break
await waitUntilProofRequired(slotId)
let missingPeriod = periodicity.periodOf(await ethProvider.currentTime())
await advanceToNextPeriod()
discard await marketplace.markProofAsMissing(slotId, missingPeriod).confirm(1)
check eventually receivedIds == @[request.id]
await subscription.unsubscribe()
test "subscribes only to a certain request cancellation":
var otherRequest = request
otherRequest.nonce = Nonce.example
await market.requestStorage(request)
await market.requestStorage(otherRequest)
var receivedIds: seq[RequestId]
proc onRequestCancelled(requestId: RequestId) =
receivedIds.add(requestId)
let subscription = await market.subscribeRequestCancelled(request.id, onRequestCancelled)
await advanceToCancelledRequest(otherRequest) # shares expiry with otherRequest
await market.withdrawFunds(otherRequest.id)
await market.withdrawFunds(request.id)
check eventually receivedIds == @[request.id]
await subscription.unsubscribe()
test "supports proof submission subscriptions":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
await advanceToNextPeriod()
var receivedIds: seq[SlotId]
proc onProofSubmission(id: SlotId) =
receivedIds.add(id)
let subscription = await market.subscribeProofSubmission(onProofSubmission)
await market.submitProof(slotId(request.id, slotIndex), proof)
check eventually receivedIds == @[slotId(request.id, slotIndex)]
await subscription.unsubscribe()
test "request is none when unknown":
check isNone await market.getRequest(request.id)
test "can retrieve active requests":
await market.requestStorage(request)
var request2 = StorageRequest.example
request2.client = accounts[0]
await market.requestStorage(request2)
check (await market.myRequests()) == @[request.id, request2.id]
test "retrieves correct request state when request is unknown":
check (await market.requestState(request.id)) == none RequestState
test "can retrieve request state":
await market.requestStorage(request)
for slotIndex in 0..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
check (await market.requestState(request.id)) == some RequestState.Started
test "can retrieve active slots":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex - 1)
await market.fillSlot(request.id, slotIndex - 1, proof, request.ask.collateral)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId1 = request.slotId(slotIndex - 1)
let slotId2 = request.slotId(slotIndex)
check (await market.mySlots()) == @[slotId1, slotId2]
test "returns none when slot is empty":
await market.requestStorage(request)
let slotId = request.slotId(slotIndex)
check (await market.getActiveSlot(slotId)) == none Slot
test "can retrieve request details from slot id":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId = request.slotId(slotIndex)
let expected = Slot(request: request, slotIndex: slotIndex)
check (await market.getActiveSlot(slotId)) == some expected
test "retrieves correct slot state when request is unknown":
let slotId = request.slotId(slotIndex)
check (await market.slotState(slotId)) == SlotState.Free
test "retrieves correct slot state once filled":
await market.requestStorage(request)
await market.reserveSlot(request.id, slotIndex)
await market.fillSlot(request.id, slotIndex, proof, request.ask.collateral)
let slotId = request.slotId(slotIndex)
check (await market.slotState(slotId)) == SlotState.Filled
test "can query past StorageRequested events":
var request1 = StorageRequest.example
var request2 = StorageRequest.example
request1.client = accounts[0]
request2.client = accounts[0]
await market.requestStorage(request)
await market.requestStorage(request1)
await market.requestStorage(request2)
# `market.requestStorage` executes an `approve` tx before the
# `requestStorage` tx, so that's two PoA blocks per `requestStorage` call (6
# blocks for 3 calls). We don't need to check the `approve` for the first
# `requestStorage` call, so we only need to check 5 "blocks ago". "blocks
# ago".
proc getsPastRequest(): Future[bool] {.async.} =
let reqs =
await market.queryPastStorageRequestedEvents(blocksAgo = 5)
reqs.mapIt(it.requestId) == @[request.id, request1.id, request2.id]
check eventually await getsPastRequest()
test "can query past SlotFilled events":
await market.requestStorage(request)
await market.reserveSlot(request.id, 0.u256)
await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral)
let slotId = request.slotId(slotIndex)
# `market.fill` executes an `approve` tx before the `fillSlot` tx, so that's
# two PoA blocks per `fillSlot` call (6 blocks for 3 calls). We don't need
# to check the `approve` for the first `fillSlot` call, so we only need to
# check 5 "blocks ago".
let events =
await market.queryPastSlotFilledEvents(blocksAgo = 5)
check events == @[
SlotFilled(requestId: request.id, slotIndex: 0.u256),
SlotFilled(requestId: request.id, slotIndex: 1.u256),
SlotFilled(requestId: request.id, slotIndex: 2.u256),
]
test "can query past SlotFilled events since given timestamp":
await market.requestStorage(request)
await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
# The SlotFilled event will be included in the same block as
# the fillSlot transaction. If we want to ignore the SlotFilled event
# for this first slot, we need to jump to the next block and use the
# timestamp of that block as our "fromTime" parameter to the
# queryPastSlotFilledEvents function.
await ethProvider.advanceTime(10.u256)
let (_, fromTime) =
await ethProvider.blockNumberAndTimestamp(BlockTag.latest)
await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral)
let events = await market.queryPastSlotFilledEvents(
fromTime = fromTime.truncate(SecondsSince1970))
check events == @[
SlotFilled(requestId: request.id, slotIndex: 1.u256),
SlotFilled(requestId: request.id, slotIndex: 2.u256)
]
test "queryPastSlotFilledEvents returns empty sequence of events when " &
"no SlotFilled events have occurred since given timestamp":
await market.requestStorage(request)
await market.reserveSlot(request.id, 0.u256)
await market.reserveSlot(request.id, 1.u256)
await market.reserveSlot(request.id, 2.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
await market.fillSlot(request.id, 1.u256, proof, request.ask.collateral)
await market.fillSlot(request.id, 2.u256, proof, request.ask.collateral)
await ethProvider.advanceTime(10.u256)
let (_, fromTime) =
await ethProvider.blockNumberAndTimestamp(BlockTag.latest)
let events = await market.queryPastSlotFilledEvents(
fromTime = fromTime.truncate(SecondsSince1970))
check events.len == 0
test "past event query can specify negative `blocksAgo` parameter":
await market.requestStorage(request)
check eventually (
(await market.queryPastStorageRequestedEvents(blocksAgo = -2)) ==
(await market.queryPastStorageRequestedEvents(blocksAgo = 2))
)
test "pays rewards and collateral to host":
await market.requestStorage(request)
let address = await host.getAddress()
switchAccount(host)
await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 1..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
let requestEnd = await market.getRequestEnd(request.id)
await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
let startBalance = await token.balanceOf(address)
await market.freeSlot(request.slotId(0.u256))
let endBalance = await token.balanceOf(address)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalance == (startBalance + expectedPayout + request.ask.collateral)
test "pays rewards to reward recipient, collateral to host":
market = OnChainMarket.new(marketplace, hostRewardRecipient.some)
let hostAddress = await host.getAddress()
await market.requestStorage(request)
switchAccount(host)
await market.reserveSlot(request.id, 0.u256)
await market.fillSlot(request.id, 0.u256, proof, request.ask.collateral)
let filledAt = (await ethProvider.currentTime()) - 1.u256
for slotIndex in 1..<request.ask.slots:
await market.reserveSlot(request.id, slotIndex.u256)
await market.fillSlot(request.id, slotIndex.u256, proof, request.ask.collateral)
let requestEnd = await market.getRequestEnd(request.id)
await ethProvider.advanceTimeTo(requestEnd.u256 + 1)
let startBalanceHost = await token.balanceOf(hostAddress)
let startBalanceReward = await token.balanceOf(hostRewardRecipient)
await market.freeSlot(request.slotId(0.u256))
let endBalanceHost = await token.balanceOf(hostAddress)
let endBalanceReward = await token.balanceOf(hostRewardRecipient)
let expectedPayout = request.expectedPayout(filledAt, requestEnd.u256)
check endBalanceHost == (startBalanceHost + request.ask.collateral)
check endBalanceReward == (startBalanceReward + expectedPayout)