diff --git a/docs/contracts/access/hashregistry.md b/docs/contracts/access/hashregistry.md index 866b4006..0bc57924 100644 --- a/docs/contracts/access/hashregistry.md +++ b/docs/contracts/access/hashregistry.md @@ -90,7 +90,7 @@ Instead, the signer can be swapped out to make their signatures invalid. ### Delegation signatures -The delegation signatures are in the following [ERC-191](https://eips.ethereum.org/EIPS/eip-191) format: +The [delegation](../../glossary.md#delegation) signatures are in the following [ERC-191](https://eips.ethereum.org/EIPS/eip-191) format: ```solidity bytes32 ethSignedMessageHash = toEthSignedMessageHash( diff --git a/docs/contracts/api3-server-v1/airseekerregistry.md b/docs/contracts/api3-server-v1/airseekerregistry.md index 5718ebbd..ee8fdb7d 100644 --- a/docs/contracts/api3-server-v1/airseekerregistry.md +++ b/docs/contracts/api3-server-v1/airseekerregistry.md @@ -1,24 +1,24 @@ # AirseekerRegistry -All API3 [data feeds](../../glossary.md#data-feed) are served over the [Api3ServerV1](./api3serverv1.md) contract. -[Airseeker](../../glossary.md#airseeker) is a piece of API3 data feed infrastructure that pushes [API provider](../../glossary.md#api-provider)-[signed data](../../glossary.md#signed-data) to Api3ServerV1 when the conditions specified on AirseekerRegistry are satisfied. +[Base feeds](../../glossary.md#base-feed) are served over the [Api3ServerV1](./api3serverv1.md) contract. +[Airseeker](../../glossary.md#airseeker) is a piece of API3 infrastructure that pushes [API provider](../../glossary.md#api-provider)-[signed data](../../glossary.md#signed-data) to Api3ServerV1 when the conditions specified on AirseekerRegistry are satisfied. In other words, AirseekerRegistry is an on-chain configuration file for Airseeker. This is preferred for two reasons: - The reconfiguration of data feed infrastructure through a redeployment or an API call is error-prone and should be avoided. On-chain reconfiguration is preferable because it can be restricted according to rules enforced by a contract (e.g., a multisig would require a specific number of signatures), which may reduce the probability of errors and severity of consequences. - The on-chain reconfiguration can be integrated to other contracts to streamline the process. - For example, [Api3MarketV2](./api3marketv2.md) automatically updates AirseekerRegistry based on user payments, removing the need for any manual steps. + For example, [Api3MarketV2](./api3marketv2.md) automatically updates AirseekerRegistry based on user payments made over the [API3 Market](../../glossary.md#api3-market) frontend, removing the need for any manual steps. ## How Airseeker uses AirseekerRegistry Airseeker periodically checks if any of the active data feeds on AirseekerRegistry needs to be updated (according to the on-chain state and respective [update parameters](../../glossary.md#update-parameters)), and updates the ones that do. `activeDataFeed()` is used for this, which returns all data that Airseeker needs about a data feed with a specific index. -To reduce the number of RPC calls, Airseeker batches these calls using `multicall()`. -The first of these multicalls includes an `activeDataFeedCount()` call, which tells Airseeker how many multicalls it should make to fetch data for all active data feeds (e.g., if Airseeker is making calls in batches of 10 and there are 44 active data feeds, 5 multicalls would need to be made). +To reduce the number of RPC calls, Airseeker batches these calls using [SelfMulticall](../utils/selfmulticall.md)'s `multicall()`. +The first of these multicalls includes an `activeDataFeedCount()` call, which tells Airseeker how many multicalls it should make to fetch data for all active data feeds (e.g., if Airseeker is making calls in batches of 10 and there are 44 active data feeds, 5 multicalls would need to be made.) In the case that the active data feeds change (in that they become activated/deactivated) while Airseeker is making these multicalls, Airseeker may fetch the same feed in two separate batches, or miss a data feed. -This is deemed acceptable, assuming that active data feeds will not change very frequently and Airseeker will run its update loop very frequently (meaning that any missed data feed will be handled on the next iteration). +This is deemed acceptable, assuming that active data feeds will not change very frequently and Airseeker will run its update loop very frequently (meaning that any missed data feed will be handled on the next iteration.) Let us go over what `activeDataFeed()` returns. @@ -41,7 +41,7 @@ function activeDataFeed(uint256 index) `activeDataFeed()` returns `dataFeedId` and `dapiName`. `dataFeedId` and `dapiName` are not needed for the update functionality, and are only provided for Airseeker to refer to in its logs. -`dataFeedDetails` is contract ABI-encoded [Airnode addresses](../../glossary.md#airnode-address) and [template](../../glossary.md#template) IDs belonging to the data feed identified by `dataFeedId`. +`dataFeedDetails` is contract ABI-encoded [Airnode addresses](../../glossary.md#airnode-address) and [template](../../glossary.md#template) IDs belonging to the [data feed](../../glossary.md#data-feed) identified by `dataFeedId`. When a [signed API](../../glossary.md#signed-api) is called through the URL `$SIGNED_API_URL/public/$AIRNODE_ADDRESS`, it returns an array of signed data, which is keyed by template IDs (e.g., https://signed-api.api3.org/public/0xc52EeA00154B4fF1EbbF8Ba39FDe37F1AC3B9Fd4). Therefore, `dataFeedDetails` is all Airseeker needs to fetch the signed data it will use to update the data feed. @@ -49,7 +49,7 @@ Therefore, `dataFeedDetails` is all Airseeker needs to fetch the signed data it These values are compared with the aggregation of the values returned by the signed APIs to determine if an update is necessary. `beaconValues` and `beaconTimestamps` are the current values of the constituent [Beacons](../../glossary.md#beacon) of the data feed identified by `dataFeedId`. Airseeker updates data feeds through a multicall of individual calls that update each underlying Beacon, followed by a call that updates the [Beacon set](../../glossary.md#beacon-set) using the Beacons. -Having the Beacon readings allows Airseeker to predict the outcome of the individual Beacon updates and omit them as necessary (e.g., if the on-chain Beacon value is fresher than what the signed API returns, which guarantees that that Beacon update will revert, Airseeker does not attempt to update that Beacon). +Having the Beacon readings allows Airseeker to predict the outcome of the individual Beacon updates and omit them as necessary (e.g., if the on-chain Beacon value is fresher than what the signed API returns, which guarantees that that Beacon update will revert, Airseeker does not attempt to update that Beacon.) `updateParameters` is contract ABI-encoded update parameters in a format that Airseeker recognizes. Currently, the only format used is @@ -94,14 +94,14 @@ The owner is responsible with leaving the state of this contract in a way that A Otherwise, Airseeker behavior is not defined (but it can be expected that the respective data feed will not be updated under any condition). The points to consider while activating a data feed name are as follow: -- If a [dAPI](../../glossary.md#dapi) name is being used, it should be set at [Api3ServerV1](./api3serverv1.md) +- If a [dAPI](../../glossary.md#dapi) name is being used, it should be set at [Api3ServerV1](./api3serverv1.md). - The data feed should be registered by calling `registerDataFeed()`. If a dAPI name has been used, this should be repeated whenever the dAPI name is updated. -- The update parameters of the data feed should be set by calling `setDataFeedIdUpdateParameters()` or `setDapiNameUpdateParameters()` +- The update parameters of the data feed should be set by calling `setDataFeedIdUpdateParameters()` or `setDapiNameUpdateParameters()`. - The signed API URLs of the respective Airnodes should be set by calling `setSignedApiUrl()`. If a dAPI name has been used, this should be repeated whenever the dAPI name is updated. The signed API URL of an Airnode may change, in which case this should be reflected on AirseekerRegistry by calling `setSignedApiUrl()` again. -- The respective [sponsor wallet](../../glossary.md#sponsor-wallet) should be funded +- The respective [sponsor wallet](../../glossary.md#sponsor-wallet) should be funded. Note that some of the steps above imply a need for maintenance when dAPI names change, signed API URLs change and sponsor wallets run out. It is recommended to run automated workers to handle these cases, or at least these aspects should be monitored and responsible parties should be alerted when an intervention is needed. diff --git a/docs/contracts/api3-server-v1/api3marketv2.md b/docs/contracts/api3-server-v1/api3marketv2.md index c6a8c38f..ee3b35b3 100644 --- a/docs/contracts/api3-server-v1/api3marketv2.md +++ b/docs/contracts/api3-server-v1/api3marketv2.md @@ -3,7 +3,7 @@ API3 users interact with Api3MarketV2 over the [API3 market](../../glossary.md#api3-market) frontend to purchase [data feed](../../glossary.md#data-feed) [subscriptions](../../glossary.md#subscription). Api3MarketV2 has an accompanying [AirseekerRegistry](./airseekerregistry.md) that it owns. User interactions update AirseekerRegistry, which immediately reconfigures the respective [Airseeker](../../glossary.md#airseeker). -For example, buying a subscription for a [dAPI](../../glossary.md#dapi) that is currently deactivated will activate it and set its [update parameters](../../glossary.md#update-parameters) to the ones from the subscription, causing Airseeker to immediately start executing updates as specified. +For example, buying a subscription for a [dAPI](../../glossary.md#dapi) that is currently deactivated will activate it and set its [update parameters](../../glossary.md#update-parameters) as the ones from the subscription plan, causing Airseeker to immediately start executing updates as specified. ## The owner @@ -86,25 +86,27 @@ This section describes what happens under the hood of the API3 Market frontend. The requirements for a `buySubscription()` call to succeed are as follow: -- The `dapiManagementAndDapiPricingMerkleData`, which prove that the rest of the arguments are from the Merkle trees whose roots are currently registered on Api3MarketV2, are valid -- The subscription can be added to the queue of the dAPI, which means that it objectively improves the queue and does not cause it to exceed the maximum limit of `maximumSubscriptionQueueLength` (whose value is determined at Api3MarketV2 deployment) items -- The data feed is registered at AirseekerRegistry -- The data feed has been updated at most a day ago -- The call sends enough funds that when forwarded to the sponsor wallet, the balance of the sponsor wallet exceeds what `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns +- The `dapiManagementAndDapiPricingMerkleData`, which prove that the rest of the arguments are from the Merkle trees whose roots are currently registered on Api3MarketV2, are valid. +- The subscription can be added to the queue of the dAPI, which means that it objectively improves the queue and does not cause it to exceed the maximum limit of `maximumSubscriptionQueueLength` (whose value is determined at Api3MarketV2 deployment) items. +- The data feed is registered at AirseekerRegistry. +- The data feed has been updated at most a day ago. +- The call sends enough funds that when forwarded to the sponsor wallet, the balance of the sponsor wallet exceeds what `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns. -The user should first fetch the Merkle leaf values and proofs for the subscription they wish to purchase, and call `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` with the arguments. +The client should first fetch the Merkle leaf values and proofs for the subscription they wish to purchase, and call `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` with the arguments. This call reverting indicates that the subscription cannot be added to the queue of the dAPI, so the subscription cannot be purchased. -In the case that it does not revert, the user should check the sponsor wallet balance to find out how much they need to pay for the subscription (if any). +In the case that it does not revert, the client should check the sponsor wallet balance to find out how much they need to pay for the subscription (if any). Here, it is a good practice to overestimate for the probability of the sponsor wallet sending a transaction before the subscription purchase can be confirmed. For example, say the sponsor wallet balance is `1 ETH`, `computeExpectedSponsorWalletBalanceAfterSubscriptionIsAdded()` returns `2 ETH`, and the user is buying a 30 day subscription whose price is `1.5 ETH`. The daily price of the subscription would be `1.5 / 30 = 0.05 ETH`, which would be a decent headroom. Then, instead of sending `2 - 1 = 1 ETH`, the user could send `1 + 0.05 = 1.05 ETH`, which would be very unlikely to revert if the price is accurate. -Before making the `buySubscription()` call, the user should make sure that the data feed is registered at AirseekerRegistry and the data feed has been updated in the last day at Api3ServerV1. +Before making the `buySubscription()` call, the client should make sure that the data feed is registered at AirseekerRegistry and the data feed has been updated in the last day at Api3ServerV1. For that, they can do a static multicall to `[getDataFeedData(), registerDataFeed(), getDataFeedData()]`, where the returndata of the first `getDataFeedData()` indicate if the data feed needs to be registered, and the returndata of the second `getDataFeedData()` indicate if respective Beacons or Beacon set need to be updated. If the data feed needs to be registered and/or updated, these can be done in a single multicall that finally calls `buySubscription()`. The data feed details needed to register the data feed would most likely be fetched from the same source that serves the Merkle tree data, and the signed data needed to update the data feed would be fetched from a signed API. +For convenience, the client can also add the [Api3ReaderProxyV1](./proxies/api3readerproxyv1.md) deployment to the subscription purchase multicall by calling `deployApi3ReaderProxyV1()`. + One thing to note here is that data feed updates revert when they are not successful (e.g., because another party updated the data feed with data whose [timestamp](./api3serverv1.md#data-feed-timestamps) is more recent than what the user has attempted to use), and thus the multicall should be done with `tryMulticallAndBuySubscription()`. Another pitfall here is that calling `eth_estimateGas` with `tryMulticallAndBuySubscription()` will return an arbitrary amount (because `eth_estimateGas` looks for a gas limit that causes the transaction to not revert and `tryMulticallAndBuySubscription()` never reverts in the multicall phase by design), which is why the `eth_estimateGas` call should be done with `multicallAndBuySubscription()`. diff --git a/docs/contracts/api3-server-v1/api3serverv1.md b/docs/contracts/api3-server-v1/api3serverv1.md index 3fe88744..8116c682 100644 --- a/docs/contracts/api3-server-v1/api3serverv1.md +++ b/docs/contracts/api3-server-v1/api3serverv1.md @@ -20,11 +20,11 @@ In practice, downtime and latency of individual parties is a common issue, and t However, the alternative to requiring full participation causes a critical vulnerability. Say that the data from 21 parties are aggregated in a synchronous manner. -It is generally understood that this aggregation can be compromised with at least 11 malicious parties. -However, to avoid data feed-level downtime, data feeds that are updated synchronously are often configured to be able to update even if some of the parties are late or unavailable. -For example, Chainlink deems 14 responses to be adequate to come to an initial consensus in such a setting. -This means that in the case that 7 parties are coincidentally unresponsive or even only late (due to a node bug, a global Cloudflare outage, etc.), 14 parties will be allowed to form a consensus, where 7 malicious parties are enough to compromise the data feed. -Therefore, the security guarantees of synchronously updated data feeds are much lower than what the general public is led to believe (in this case, a 34% attack suffices to control the data feed rather than the implied 51%). +It is generally misunderstood that this aggregation can be compromised with at least 11 malicious parties. +However, to avoid data feed-level downtime, data feeds that are updated synchronously are often configured to be able to come to an initial consensus even if some of the parties are late or unavailable. +(For example, 14 out of 21 is often deemed to be adequate for an initial consensus in such a setting.) +This means that in the case that 7 parties are coincidentally unresponsive or even merely late (due to a node bug, a global Cloudflare outage, etc.), 14 parties will be allowed to form an initial consensus, where 7 malicious parties are enough to compromise the data feed. +Therefore, the security guarantees of synchronously updated data feeds are much lower than what the general public is led to believe (in this example, a 34% attack suffices to control the data feed rather than the implied 51%). As a solution to this issue, Api3ServerV1 enables each [API provider](../../glossary.md#api-provider) to maintain a single-source data feed of their own, a [Beacon](../../glossary.md#beacon), and enables arbitrary combinations of these Beacons to be aggregated on-chain to form [Beacon sets](../../glossary.md#beacon-set). In the case that an issue debilitates the infrastructure of an API provider, their individual Beacon will stop getting updated, yet its most recent value will continue contributing to the Beacon set aggregation. @@ -43,19 +43,19 @@ Each Beacon can only be updated with signed data whose timestamp is larger than Since Beacon sets can only be aggregated out of Beacons and the Beacon set timestamp is the median of the timestamps of the respective Beacons, Beacon set timestamps also never decrease. (As a note, Beacon set updates that keep the timestamp the same are allowed if the aggregation result changes, considering that the contributing Beacon timestamps must have increased.) -Although data feed timestamps are mainly nonces that prevent replay attacks, they have a secondary functionality of indicating the freshness of the data. +Although data feed timestamps are mainly nonces that prevent replay attacks, they have a secondary function of indicating the freshness of the data. For example, if one expects the [heartbeat](../../glossary.md#heartbeat) interval of a data feed to be one day, they can safely require the respective timestamp to be no older than one day. As a note, some alternative data feed implementations use the timestamp of the block in which the data feed is updated as the update timestamp. -Since our data feed timestamp is a more realistic measure of freshness, it will lag a few seconds behind this value, which allows them to be used interchangeably in most cases. +Since our data feed timestamp is a more realistic measure of freshness, it will lag a few seconds behind this value, which still allows them to be used interchangeably in most cases. However, this may not be the case if the chain time drifts or the data feed timestamps are misreported. Since understanding the implications of this difference to the full extent is difficult, the users are not recommended to use the data feed timestamp in their contract logic beyond validating a heartbeat interval requirement. ## dAPIs -The data feed IDs in Api3ServerV1 immutably define a specific aggregation of calls to respective API providers. -This is fully trust-minimized, [first-party oracle](../../glossary.md#first-party-oracles)-based setup. -However, [dApps](../../glossary.md#dapp) often require a fully-managed solution, where the API provider curation and the maintenance of API provider integrations are handled by experts. +The data feed IDs in Api3ServerV1 immutably define a specific aggregation of API calls to respective API providers. +This is the trust-minimized, [first-party oracle](../../glossary.md#first-party-oracles)-based setup. +However, [dApps](../../glossary.md#dapp) often require a fully-managed solution, where the API provider curation and the maintenance of API provider integrations are handled by domain experts. Api3ServerV1 implements [dAPI](../../glossary.md#dapi) names, which are `bytes32`-type strings that are mapped to the immutable data feed IDs mentioned above. Accounts that have the "dAPI name setter" role are able to update this mapping, which at this moment are the [manager multisig](../../glossary.md#manager-multisig) and [Api3MarketV2](./api3marketv2.md). diff --git a/docs/contracts/api3-server-v1/api3serverv1oevextension.md b/docs/contracts/api3-server-v1/api3serverv1oevextension.md index 97239155..7630ed59 100644 --- a/docs/contracts/api3-server-v1/api3serverv1oevextension.md +++ b/docs/contracts/api3-server-v1/api3serverv1oevextension.md @@ -2,15 +2,15 @@ Api3ServerV1OevExtension extends [Api3ServerV1](./api3serverv1.md) and implements [OEV](../../glossary.md#oev) functionality, supplanting OevDataFeedServer and OevDapiServer. Api3ServerV1OevExtension houses [OEV feeds](../../glossary.md#oev-feed) updated by [searchers](../../glossary.md#searcher) that win the respective [OEV auctions](../../glossary.md#oev-auction). -Although these base feeds can be read by calling Api3ServerV1OevExtension directly, the users are recommended to read them (together with [base feeds](../../glossary.md#base-feed) from Api3ServerV1) through [proxy contracts](../../glossary.md#proxy), which implement a convenient interface. +Although these OEV feeds can be read by calling Api3ServerV1OevExtension directly, the users are recommended to read them (together with [base feeds](../../glossary.md#base-feed) from Api3ServerV1) through [proxy contracts](../../glossary.md#proxy), which implement a convenient interface. ## How do OEV feeds get updated? The base feeds of Api3ServerV1 can be updated by anyone using the [signed data](../../glossary.md#signed-data) served by the respective [signed APIs](../../glossary.md#signed-api). The OEV feeds of Api3ServerV1OevExtension are similar in that they are also updated using signed data fetched from signed APIs. -However, to be able to execute these updates, one needs to have won an OEV auction and paid the respective bid amount. +However, to be able to execute these updates, one needs to have won an OEV auction and paid the respective [bid](../../glossary.md#bid) amount. -While placing a bid at [OevAuctionHouse](./oevauctionhouse.md), a searcher basically states "I want the account with the address `0x1234....cdef` to be able to update the OEV feeds of the dApp with ID `1337` using signed data whose timestamp is at most `1726848498`, and I am willing to pay `10 ETH` to be able to do so." +While placing a bid at [OevAuctionHouse](./oevauctionhouse.md), a searcher basically states "I want the account with the address `0x1234....cdef` to be able to update the OEV feeds of the dApp with ID `1337` using signed data whose [timestamp](./api3serverv1.md#data-feed-timestamps) is at most `1726848498`, and I am willing to pay `10 ETH` to be able to do so." If the searcher has [deposited](../../glossary.md#deposit) enough [collateral](../../glossary.md#collateral) and `10 ETH` is the largest bid amount, the [auction resolver](../../glossary.md#auction-resolver) responds with an [award](../../glossary.md#award) transaction, which includes a signature that states "I allow the account with address `0x1234....cdef` to be able to update the OEV feeds of the dApp with ID `1337` using signed data whose timestamp is at most `1726848498` if it pays `10 ETH`." The searcher can then use the account with the address `0x1234....cdef` to call `payOevBid()` of Api3ServerV1OevExtension and pay `10 ETH`, which allows the same account to update the respective OEV feeds using signed data with the timestamp limitation. @@ -30,26 +30,35 @@ To achieve this, where `T` is one [auction period](../../glossary.md#auction-per In other words, there are three uses of signed data across time: 1. Signed API publishes real-time data signed to allow the auction winner to execute OEV feed updates. - Searchers use this to simulate OEV feed updates to determine their bid amounts. -1. Searchers that have placed any bids based on the data above store the published data, and use them to execute OEV updates in case they win the auction. + Searchers use this to [simulate](#simulating-oev-extraction) OEV feed updates to determine their bid amounts. +1. Searchers that have placed any bids based on the data above store the published data, and use them to [execute](#executing-oev-extraction) OEV updates in case they win the auction. 1. Signed API publishes `2T`-delayed data signed to allow anyone to execute base feed updates. This is used for regular [MEV](../../glossary.md#mev) extraction and upholding data feed specs such as [deviation](../../glossary.md#deviation) threshold and [heartbeat](../../glossary.md#heartbeat) interval. -## Extracting OEV with a multicall +## Simulating OEV extraction + +Api3ServerV1OevExtension implements two functions, `simulateDappOevDataFeedUpdate()` and `simulateExternalCall()`, for searchers to be able to simulate OEV extraction with real-time signed data. +Both of these functions can only be called by `address(0)`, i.e., the searcher is intended to call these using `eth_call` while impersonating `address(0)`. + +`simulateDappOevDataFeedUpdate()` allows the searcher to simulate OEV feed updates with real-time signed data without any further requirements. +`simulateExternalCall()` allows the searcher to call their OEV extraction contract (e.g., one that liquidates positions making use of flash loans) in the same multicall that has called `simulateDappOevDataFeedUpdate()` before and return data that will inform the searcher of the expected revenue from OEV, which they will use to decide their bid amount. + +## Executing OEV extraction The winning searcher makes three calls: -1. Using the account whose address that they have specified in their bid details, call `payOevBid()` of Api3ServerV1OevExtension to assume OEV update privileges -1. Using the same account, call `updateDappOevDataFeedWithAllowedSignedData()` to execute an OEV update -1. Call the target dApp to extract OEV +1. Using the account whose address that they have specified in their bid details, call `payOevBid()` of Api3ServerV1OevExtension to assume OEV update privileges. +1. Using the same account, call `updateDappOevDataFeedWithAllowedSignedData()` to execute an OEV update. +1. Call the target dApp to extract OEV. Steps 2 and 3 must be done in a multicall to prevent third-parties from interjecting between and stealing the OEV. Furthermore, steps 1 and 3 must be done in the same multicall to utilize a flash loan to cover the bid amount. Therefore, steps 1, 2 and 3 are intended to be done in the same multicall, where steps 2 and 3 can be repeated with different updates. -Since the bid only specifies the address of the contract that is intended to make this multicall, that contract needs to be personalized (e.g., put its interface behind `onlyOwner`) for third-parties to not be able to action on the awarded bid. +Since the bid only specifies the address of the contract that is intended to make this multicall, that contract needs to be personalized (e.g., put its interface behind `onlyOwner`) for third-parties to not be able to act on the awarded bid. ## How are the OEV auction proceeds handled? The OEV auction proceeds will be paid out to the parties that best represent the respective dApps through a protocol that is beyond the scope of the contracts in this repo. The [manager](../../glossary.md#manager) of this contract or an account that it granted the withdrawer role to can withdraw as much of the OEV auction proceeds as they wish, and send these funds to the respective accounts as necessary. +Considering that OEV proceeds will form a slow and steady flow that will be paid out frequently, this was not seen as a security issue and was preferred for its flexibility. diff --git a/docs/contracts/api3-server-v1/oevauctionhouse.md b/docs/contracts/api3-server-v1/oevauctionhouse.md index 47ae9124..65f9ee05 100644 --- a/docs/contracts/api3-server-v1/oevauctionhouse.md +++ b/docs/contracts/api3-server-v1/oevauctionhouse.md @@ -11,15 +11,15 @@ OEV auctions are done on-chain to address two issues: Considering that we are building the OEV auction platform for all dApps living on all chains, simply scaling up the infrastructure to meet this demand is not realistic, and we should have a mechanism to downregulate the demand. This is a long-solved problem in blockchain transactions through the gas fee, and thus hosting the auctions on-chain is an obvious solution to this problem. 2. An OEV auction is an oracle service in essence, for which it is important to be able to prove a good track record. - For this, a paper trail of the entire communication between the [auctioneer](../../glossary.md#auctioneer) and the searchers need to be kept, and a blockchain is a natural solution to this. + For this, a paper trail of the entire communication between the [auctioneers](../../glossary.md#auctioneer) and the searchers need to be kept, and a blockchain is a natural solution to this. Consider this for a counterexample: - A searcher claims that they call the auctioneer API to make bids that should win, but the auctioneer keeps [awarding](../../glossary.md#award) the updates to other, smaller bids. + A searcher claims that they call the (hypothetical) auctioneer API to make bids that should win, but the auctioneer keeps [awarding](../../glossary.md#award) the updates to other, smaller bids. The auctioneer would not be able to disprove this claim, as it is not possible to prove that an API call has not been received, yet whether an on-chain transaction has been confirmed is conclusively verifiable. ## Auction schedule Each dApp has a single, independent auction track. -When a searcher wins an auction for a dApp, they gain OEV feed update privileges across all dAPIs that the dApp uses. +When a searcher wins an auction for a dApp, they gain OEV feed update privileges across all [dAPIs](../../glossary.md#dapi) that the dApp uses. Therefore, this section describes the schedule of a single auction track, which covers all OEV opportunities of a dApp. Auctions take a fixed amount of time, happen periodically, and are packed tightly. @@ -45,21 +45,22 @@ Therefore, for this dApp, a new auction will start at every UNIX timestamp `k * ## Collateral and protocol fee -Whenever a bidder wins an auction, OevAuctionHouse locks up some of their funds to charge either a collateral or a protocol fee. +Whenever a bidder wins an auction, OevAuctionHouse locks up some of their funds to charge either a [collateral](../../glossary.md#collateral) or a [protocol fee](../../glossary.md#protocol-fee). In the case that the winner promptly pays their bid and report that they have done so, they are charged a protocol fee out of the locked funds and the rest of the funds gets released. Failing to do so results in them being charged a collateral amount and the rest of the funds gets released. -Since the initial implementation, we have decided to implement the protocol monetization logic elsewhere. -As a result, the protocol fee in this contract will be set to zero. -In the rest of the documentation, we omit the protocol fee, as it has no effect to the flow when it is set to zero. +> [!WARNING] +> Since the initial implementation, we have decided to implement the protocol monetization logic elsewhere. +> As a result, the protocol fee in this contract will be set to zero. +> In the rest of the documentation, we omit the protocol fee, as it has no effect to the flow when it is set to zero. ## Searcher flow - Call `deposit()` at OevAuctionHouse to deposit funds to be used as collateral. -- Continuously poll the signed API to find and store signed data that can be used to extract OEV. +- Continuously poll the [signed API](../../glossary.md#signed-api) to find and store [signed data](../../glossary.md#signed-data) that can be used to extract OEV. - Towards the end of each bid phase, calculate the total OEV amount that can be extracted during the next bid phase, and call `placeBid()` to place a bid for it if profitable. - For each placed bid, check for an award at the end of the respective award phase by listening for the `AwardedBid` event. -- For each award, call `payOevBid()` at Api3ServerV1OevExtension of the respective chain to pay the bid and assume OEV update privileges. +- For each award, call `payOevBid()` at [Api3ServerV1OevExtension](./api3serverv1oevextension.md) of the respective chain to pay the bid and assume OEV update privileges. - Use the previously stored signed data that can extract OEV during the bid phase of the next auction to capture the detected OEV opportunities. - For each paid bid, call `reportFulfillment()` at OevAuctionHouse to request for the collateral that was locked during the award to be released. @@ -92,7 +93,7 @@ In the case that `T` is much smaller than `MAXIMUM_BID_LIFETIME`, `MAXIMUM_BID_L Bidders can attempt to deny service by placing winning bids that satisfy all conditions required to win, and do one of the following while the award transaction is pending: 1. Withdraw deposited collateral -1. Cancel the bid +2. Cancel the bid To prevent the first, withdrawals are done in two steps: `initiateWithdrawal()` is called first, and `withdraw()` gets called `WITHDRAWAL_WAITING_PERIOD` after that. @@ -109,12 +110,12 @@ OevAuctionHouse inherits [AccessControlRegistryAdminnedWithManager](../access/ac Multiple accounts can be granted this role, which is allowed to: 1. Award bids (which locks up collateral) -1. Confirm fulfillments, i.e., confirm that awarded bids have been paid for (which releases the collateral) -1. Contradict fulfillments, i.e., contradict that awarded bids have been paid for (which slashes the collateral) +2. Confirm [fulfillments](../../glossary.md#fulfillment), i.e., confirm that awarded bids have been paid for (which releases the collateral) +3. Contradict fulfillments, i.e., contradict that awarded bids have been paid for (which slashes the collateral) -The first of these is done by the [auction creator](https://github.com/api3dao/oev-auctioneer/tree/main/src/auction-creator) and the last two are done by the [auction cop](https://github.com/api3dao/oev-auctioneer/tree/main/src/auction-cop). +The first of these is done by the "auction resolver" and the last two are done by the "auction cop". -### Auction creator flow +### Auction resolver flow - At the start of each award phase, fetch `PlacedBid` and `ExpeditedBidExpiration` logs during the respective bid phase. - Select the highest bid that satisfies all of the following: @@ -131,20 +132,20 @@ The first of these is done by the [auction creator](https://github.com/api3dao/o ### Finality considerations -- While determining the winner of an auction, the auction creator checks if the highest bidder has sufficient deposit to cover the collateral requirement of their bid. +- While determining the winner of an auction, the auction resolver checks if the highest bidder has sufficient deposit to cover the collateral requirement of their bid. However, the bidder can receive another award between this check and the confirmation of the award transaction, which can result in the bidder not having sufficient deposit when the award transaction gets confirmed. In this case, the auctioneer should not try awarding another bidder, as the awarded signature will already have been exposed in the reverting award transaction. -- Normally, the auction creator is expected to deliver the award within the award phase. +- Normally, the auction resolver is expected to deliver the award within the award phase. In the case that the delivery is delayed due to networking or finality issues, it is preferable to not lock up the collateral of the bidder. - For this, auction creators should use a sufficiently small `awardExpirationTimestamp` while calling `awardBid()`. + For this, auction resolvers should use a sufficiently small `awardExpirationTimestamp` while calling `awardBid()`. - It is assumed that the bidder has waited for sufficient finality before reporting their fulfillment. If the auction cop fails to confirm the bid payment due to a finality issue, it will contradict the fulfillment. ### Security implications -An auctioneer is trusted to facilitate the auction honestly (as an alternative to a trustless, on-chain order book, which has drawbacks of its own), which enables the following unwanted scenarios: +An auctioneer is trusted to facilitate the auction honestly (as an alternative to an on-chain order book), which enables the following unwanted scenarios: - It can deny service (selectively or to everyone) by not awarding bids or not confirming fulfillments. - It can contradict fulfillments that have been correctly reported. @@ -154,7 +155,7 @@ An auctioneer is trusted to facilitate the auction honestly (as an alternative t The purpose of doing the auctions on-chain is for such events (or their lack thereof) to be decisively provable. Based on the fact that the scenarios above are possible, starting from the moment a bid is created and until the fulfillment is confirmed, the respective collateral is under risk of being slashed unjustly. -Note that the auctioneer role is intended to be given to a hot wallet that a bot controls, while the contract manager is intended to be a multisig. +Note that the auctioneer role is intended to be given to a hot wallet that a bot controls, while the contract [manager](../../glossary.md#manager) is intended to be a [multisig](../../glossary.md#manager-multisig). Therefore, in the event of an unjust slashing, the funds become accessible to the multisig, and not the hot wallet. In such an occasion, the issue is intended to be resolved retrospectively by the multisig based on the on-chain records through an off-chain dispute resolution mechanism. @@ -173,10 +174,10 @@ The following parameters are used to calculate the bid topic: - `majorVersion`: A positive integer that specifies the major version of the auctioneer. Any breaking change in the behavior of the auctioneer, which can involve changes in auction rules or off-chain protocol specs, is denoted by this major version being incremented. -- `dappId`: A positive integer that specifies a dApp on a specific chain. +- [`dappId`](../../glossary.md#dapp-id): A positive integer that specifies a dApp on a specific chain. A single dApp deployed on multiple chains will have a different `dappId` for each chain deployment. -- `startTimestamp`: The start timestamp of the bid phase during which the bidder wants to execute OEV updates. -- `endTimestamp`: The end timestamp of the bid phase during which the bidder wants to execute OEV updates. +- `auctionLength`: The [auction period](../../glossary.md#auction-period) in seconds. +- `signedDataTimestampCutoff`: The largest signed data timestamp that the searcher will be allowed to use to update the respective OEV feeds if they win the auction. The bid topic is calculated as follows: @@ -184,13 +185,13 @@ The bid topic is calculated as follows: ethers.utils.keccak256( ethers.utils.solidityPack( ['uint256', 'uint256', 'uint256', 'uint256'], - [majorVersion, dappId, startTimestamp, endTimestamp] + [majorVersion, dappId, auctionLength, signedDataTimestampCutoff] ) ); ``` -By using this bid topic, the bidder confirms the major version that they are operating on, the dApp they are bidding for, and between which timestamps they want to be able to execute OEV updates. -Note that the addition of the timestamps to the bid topic implies that the bid topic changes for every auction. +By using this bid topic, the bidder confirms the major version that they are operating on, the dApp they are bidding for, and the period during which they will be able to execute OEV updates. +Note that the `signedDataTimestampCutoff` in the bid topic implies that the bid topic changes for every auction. ### `bidDetails` @@ -221,8 +222,8 @@ The fulfillment report is the hash of the transaction that the winner has sent w ### Sealed bids In a future major version, sealed bids may be supported. -For this, `dappId`, `startTimestamp` and `endTimestamp` would need to be transferred to `bidDetails`, and a non-auction specific `bidTopic` convention would need to be chosen (e.g., `keccak256(abi.encodePacked(majorVersion, auctioneerId))`). -Then, `bidDetails` can simply be `abi.encode(dappId, startTimestamp, endTimestamp, nonce)` encrypted using the public key that the auctioneer has announced, e.g., using RSA-4096. +For this, `dappId` and `signedDataTimestampCutoff` would need to be transferred to `bidDetails`, and a non-auction specific `bidTopic` convention would need to be chosen (e.g., `keccak256(abi.encodePacked(majorVersion, auctionLength, auctioneerId))`). +Then, `bidDetails` can simply be `abi.encode(dappId, signedDataTimestampCutoff, updateSenderAddress, nonce)` encrypted using the public key that the auctioneer has announced, e.g., using RSA-4096. This disables auctioneers from being able to filter logs at the RPC level, which creates the need for a centralized indexer. ## Privileged accounts @@ -266,7 +267,6 @@ Optionally, the manager can delegate the proxy setting and withdrawing responsib ## On interacting with OevAuctionHouse through a contract -As with all contracts, interactions with OevAuctionHouse are originated from an EOA sending a transaction (assuming a pre-[EIP-3074](https://eips.ethereum.org/EIPS/eip-3074) world). In the case that an EOA calls OevAuctionHouse directly, `bidder` is the address of this EOA. This means that the EOA will be able to place bids (by calling `placeBidWithExpiration()` or `placeBid()`), report fulfillments (by calling `reportFulfillment()`), withdraw (by calling `initiateWithdrawal()` and `withdraw()` in succession), or cancel ongoing withdrawals (by calling `cancelWithdrawal()`). @@ -274,7 +274,7 @@ In case that the user wants to limit the privileges of the EOA that interacts wi As a toy example, say we have lended our capital to a searcher bot operator to capture OEV on our behalf. However, we do not want the bot operator to be able to withdraw our capital. We could implement a contract that forwards the `placeBidWithExpiration()`, `placeBid()` and `reportFulfillment()` calls from the bot operator EOA, and the `initiateWithdrawal()`, `withdraw()` and `cancelWithdrawal()` calls from our EOA. -(As a note, this does not prevent the bot operator from burning through the funds by placing invalid bids, nor does it guarantee that the bot operator will share the revenue, which is why this is called a toy example.) +(As a note, this does not prevent the bot operator from burning through the funds by placing invalid bids, nor does it guarantee that the bot operator will share the revenue.) Below are the important points to consider while implementing a contract that calls `placeBidWithExpiration()` and/or `placeBid()`: diff --git a/docs/contracts/api3-server-v1/proxies/api3readerproxyv1.md b/docs/contracts/api3-server-v1/proxies/api3readerproxyv1.md index 515af535..21580ae8 100644 --- a/docs/contracts/api3-server-v1/proxies/api3readerproxyv1.md +++ b/docs/contracts/api3-server-v1/proxies/api3readerproxyv1.md @@ -1,22 +1,22 @@ # Api3ReaderProxyV1 -Api3ReaderProxyV1 is an upgradeable [proxy](../../../glossary.md#proxy) that should be used to read API3 [dAPIs](../../../glossary.md#dapi). -It implements [IApi3ReaderProxy](../../interfaces/iapi3readerproxy.md) as a first-class citizen, and partially implements Chainlink's AggregatorV2V3Interface for convenience (refer to https://github.com/api3dao/migrate-from-chainlink-to-api3 for more information about the latter). +Api3ReaderProxyV1 is an upgradeable [proxy](../../../glossary.md#proxy) that API3 [dAPIs](../../../glossary.md#dapi) should be read through. +It implements [IApi3ReaderProxy](../../interfaces/iapi3readerproxy.md) as a first-class citizen, and partially implements Chainlink's AggregatorV2V3Interface for convenience (refer to https://github.com/api3dao/migrate-from-chainlink-to-api3 for more information about the latter.) Api3ReaderProxyV1 is a UUPS-upgradeable proxy with a proxy-specific implementation. This unusual design makes the contract upgradeable while minimizing storage reads at runtime. To ensure that the implementation and proxy pair is deployed correctly, [Api3ReaderProxyV1Factory](./api3readerproxyv1factory.md) should be used. While purchasing a [subscription](../../../glossary.md#subscription), the [API3 Market](../../../glossary.md#api3-market) frontend has the user deploy a generic Api3ReaderProxyV1 with the [dApp ID](../../../glossary.md#dapp-id) `1`. -If the [dApp](../../../glossary.md#dapp) wishes to receive [OEV auction](../../../glossary.md#oev-auction) proceeds, they should deploy an Api3ReaderProxyV1 with their own specific dApp ID using the provided graphical interface and use that instead. +If the [dApp](../../../glossary.md#dapp) wishes to receive [OEV auction](../../../glossary.md#oev-auction) proceeds, they should deploy an Api3ReaderProxyV1 with their own specific dApp ID and use that instead. ## Combining a base feed and an OEV feed -Each Api3ReaderProxyV1 is meant for a dApp to read a dAPI. -When `read()` is called, Api3ReaderProxyV1 first reads the base feed specific to the dAPI from Api3ServerV1. +Each Api3ReaderProxyV1 is meant for a dApp to use to read a dAPI. +When `read()` is called, Api3ReaderProxyV1 first reads the [base feed](../../../glossary.md#base-feed) specific to the dAPI from [Api3ServerV1](../api3serverv1.md). The base feed provides the strongest availability guarantees possible, as it is allowed to be updated by anyone. As a side effect of its permissionless nature, the base feed exposes the entire [OEV](../../../glossary.md#oev) to the public. -Then, Api3ReaderProxyV1 reads the OEV feed specific to the dAPI–dApp pair from Api3ServerV1OevExtension. +Then, Api3ReaderProxyV1 reads the [OEV feed](../../../glossary.md#oev-feed) specific to the dAPI–dApp pair from Api3ServerV1OevExtension. The OEV feed is only allowed to be updated by the [searcher](../../../glossary.md#searcher) that won the respective OEV auction. The [signed data](../../../glossary.md#signed-data) for doing OEV feed updates is published earlier than the signed data for doing base feed updates, which prevents the base feed from exposing any OEV given that there are active searchers. diff --git a/docs/contracts/api3-server-v1/proxies/api3readerproxyv1factory.md b/docs/contracts/api3-server-v1/proxies/api3readerproxyv1factory.md index 465813ff..172f79ad 100644 --- a/docs/contracts/api3-server-v1/proxies/api3readerproxyv1factory.md +++ b/docs/contracts/api3-server-v1/proxies/api3readerproxyv1factory.md @@ -1,14 +1,16 @@ # Api3ReaderProxyV1Factory Api3ReaderProxyV1Factory is the contract that should be used to deploy [Api3ReaderProxyV1](./api3readerproxyv1.md) contracts. -It deploys both a UUPS-upgradeable proxy and an implementation, and returns the address of the proxy. +It deploys a UUPS-upgradeable proxy and the respective implementation, and returns the address of the proxy. + +## Api3ReaderProxyV1 deployment metadata Api3ReaderProxyV1Factory deploys Api3ReaderProxyV1 deterministically and Api3ReaderProxyV1 is Ownable. -This may cause the following scenario +This may cause the following scenario: -- Alice is the owner of Api3ReaderProxyV1Factory -- Api3ReaderProxyV1Factory deploys a new Api3ReaderProxyV1 (whose owner is Alice) -- Alice transfers the ownership of the Api3ReaderProxyV1 to Bob -- Alice needs to deploy another Api3ReaderProxyV1 with identical parameters and transfer its ownership to Charlie, yet cannot because of the contract addresses will collide +- Alice is the owner of Api3ReaderProxyV1Factory. +- Api3ReaderProxyV1Factory deploys a new Api3ReaderProxyV1, whose owner is Alice. +- Alice transfers the ownership of the new Api3ReaderProxyV1 to Bob. +- Alice needs to deploy another Api3ReaderProxyV1 with identical parameters (i.e., [dAPI](../../../glossary.md#dapi) name and [dApp ID](../../../glossary.md#dapp-id)) and transfer its ownership to Charlie, yet cannot because the contract addresses will collide. -As a solution, an arbitrary `metadata` value is allowed to be provided while deploying an Api3ReaderProxyV1, which acts as the deterministic deployment hash. +As a solution, an arbitrary `metadata` value is allowed to be provided while deploying an Api3ReaderProxyV1, which acts as the deterministic deployment salt. diff --git a/docs/contracts/interfaces/iapi3readerproxy.md b/docs/contracts/interfaces/iapi3readerproxy.md index 94a026cd..2c743976 100644 --- a/docs/contracts/interfaces/iapi3readerproxy.md +++ b/docs/contracts/interfaces/iapi3readerproxy.md @@ -1,5 +1,5 @@ # IApi3ReaderProxy -API3 data feeds are recommended to be read by calling the respective [proxy](../../glossary.md#proxy) through the IApi3ReaderProxy interface. +[dAPIs](../../glossary.md#dapi) are recommended to be read by calling the respective [proxy](../../glossary.md#proxy) through the IApi3ReaderProxy interface. The current proxy implementation, [Api3ReaderProxyV1](../api3-server-v1/proxies/api3readerproxyv1.md), implements IApi3ReaderProxy. It is intended for the future upgrades of Api3ReaderProxyV1 to also implement IApi3ReaderProxy, which means that using IApi3ReaderProxy should be future-proof. diff --git a/docs/contracts/utils/extendedselfmulticall.md b/docs/contracts/utils/extendedselfmulticall.md index 24b9ca36..bf3e32f8 100644 --- a/docs/contracts/utils/extendedselfmulticall.md +++ b/docs/contracts/utils/extendedselfmulticall.md @@ -1,4 +1,4 @@ # ExtendedSelfMulticall ExtendedSelfMulticall is an extended version of [SelfMulticall](./selfmulticall.md). -It allows the caller to query some account and block properties, which are specifically added to minimize [Airseeker](../../glossary.md#airseeker) RPC calls. +It allows the caller to query some account and block properties, which are specifically added to minimize the number of [Airseeker](../../glossary.md#airseeker) RPC calls. diff --git a/docs/contracts/utils/selfmulticall.md b/docs/contracts/utils/selfmulticall.md index 9750c690..7fdeb641 100644 --- a/docs/contracts/utils/selfmulticall.md +++ b/docs/contracts/utils/selfmulticall.md @@ -13,7 +13,7 @@ SelfMulticall also has an extended version, [ExtendedSelfMulticall](./extendedse ## Reading For a batch read operation, one should use `tryMulticall()` to receive a best effort response. -If all calls are guaranteed to succeed, `multicall()` can also be used instead. +If all calls are guaranteed to succeed, `multicall()` can also be used to the same effect. ## Writing diff --git a/docs/glossary.md b/docs/glossary.md index 2bfa8d91..f594e7b1 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -183,6 +183,7 @@ endpointId = keccak256(abi.encode(oisTitle, endpointName)); ## First-party oracles An [API provider](#api-provider) that provides oracle services without the use of any middlemen is a first-party oracle. +Compare to [third-party oracles](#third-party-oracles). ## Fulfillment