From e8e9820d5b2ad04a37a8e6b2fa7fd33563485dcd Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Tue, 17 Sep 2024 06:18:15 +0200 Subject: [PATCH] feat: add `--payout-address` (#870) * feat: add `--payout-address` Allows SPs to be paid out to a separate address, keeping their profits secure. Supports https://github.com/codex-storage/codex-contracts-eth/pull/144 in the nim-codex client. * Remove optional payoutAddress Change --payout-address so that it is no longer optional. There is no longer an overload in `Marketplace.sol` for `fillSlot` accepting no `payoutAddress`. * Update integration tests to include --payout-address * move payoutAddress from fillSlot to freeSlot * Update integration tests to use required payoutAddress - to make payoutAddress required, the integration tests needed to avoid building the cli params until just before starting the node, otherwise if cli params were added ad-hoc, there would be an error after a non-required parameter was added before a required parameter. * support client payout address - withdrawFunds requires a withdrawAddress parameter, directs payouts for withdrawing of client funds (for a cancelled request) to go to that address. * fix integration test adds --payout-address to validators * refactor: support withdrawFunds and freeSlot optional parameters - withdrawFunds has an optional parameter for withdrawRecipient - freeSlot has optional parameters for rewardRecipient and collateralRecipient - change --payout-address to --reward-recipient to match contract signature naming * Revert "Update integration tests to include --payout-address" This reverts commit 8f9535cf35b0f2b183ac4013a7ed11b246486964. There are some valid improvements to the integration tests, but they can be handled in a separate PR. * small fix * bump contracts to fix marketplace spec * bump codex-contracts-eth, now rebased on master * bump codex-contracts-eth now that feat/reward-address has been merged to master * clean up, comments --- codex/codex.nim | 2 +- codex/conf.nim | 5 +++ codex/contracts/market.nim | 27 +++++++++++- codex/contracts/marketplace.nim | 2 + tests/contracts/testContracts.nim | 22 ++++++++++ tests/contracts/testMarket.nim | 72 ++++++++++++++++++++++++++++++- vendor/codex-contracts-eth | 2 +- 7 files changed, 127 insertions(+), 5 deletions(-) diff --git a/codex/codex.nim b/codex/codex.nim index 6d98a5622..302362b51 100644 --- a/codex/codex.nim +++ b/codex/codex.nim @@ -110,7 +110,7 @@ proc bootstrapInteractions( quit QuitFailure let marketplace = Marketplace.new(marketplaceAddress, signer) - let market = OnChainMarket.new(marketplace) + let market = OnChainMarket.new(marketplace, config.rewardRecipient) let clock = OnChainClock.new(provider) var client: ?ClientInteractions diff --git a/codex/conf.nim b/codex/conf.nim index fb7548c77..7c1957ec0 100644 --- a/codex/conf.nim +++ b/codex/conf.nim @@ -293,6 +293,11 @@ type name: "validator-max-slots" .}: int + rewardRecipient* {. + desc: "Address to send payouts to (eg rewards and refunds)" + name: "reward-recipient" + .}: Option[EthAddress] + case persistenceCmd* {. defaultValue: noCmd command }: PersistenceCmd diff --git a/codex/contracts/market.nim b/codex/contracts/market.nim index 3e9cfa0b3..1595b4497 100644 --- a/codex/contracts/market.nim +++ b/codex/contracts/market.nim @@ -19,17 +19,24 @@ type OnChainMarket* = ref object of Market contract: Marketplace signer: Signer + rewardRecipient: ?Address MarketSubscription = market.Subscription EventSubscription = ethers.Subscription OnChainMarketSubscription = ref object of MarketSubscription eventSubscription: EventSubscription -func new*(_: type OnChainMarket, contract: Marketplace): OnChainMarket = +func new*( + _: type OnChainMarket, + contract: Marketplace, + rewardRecipient = Address.none): OnChainMarket = + without signer =? contract.signer: raiseAssert("Marketplace contract should have a signer") + OnChainMarket( contract: contract, signer: signer, + rewardRecipient: rewardRecipient ) proc raiseMarketError(message: string) {.raises: [MarketError].} = @@ -163,7 +170,23 @@ method fillSlot(market: OnChainMarket, method freeSlot*(market: OnChainMarket, slotId: SlotId) {.async.} = convertEthersError: - discard await market.contract.freeSlot(slotId).confirm(0) + var freeSlot: Future[?TransactionResponse] + if rewardRecipient =? market.rewardRecipient: + # If --reward-recipient specified, use it as the reward recipient, and use + # the SP's address as the collateral recipient + let collateralRecipient = await market.getSigner() + freeSlot = market.contract.freeSlot( + slotId, + rewardRecipient, # --reward-recipient + collateralRecipient) # SP's address + + else: + # Otherwise, use the SP's address as both the reward and collateral + # recipient (the contract will use msg.sender for both) + freeSlot = market.contract.freeSlot(slotId) + + discard await freeSlot.confirm(0) + method withdrawFunds(market: OnChainMarket, requestId: RequestId) {.async.} = diff --git a/codex/contracts/marketplace.nim b/codex/contracts/marketplace.nim index 03001a790..f98b9a80e 100644 --- a/codex/contracts/marketplace.nim +++ b/codex/contracts/marketplace.nim @@ -26,7 +26,9 @@ proc minCollateralThreshold*(marketplace: Marketplace): UInt256 {.contract, view proc requestStorage*(marketplace: Marketplace, request: StorageRequest): ?TransactionResponse {.contract.} proc fillSlot*(marketplace: Marketplace, requestId: RequestId, slotIndex: UInt256, proof: Groth16Proof): ?TransactionResponse {.contract.} proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId): ?TransactionResponse {.contract.} +proc withdrawFunds*(marketplace: Marketplace, requestId: RequestId, withdrawAddress: Address): ?TransactionResponse {.contract.} proc freeSlot*(marketplace: Marketplace, id: SlotId): ?TransactionResponse {.contract.} +proc freeSlot*(marketplace: Marketplace, id: SlotId, rewardRecipient: Address, collateralRecipient: Address): ?TransactionResponse {.contract.} proc getRequest*(marketplace: Marketplace, id: RequestId): StorageRequest {.contract, view.} proc getHost*(marketplace: Marketplace, id: SlotId): Address {.contract, view.} proc getActiveSlot*(marketplace: Marketplace, id: SlotId): Slot {.contract, view.} diff --git a/tests/contracts/testContracts.nim b/tests/contracts/testContracts.nim index 4432160db..4b59ed625 100644 --- a/tests/contracts/testContracts.nim +++ b/tests/contracts/testContracts.nim @@ -11,6 +11,7 @@ ethersuite "Marketplace contracts": let proof = Groth16Proof.example var client, host: Signer + var rewardRecipient, collateralRecipient: Address var marketplace: Marketplace var token: Erc20Token var periodicity: Periodicity @@ -24,6 +25,8 @@ ethersuite "Marketplace contracts": setup: client = ethProvider.getSigner(accounts[0]) host = ethProvider.getSigner(accounts[1]) + rewardRecipient = accounts[2] + collateralRecipient = accounts[3] let address = Marketplace.address(dummyVerifier = true) marketplace = Marketplace.new(address, ethProvider.getSigner()) @@ -82,8 +85,27 @@ ethersuite "Marketplace contracts": let startBalance = await token.balanceOf(address) discard await marketplace.freeSlot(slotId) let endBalance = await token.balanceOf(address) + check endBalance == (startBalance + request.ask.duration * request.ask.reward + request.ask.collateral) + test "can be paid out at the end, specifying reward and collateral recipient": + switchAccount(host) + let hostAddress = await host.getAddress() + await startContract() + let requestEnd = await marketplace.requestEnd(request.id) + await ethProvider.advanceTimeTo(requestEnd.u256 + 1) + let startBalanceHost = await token.balanceOf(hostAddress) + let startBalanceReward = await token.balanceOf(rewardRecipient) + let startBalanceCollateral = await token.balanceOf(collateralRecipient) + discard await marketplace.freeSlot(slotId, rewardRecipient, collateralRecipient) + let endBalanceHost = await token.balanceOf(hostAddress) + let endBalanceReward = await token.balanceOf(rewardRecipient) + let endBalanceCollateral = await token.balanceOf(collateralRecipient) + + check endBalanceHost == startBalanceHost + check endBalanceReward == (startBalanceReward + request.ask.duration * request.ask.reward) + check endBalanceCollateral == (startBalanceCollateral + request.ask.collateral) + test "cannot mark proofs missing for cancelled request": let expiry = await marketplace.requestExpiry(request.id) await ethProvider.advanceTimeTo((expiry + 1).u256) diff --git a/tests/contracts/testMarket.nim b/tests/contracts/testMarket.nim index bb01aa774..a78a414ef 100644 --- a/tests/contracts/testMarket.nim +++ b/tests/contracts/testMarket.nim @@ -1,30 +1,47 @@ 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 + 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 hostRewardRecipient: Address + + 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.config() + 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]) slotIndex = (request.ask.slots div 2).u256 @@ -72,11 +89,18 @@ ethersuite "On-Chain Market": let r = await market.getRequest(request.id) check (r) == some request - test "supports withdrawing of funds": + 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] @@ -370,3 +394,49 @@ ethersuite "On-Chain Market": (await market.queryPastEvents(StorageRequested, blocksAgo = -2)) == (await market.queryPastEvents(StorageRequested, blocksAgo = 2)) ) + + test "pays rewards and collateral to host": + await market.requestStorage(request) + + let address = await host.getAddress() + switchAccount(host) + + for slotIndex in 0..