Skip to content
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
156 changes: 156 additions & 0 deletions packages/contracts/helpers/tasks/set-market-fee-recipient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { ContractTransactionResponse, getAddress, ZeroAddress } from 'ethers'
import { task, types } from 'hardhat/config'
import { HardhatRuntimeEnvironment } from 'hardhat/types'

/**
* Sets where a market's fee goes.
*
* TellerV2 pays the marketplace fee out of every loan it funds, to
* `MarketRegistry.getMarketFeeRecipient(marketId)`. A market created without
* one set - which is every market `bootstrap-markets` creates - falls back to
* the market owner, i.e. the deployer. This points it somewhere else.
*
* It governs loans funded from now on. A fee already paid stays where it went.
*
* Same shape as `set-market-payment-default`: ids come off the registry, not
* the bootstrap receipt, so it runs on any chain; a market owned by another
* address is reported and skipped rather than attempted and reverted; and the
* value is read back after the write, because `getMarketFeeRecipient` falls
* back to the owner when unset and a receipt alone proves nothing.
*
* yarn hh set-market-fee-recipient --network robinhood \
* --recipient 0x... --markets 3 --dry-run true
*/

interface MarketRegistryLike {
marketCount: () => Promise<bigint>
getMarketOwner: (marketId: bigint) => Promise<string>
getMarketFeeRecipient: (marketId: bigint) => Promise<string>
getMarketplaceFee: (marketId: bigint) => Promise<bigint>
setMarketFeeRecipient: (
marketId: bigint,
recipient: string
) => Promise<ContractTransactionResponse>
}

/** "1,2, 3" -> [1n, 2n, 3n], rejecting anything that is not a market id. */
const parseMarketIds = (raw: string): bigint[] => {
const ids = raw
.split(',')
.map((part) => part.trim())
.filter((part) => part.length > 0)
.map((part) => {
if (!/^\d+$/.test(part)) {
throw new Error(`"${part}" is not a market id. Expected e.g. 1,2,3,4`)
}
return BigInt(part)
})

return [...new Set(ids)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
}

task(
'set-market-fee-recipient',
"Sets the address a market's marketplace fee is paid to"
)
.addParam('recipient', 'Address to receive the market fee', undefined, types.string)
.addParam(
'markets',
'Comma-separated market ids. Required: a fee recipient is never swept across every market by default.',
undefined,
types.string
)
.addOptionalParam(
'dryRun',
'Print what would be written without sending transactions',
false,
types.boolean
)
.setAction(async (args, hre: HardhatRuntimeEnvironment): Promise<void> => {
const { network, ethers } = hre

// getAddress throws on a malformed address and on a wrong EIP-55 checksum,
// which is the point: this is where the money goes.
const recipient = getAddress(String(args.recipient).trim())
if (recipient === ZeroAddress) {
throw new Error(
'--recipient is the zero address. MarketRegistry reads that as "unset" and pays the owner.'
)
}

const marketIds = parseMarketIds(args.markets as string)
if (marketIds.length === 0) {
throw new Error('--markets is empty. Name the market ids to change.')
}

const registryContract = await hre.contracts.get('MarketRegistry')
const registry = registryContract as unknown as MarketRegistryLike
const registryAddress = await registryContract.getAddress()

const [signer] = await ethers.getSigners()
const sender = await signer.getAddress()
const count = await registry.marketCount()

console.log(`network ${network.name} (${network.config.chainId})`)
console.log(`registry ${registryAddress}`)
console.log(`signer ${sender}`)
console.log(`recipient ${recipient}`)
console.log(`markets ${marketIds.join(', ')} of ${count} on the registry`)

const outOfRange = marketIds.filter((id) => id > count || id === 0n)
if (outOfRange.length > 0) {
throw new Error(
`Market id(s) ${outOfRange.join(', ')} do not exist on ${
network.name
}: the registry has ${count} market(s), numbered 1..${count}.`
)
}

let written = 0
let skipped = 0

for (const marketId of marketIds) {
const owner = await registry.getMarketOwner(marketId)
const existing = await registry.getMarketFeeRecipient(marketId)
const fee = await registry.getMarketplaceFee(marketId)

console.log(`\n market ${marketId}`)
console.log(` owner ${owner}`)
console.log(` fee ${fee} bps`)
console.log(` current ${existing}`)
console.log(` new ${recipient}`)

if (owner.toLowerCase() !== sender.toLowerCase()) {
console.log(' owned by another address - skipping')
skipped++
continue
}

if (existing.toLowerCase() === recipient.toLowerCase()) {
console.log(' unchanged - skipping')
skipped++
continue
}

if (args.dryRun) continue

const tx = await registry.setMarketFeeRecipient(marketId, recipient)
const rcpt = await tx.wait()
console.log(` -> ${rcpt?.hash ?? tx.hash}`)

const after = await registry.getMarketFeeRecipient(marketId)
console.log(` confirmed ${after}`)
if (after.toLowerCase() !== recipient.toLowerCase()) {
throw new Error(
`Market ${marketId} still pays ${after} after the write. Expected ${recipient}.`
)
}
written++
}

console.log(
`\ndone: ${written} market(s) written, ${skipped} skipped${
args.dryRun ? ' (dry run: nothing sent)' : ''
}`
)
})
43 changes: 43 additions & 0 deletions packages/contracts/scripts/deploy-chain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@
# PAYMENT_DEFAULT_DRY_RUN=true
# with SET_PAYMENT_DEFAULT, print before/after and
# send nothing.
# SET_MARKET_FEE_RECIPIENT=true
# point markets' marketplace fee at a new address.
# Needs MARKET_FEE_RECIPIENT and MARKET_FEE_MARKETS.
# MARKET_FEE_RECIPIENT=<address>
# where the fee goes from now on.
# MARKET_FEE_MARKETS=<ids>
# comma-separated market ids. Required: never swept
# across every market by default.
# MARKET_FEE_DRY_RUN=true with SET_MARKET_FEE_RECIPIENT, print before/after
# and send nothing.
# SWAP_VIA_LIFI=true swap one ERC-20 for another from the deployer,
# routed by LI.FI, and stop. Needs SWAP_FROM, SWAP_TO
# and SWAP_AMOUNT. For funding a pool whose principal
Expand Down Expand Up @@ -748,6 +758,38 @@ if [ "${SET_PAYMENT_DEFAULT:-}" = "true" ]; then
exit 0
fi

# Point a market's marketplace fee somewhere other than its owner.
#
# A market with no fee recipient pays its owner, and every market the bootstrap
# creates is owned by the deployer - so without this the fee on every loan lands
# in a burner key. Market ids are required rather than defaulting to the whole
# registry: redirecting fees is a decision about specific markets.
if [ "${SET_MARKET_FEE_RECIPIENT:-}" = "true" ]; then
[ -d "deployments/$NETWORK" ] || fail \
"SET_MARKET_FEE_RECIPIENT needs deployments/$NETWORK in this checkout."
[ -n "${MARKET_FEE_RECIPIENT:-}" ] || fail \
"SET_MARKET_FEE_RECIPIENT is set but MARKET_FEE_RECIPIENT is not."
[ -n "${MARKET_FEE_MARKETS:-}" ] || fail \
"SET_MARKET_FEE_RECIPIENT is set but MARKET_FEE_MARKETS is not. Name the markets."
[ -n "${DEPLOYER_MNEMONIC:-}" ] || fail "DEPLOYER_MNEMONIC is not set."
printf '%s' "$DEPLOYER_MNEMONIC" > mnemonic.secret
chmod 600 mnemonic.secret
trap 'rm -f mnemonic.secret' EXIT

if [ "${MARKET_FEE_DRY_RUN:-}" = "true" ]; then
log "Set market fee recipient on $NETWORK markets ${MARKET_FEE_MARKETS} to ${MARKET_FEE_RECIPIENT} (dry run — nothing will be sent)"
yarn hh set-market-fee-recipient --network "$NETWORK" \
--recipient "$MARKET_FEE_RECIPIENT" --markets "$MARKET_FEE_MARKETS" --dry-run true
else
log "Set market fee recipient on $NETWORK markets ${MARKET_FEE_MARKETS} to ${MARKET_FEE_RECIPIENT}"
yarn hh set-market-fee-recipient --network "$NETWORK" \
--recipient "$MARKET_FEE_RECIPIENT" --markets "$MARKET_FEE_MARKETS"
fi

log "Done — $NETWORK (market fee recipient)"
exit 0
fi

# Make a Uniswap V3 pool's TWAP readable.
#
# A pool priced off a TWAP needs its oracle pool to hold observations spanning
Expand Down Expand Up @@ -1168,6 +1210,7 @@ if [ "${DEPLOY_PROTOCOL:-}" != "true" ]; then

BOOTSTRAP_MARKETS=true create this chain's markets and lender pools
SET_PRICE_CAPS=true cap existing pools at their current oracle price
SET_MARKET_FEE_RECIPIENT=true send a market's fee to a new address
RUN_TAGS=<tags> run named deploy tags against a deployed chain
VERIFY_ONLY=true verify already-deployed contracts
REDEEM_POOL=true take the deployer's own deposit back out of a pool
Expand Down
Loading