diff --git a/README.md b/README.md index b8907a9d..633eb0ab 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ Please check out existing EIPs, such as [EIP-1](eip-0001.md), to understand the | [EIP-0020](eip-0020.md) | ErgoPay Protocol | | [EIP-0021](eip-0021.md) | Genuine tokens verification | | [EIP-0022](eip-0022.md) | Auction Contract | +| [EIP-0023](eip-0023/eip-0023.md) | Oracle pool 2.0 | | [EIP-0024](eip-0024.md) | Artwork contract | | [EIP-0025](eip-0025.md) | Payment Request URI | | [EIP-0027](eip-0027.md) | Emission Retargeting Soft-Fork | | [EIP-0031](eip-0031.md) | Babel Fees | -| [EIP-0039](eip-0039.md) | Monotonic box creation height rule | \ No newline at end of file +| [EIP-0039](eip-0039.md) | Monotonic box creation height rule | diff --git a/eip-0023/contracts/ballot_contract.es b/eip-0023/contracts/ballot_contract.es new file mode 100644 index 00000000..7e74a613 --- /dev/null +++ b/eip-0023/contracts/ballot_contract.es @@ -0,0 +1,32 @@ +{ // This box (ballot box): + // R4 the group element of the owner of the ballot token [GroupElement] + // R5 the creation height of the update box [Int] + // R6 the value voted for [Coll[Byte]] + // R7 the reward token id [Coll[Byte]] + // R8 the reward token amount [Long] + + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual + + val minStorageRent = 10000000L // TODO replace with actual + + val selfPubKey = SELF.R4[GroupElement].get + + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.R4[GroupElement].isDefined && // ballot boxes are transferable by setting different value here + output.propositionBytes == SELF.propositionBytes && + output.tokens == SELF.tokens && + output.value >= minStorageRent + + val update = INPUTS.size > 1 && + INPUTS(1).tokens.size > 0 && + INPUTS(1).tokens(0)._1 == updateNFT && // can only update when update box is the 2nd input + output.R4[GroupElement].get == selfPubKey && // public key is preserved + output.value >= SELF.value && // value preserved or increased + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid vote + + val owner = proveDlog(selfPubKey) + + owner || (isSimpleCopy && update) +} diff --git a/eip-0023/contracts/oracle_contract.es b/eip-0023/contracts/oracle_contract.es new file mode 100644 index 00000000..4606531e --- /dev/null +++ b/eip-0023/contracts/oracle_contract.es @@ -0,0 +1,49 @@ +{ // This box (oracle box) + // R4 public key (GroupElement) + // R5 epoch counter of current epoch (Int) + // R6 data point (Long) or empty + + // tokens(0) oracle token (one) + // tokens(1) reward tokens collected (one or more) + // + // When publishing a datapoint, there must be at least one reward token at index 1 + // + // We will connect this box to pool NFT in input #0 (and not the refresh NFT in input #1) + // This way, we can continue to use the same box after updating pool + // This *could* allow the oracle box to be spent during an update + // However, this is not an issue because the update contract ensures that tokens and registers (except script) of the pool box are preserved + + // Private key holder can do following things: + // 1. Change group element (public key) stored in R4 + // 2. Store any value of type in or delete any value from R4 to R9 + // 3. Store any token or none at 2nd index + + // In order to connect this oracle box to a different refreshNFT after an update, + // the oracle should keep at least one new reward token at index 1 when publishing data-point + + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + + val otherTokenId = INPUTS(0).tokens(0)._1 + + val minStorageRent = 10000000L + val selfPubKey = SELF.R4[GroupElement].get + val outIndex = getVar[Int](0).get + val output = OUTPUTS(outIndex) + + val isSimpleCopy = output.tokens(0) == SELF.tokens(0) && // oracle token is preserved + output.propositionBytes == SELF.propositionBytes && // script preserved + output.R4[GroupElement].isDefined && // output must have a public key (not necessarily the same) + output.value >= minStorageRent // ensure sufficient Ergs to ensure no garbage collection + + val collection = otherTokenId == poolNFT && // first input must be pool box + output.tokens(1)._1 == SELF.tokens(1)._1 && // reward tokenId is preserved (oracle should ensure this contains a reward token) + output.tokens(1)._2 > SELF.tokens(1)._2 && // at least one reward token must be added + output.R4[GroupElement].get == selfPubKey && // for collection preserve public key + output.value >= SELF.value && // nanoErgs value preserved + ! (output.R5[Any].isDefined) // no more registers; prevents box from being reused as a valid data-point + + val owner = proveDlog(selfPubKey) + + // owner can choose to transfer to another public key by setting different value in R4 + isSimpleCopy && (owner || collection) +} diff --git a/eip-0023/contracts/pool_contract.es b/eip-0023/contracts/pool_contract.es new file mode 100644 index 00000000..01f243c8 --- /dev/null +++ b/eip-0023/contracts/pool_contract.es @@ -0,0 +1,16 @@ +{ + // This box (pool box) + // epoch start height is stored in creation Height (R3) + // R4 Current data point (Long) + // R5 Current epoch counter (Int) + // + // tokens(0) pool token (NFT) + // tokens(1) reward tokens + // When initializing the box, there must be one reward token. When claiming reward, one token must be left unclaimed + + val otherTokenId = INPUTS(1).tokens(0)._1 + val refreshNFT = fromBase64("VGpXblpyNHU3eCFBJUQqRy1LYU5kUmdVa1hwMnM1djg=") // TODO replace with actual + val updateNFT = fromBase64("YlFlVGhXbVpxNHQ3dyF6JUMqRi1KQE5jUmZValhuMnI=") // TODO replace with actual + + sigmaProp(otherTokenId == refreshNFT || otherTokenId == updateNFT) +} diff --git a/eip-0023/contracts/refresh_contract.es b/eip-0023/contracts/refresh_contract.es new file mode 100644 index 00000000..cdd49628 --- /dev/null +++ b/eip-0023/contracts/refresh_contract.es @@ -0,0 +1,76 @@ +{ // This box (refresh box) + // + // tokens(0) refresh token (NFT) + + val oracleTokenId = fromBase64("KkctSmFOZFJnVWtYcDJzNXY4eS9CP0UoSCtNYlBlU2g=") // TODO replace with actual + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + val epochLength = 30 // TODO replace with actual + val minDataPoints = 4 // TODO replace with actual + val buffer = 4 // TODO replace with actual + val maxDeviationPercent = 5 // percent // TODO replace with actual + + val minStartHeight = HEIGHT - epochLength + val spenderIndex = getVar[Int](0).get // the index of the data-point box (NOT input!) belonging to spender + + val poolIn = INPUTS(0) + val poolOut = OUTPUTS(0) + val selfOut = OUTPUTS(1) + + def isValidDataPoint(b: Box) = if (b.R6[Long].isDefined) { + b.creationInfo._1 >= minStartHeight && // data point must not be too old + b.tokens(0)._1 == oracleTokenId && // first token id must be of oracle token + b.R5[Int].get == poolIn.R5[Int].get // it must correspond to this epoch + } else false + + val dataPoints = INPUTS.filter(isValidDataPoint) + val pubKey = dataPoints(spenderIndex).R4[GroupElement].get + + val enoughDataPoints = dataPoints.size >= minDataPoints + val rewardEmitted = dataPoints.size * 2 // one extra token for each collected box as reward to collector + val epochOver = poolIn.creationInfo._1 < minStartHeight + + val startData = 1L // we don't allow 0 data points + val startSum = 0L + // we expect data-points to be sorted in INCREASING order + + val lastSortedSum = dataPoints.fold((startData, (true, startSum)), { + (t: (Long, (Boolean, Long)), b: Box) => + val currData = b.R6[Long].get + val prevData = t._1 + val wasSorted = t._2._1 + val oldSum = t._2._2 + val newSum = oldSum + currData // we don't have to worry about overflow, as it causes script to fail + + val isSorted = wasSorted && prevData <= currData + + (currData, (isSorted, newSum)) + } + ) + + val lastData = lastSortedSum._1 + val isSorted = lastSortedSum._2._1 + val sum = lastSortedSum._2._2 + val average = sum / dataPoints.size + + val maxDelta = lastData * maxDeviationPercent / 100 + val firstData = dataPoints(0).R6[Long].get + + proveDlog(pubKey) && + epochOver && + enoughDataPoints && + isSorted && + lastData - firstData <= maxDelta && + poolIn.tokens(0)._1 == poolNFT && + poolOut.tokens(0) == poolIn.tokens(0) && // preserve pool NFT + poolOut.tokens(1)._1 == poolIn.tokens(1)._1 && // reward token id preserved + poolOut.tokens(1)._2 >= poolIn.tokens(1)._2 - rewardEmitted && // reward token amount correctly reduced + poolOut.tokens.size == poolIn.tokens.size && // cannot inject more tokens to pool box + poolOut.R4[Long].get == average && // rate + poolOut.R5[Int].get == poolIn.R5[Int].get + 1 && // counter + poolOut.propositionBytes == poolIn.propositionBytes && // preserve pool script + poolOut.value >= poolIn.value && + poolOut.creationInfo._1 >= HEIGHT - buffer && // ensure that new box has correct start epoch height + selfOut.tokens == SELF.tokens && // refresh NFT preserved + selfOut.propositionBytes == SELF.propositionBytes && // script preserved + selfOut.value >= SELF.value +} diff --git a/eip-0023/contracts/update_contract.es b/eip-0023/contracts/update_contract.es new file mode 100644 index 00000000..846e7ce8 --- /dev/null +++ b/eip-0023/contracts/update_contract.es @@ -0,0 +1,61 @@ +{ // This box (update box): + // Registers empty + // + // ballot boxes (Inputs) + // R4 the pub key of voter [GroupElement] (not used here) + // R5 the creation height of this box [Int] + // R6 the value voted for [Coll[Byte]] (hash of the new pool box script) + // R7 the reward token id in new box (optional, if not present then reward token is preserved) + // R8 the number of reward tokens in new box (optional, if not present then reward token is preserved)) + + val poolNFT = fromBase64("RytLYlBlU2hWbVlxM3Q2dzl6JEMmRilKQE1jUWZUalc=") // TODO replace with actual + + val ballotTokenId = fromBase64("P0QoRy1LYVBkU2dWa1lwM3M2djl5JEImRSlIQE1iUWU=") // TODO replace with actual + + val minVotes = 6 // TODO replace with actual + + val poolIn = INPUTS(0) // pool box is 1st input + val poolOut = OUTPUTS(0) // copy of pool box is the 1st output + + val updateBoxOut = OUTPUTS(1) // copy of this box is the 2nd output + + // compute the hash of the pool output box. This should be the value voted for + val poolOutHash = blake2b256(poolOut.propositionBytes) + val rewardTokenId = poolOut.tokens(1)._1 + val rewardAmt = poolOut.tokens(1)._2 + + val validPoolIn = poolIn.tokens(0)._1 == poolNFT + + val validPoolOut = poolIn.tokens(0) == poolOut.tokens(0) && // NFT preserved + poolIn.value == poolOut.value && // value preserved + poolIn.R4[Long] == poolOut.R4[Long] && // rate preserved + poolIn.R5[Int] == poolOut.R5[Int] && // counter preserved + ! (poolOut.R6[Any].isDefined) + + + val validUpdateOut = updateBoxOut.tokens == SELF.tokens && + updateBoxOut.propositionBytes == SELF.propositionBytes && + updateBoxOut.value >= SELF.value && + updateBoxOut.creationInfo._1 > SELF.creationInfo._1 && + ! (updateBoxOut.R4[Any].isDefined) + + val rewardTokenPreserved = poolIn.tokens(1)._1 == rewardTokenId && // check reward token id is preserved + poolIn.tokens(1)._2 == rewardAmt // check reward token amt is preserved + + def isValidBallot(b:Box) = if (b.tokens.size > 0) { + val validRewardToken = if (b.R7[Coll[Byte]].isDefined && b.R8[Long].isDefined) { + b.R7[Coll[Byte]].get == rewardTokenId && // check rewardTokenId voted for + b.R8[Long].get == rewardAmt // check rewardTokenAmt voted for + } else rewardTokenPreserved + b.tokens(0)._1 == ballotTokenId && + b.R5[Int].get == SELF.creationInfo._1 && // ensure vote corresponds to this box by checking creation height + b.R6[Coll[Byte]].get == poolOutHash && // check proposition voted for + validRewardToken + } else false + + val ballotBoxes = INPUTS.filter(isValidBallot) + + val votesCount = ballotBoxes.fold(0L, {(accum: Long, b: Box) => accum + b.tokens(0)._2}) + + sigmaProp(validPoolIn && validPoolOut && validUpdateOut && votesCount >= minVotes) +} diff --git a/eip-0023/eip-0023.md b/eip-0023/eip-0023.md new file mode 100644 index 00000000..cfe36480 --- /dev/null +++ b/eip-0023/eip-0023.md @@ -0,0 +1,336 @@ +# Oracle Pool v2.0 + +* Author: @scalahub, @greenhat, @kettlebell, @SethDusek +* Status: Proposed +* Created: 07-Sep-2021 +* License: CC0 +* Forking: not needed + +This is a proposed update to the oracle pool v1.0 currently deployed and documented in [EIP16](https://github.com/ergoplatform/eips/blob/eip16/eip-0016.md). + +## Contents +- [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Reward Mechanism](#reward-mechanism) + - [Refresh Mechanism](#refresh-mechanism) + - [Update Mechanism](#update-mechanism) +- [Tokens](#tokens) +- [Boxes](#boxes) +- [Contracts](#contracts) + - [Pool Contract](#pool-contract) + - [Refresh Contract](#refresh-contract) + - [Oracle Contract](#oracle-contract) + - [Ballot Contract](#ballot-contract) + - [Update Contract](#update-contract) +- [Trasactions](#transactions) + - [Refresh Pool](#refresh-pool) + - [Publish Data Point](#publish-data-point) + - [Extract Reward Tokens](#extract-reward-tokens) + - [Transfer Oracle Token](#transfer-oracle-token) + - [Vote for Update](#vote-for-update) + - [Update Pool Box](#update-pool-box) + - [Update Rules](#update-rules) + - [Transfer Ballot Token](#transfer-ballot-token) + +## Introduction +In order to motivate the changes proposed in this document, consider the drawbacks of Oracle pool v1.0: + +1. Rewards generate a lot of dust +2. Current rewards are too low (related to 1) +3. There are two types of pool boxes. This makes dApps and update mechanism more complex +4. Oracle tokens are non-transferable, and so oracles are locked permanently. The same goes with ballot tokens. + +Oracle pool v2.0 aims to address the above. + +Below is a summary of the main new features in v2.0 and how it differs from v1.0. + +- **Single pool address**: This version of the pool will have only one address, the *pool address*. +- **Epoch counter**: Pool box will additionally store a counter that is incremented on each collection. This will allow more sophisticated dApps. +- **Compact pool box**: Pool box is separated from the logic of pool management, which is captured instead in a **refresh** box. This makes the pool box very small for use in other dApps. +- **Refresh box**: The refresh box is used for collecting data-points. +- **Reward in tokens**: The posting reward will be in the form of tokens instead of Ergs. These tokens can be redeemed separately, which is not part of the protocol. + The pool box emits such reward tokens. +- **No separate funding process**: The pool box emits only reward tokens and won't be handing out Ergs. Thus, there won't be a separate funding process required. +- **Reward accumulated**: We will not be creating a new box for rewarding each posting to prevent dust. Instead, the rewards will be accumulated directly in the oracle boxes. +- **Oracle boxes spent in collection**: Because the rewards must be accumulated, the oracle boxes will be considered as inputs rather than data-inputs when collecting individual rates for averaging. + These inputs will be spent, and a copy of the box with the reward will be created. + This gives us the ability to accumulate rewards, while keeping the transaction size similar to when using them as data-inputs in v1.0. + Additionally, this allows us to outsource part of the reward logic to the oracle boxes. + + **Note:** The pool box will still be used as data input in other dApps. +- **Transferable oracle tokens**: Oracle tokens are free to be transferred between public keys. +- **Similar update mechanism**: We will have the similar update mechanism as in v1.0 (threshold number of ballot token holders must vote for an update). +- **Transferable ballot tokens**: Similar to oracle tokens, the ballot tokens are free to be transferred between public keys. + +### Prerequisites + +The design below has some default parameters proposed for the ERG/USD oracle pool. + +If you want to run your own pool, for example, to serve a different pair, then the following parameters need to be decided. + +|Parameter|Symbol in code|Default value|Meaning| +|---|---|---|---| +|Epoch period|**epochLength**|30|The number of blocks for which a rate is locked in the pool box.
Each block is 2 mins on average
For a fast changing rate, use shorter value (such as 5)| +|Buffer| **buffer** | 4 | The max error allowed in the declared start height of new a epoch
Ideally, this should be the height at which tx gets mined
However, due to congestion, we allow this error margin| +|Total oracles| | 15 | The total number of oracles allowed to post data points
An oracle can post at most one data point at any time | +|Minimum data points| **minDataPoints** | 4 | The minimum number of fresh data points needed to refresh pool box
A data point is fresh if it has been posted in the last **epochLength** blocks| +|Maximum deviation percent| **maxDeviationPercent** | 5 | Maximum allowed difference between the first and last data points
in terms of the first data point
Data points are sorted in increasing order| +|Total ballots| | 15 | The total number of people allowed to vote for an update | +|Minimum votes| **minVotes** | 6 | The minimum votes needed to update pool params (any of the above)| + +Once the parameters are decided, generate the [tokens](#tokens) in the correct quantity and update the values in the [contracts](#contracts). +Next, find the right number of trusted oracles and ballot holders and distribute the tokens to them. + +### Reward Mechanism + +In v1.0, the pool was responsible for rewarding each oracle for posting a data-point. In v2.0, the pool simply certifies that a data-point was posted, and a separate reward mechanism is proposed. This keeps the contract smaller and more flexible. + +The certificates are in the form of tokens emitted by the pool box. Thus, the pool box also contains the reward tokens to be emitted. + +Once there are sufficient number of such tokens, an oracle can exchange or burn them in return for a reward. + +### Refresh Mechanism + +In v1.0, the pool was refreshed by moving from the epoch-preparation phase to the live epoch phase. +In v2.0, although there is a single pool box, the logic of pool refresh is stored in a separate box, the **refresh box**. +The refresh box is identified by a separate token, the *refresh-NFT*, which is referenced in the pool and oracle contracts. + +### Update Mechanism + +The update mechanism is similar to that in v1.0. A threshold number (currently 6) of ballots must be cast for a proposed update. Such ballots are stored in a **ballot box** and identified by a **ballot token**. +The ballot token holders and oracle token holders need not be the same people. +The logic for updating the pool is stored in an **update box**, which identified by the **update-NFT**. This NFT is referenced in the pool and ballot boxes. +The ballot box allows the owner to vote for 3 things: + +1. The script of the new pool box. +2. The reward token id to be stored in the new pool box. +3. The reward token amounts to be stored in the new pool box. + +Apart from this, the new pool box *must* preserve the remaining values from the old pool box. + +**Note** The update box logic acts on the pool box and not the refresh box. + +## Tokens + +The system has the following types of tokens. Note that we use the term **NFT** to refer to any token that was issued in quantity 1. + +| Token | Issued quantity | purpose | where stored | +|---|---|---|---| +|Pool-NFT | 1 | Identify pool box | Pool box | +|Refresh-NFT | 1 | Identify refresh box | Refresh box | +|Update-NFT | 1 | Identify update box | Update box | +|Oracle tokens | 15 | Identify each oracle box | Oracle boxes | +|Ballot tokens | 15 | Identify each ballot box | Ballot boxes | +|Reward tokens | 100 million | Reward oracles | Pool box
Oracle boxes | + +## Boxes + +There are a total of 5 contracts, each corresponding to a box type + +| Box | Quantity | Tokens | Additional registers used | Purpose | Spending transactions | +|---|---|---|---|---|---| +|Pool | 1 | Pool-NFT
Reward tokens| R4: Rate (Long)
R5: Epoch counter (Int) | Publish pool rate for dApps
Emit reward tokens| Refresh pool
Update pool | +|Refresh| 1 | Refresh-NFT | | Refresh pool box | Refresh pool | +|Oracle | 15 | Oracle token
Reward tokens | R4: Public key (GroupElement)
R5: Epoch counter of pool box (Int)
R6: Published rate (Long) | Publish data-point
Accumulate reward tokens | Publish data-point,
Refresh pool,
Transfer oracle token,
Extract reward tokens| +|Update | 1 | Update-NFT | | Updating pool box | Update pool box | +|Ballot | 15 | Ballot token | R4: Public key (GroupElement)
R5: Update box creation height (Int)
R6: New pool box hash (Coll[Byte]) | Voting for updating pool box | Vote for update
Update pool box
Transfer ballot token | + +Before going into the transactions in the protocol, we first present the contracts for each of the above boxes. + +## Contracts with base-64-encoded hash of ergo-tree bytes + +- [Pool Contract](contracts/pool_contract.es) `8cJi+FGGU32jXyO8M2LeyWSWlerdcb1zxBWeZtyy7Y8=` +- [Refresh Contract](contracts/refresh_contract.es) `cs5c5QEirstI4ZlTyrbTjlPwWYHRW+QsedtpyOSBnH4=` +- [Oracle Contract](contracts/oracle_contract.es) `fhOYLO3s+NJCqTQDWUz0E+ffy2T1VG7ZnhSFs0RP948=` +- [Ballot Contract](contracts/ballot_contract.es) `x01xAvK0CrRCwj36vp/jon7NARR1rxplSwI5B20ZNyI=` +- [Update Contract](contracts/update_contract.es) `pQ7Dgjq1pUyISroP+RWEDf+kVNYAWjeFHzW+cpImhsQ=` +Use this Scastie playground to calculate the above hashes - [https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ](https://scastie.scala-lang.org/hnTEm2lJQPG1wRYCqPz8LQ) +## Transactions + +Oracle pool v2.0 has the following transactions. +Each of the transactions below also contain the following boxes which will not be shown. + +1. Funding input box: this will be used to fund the transaction, and will be the last input. +2. Fee output box: this will be the last output. +3. Change output box: this is optional, and if present, will be the second-last output. + +| Transaction | Boxes Involved | Purpose | +| --- | --- | --- | +| Refresh pool | Pool
Refresh
Oracles | Refresh pool box | +| Publish data point | Oracle | Publish data point | +| Extract reward tokens | Oracle | Extract reward tokens to redeem via external mechanism | +| Transfer oracle token | Oracle | Transfer oracle token to another public key| +| Vote for update | Ballot | Vote for updating pool box | +| Update pool | Pool
Update
Ballots | Update pool box | +| Transfer ballot token | Ballot | Transfer ballot token to another public key| + +None of the transactions have data-inputs. + +### Refresh pool + +| Index | Input | Output | +|---|---|---| +| 0 | Pool | Pool | +| 1 | Refresh | Refresh | +| 2 | Oracle 1 | Oracle 1 | +| 3 | Oracle 2 | Oracle 2 | +| 4 | Oracle 3 | Oracle 3 | +| ... | ... | ... | + +The purpose of this transaction is to take the average of the rates in all the oracle boxes and update the rate in the pool box whenever the +epoch gets over (i.e., the current height is > creation height + epoch length). +We consider such a pool box to be *stale* that needs to be refreshed. + +1. The first input is the pool box that simply requires the second input to be the refresh box (i.e., contain the refresh token) ([smart contract](#pool-contract)). +2. The second input is a refresh box that contains the following logic ([smart contract](#refresh-contract)): + - The first input is a stale pool box, that is a box with the pool token and creation height lower than the current height minus epoch length. + - This transaction can only be done by someone holding a oracle token and having published an oracle box (see below). + - Any value published within the last epoch length by someone holding the oracle token is considered *latest*. This is called an oracle box. + In particular, for the box to be considered an oracle box, the following must hold: + - It must have an oracle token at index 0. + - Register R4 must contain a group element. + - Register R5 must be the current epoch counter (from R5 of the stale pool box). + - Register R6 must contain a long value, which will be assumed to be the rate. + - Its creation height must not be less than the current height minus epoch length. + - There must be at least a certain number of oracle boxes (currently 4) as inputs. + - The oracle boxes must be arranged in increasing order of their R6 values (rate). + - The first oracle box's rate must be within 5% of that of the last, and must be > 0. + - The first output must be a new pool box as follows: + - Registers R0 (value), R1 (script), R2 (tokens) are preserved from the old pool box. + - The creation height (stored in R3) must be at greater than or equal to the current height minus 4. + - The rate (stored in R4) must be the average of the rates in all the oracle boxes. + - The epoch counter (stored in R5) must be incremented by 1. + - Registers R6 and onward are empty. + - The quantity of the second token (reward) must be decremented by at most twice the number of valid rate boxes. + - The second output must be a new refresh box as follows: + - Registers R0 (value), R1 (script), and the first token (refresh NFT) are preserved from the old refresh box. +3. Each input oracle box has following logic ([smart contract](#oracle-contract)): + - The first input is a pool box (i.e., has the pool token). + - An output oracle box (acting as a copy of this box) must be created as follows: + - The following values are directly copied: R0 (nanoErgs), R1 (script), the first token (oracle token) (stored in R2) and R4 (GroupElement). + - The second token (reward token) is incremented by at least 1. + +Suppose there are *n* valid oracle boxes, then there are 2*n* reward tokens released. +It is expected that whoever creates the refresh transaction (the *collector*) takes *n*+1 reward tokens, and the other oracles get 1 token each. +This gives incentive to use as many oracle boxes as possible during the refresh. + +### Publish data-point + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | + +This allows an oracle to publish data-point for collection in next epoch. +This entails spending the oracle box and creating a new oracle box as per the [smart contract](#oracle-contract). + +1. The public key stored in R4 defines who can spend the oracle box. +2. The logic requires the new oracle box to be as follows: + - The script and the first token are copied from this box. + - R4 contains a group element. + - The second token is the reward token in some non-zero quantity. +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should store epoch counter of the current pool box in R5. + - It should store the rate to publish in R6. + - It should ensure reward tokens are preserved. + +### Extract reward tokens + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | +| 1 | | Box with freed reward tokens | + +This allows an oracle to extract tokens obtained from publishing data point to redeem them elsewhere. + +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the same group element in R4. + - It should keep at least 1 reward token. + - It should store the balance reward tokens in some other box. + + +### Transfer oracle token + +| Index | Input | Output | +|---|---|---| +| 0 | Oracle | Oracle | + +This is used to transfer the ownership of an oracle token to another public key. + +The transaction is similar to the [publish data-point](#publish-data-point) transaction, except that Step 3 is modified as follows. + +3. The following rules (not enforced by the smart contract) to be followed. + - It should store the new owner's group element in R4. + - It should check that there is only 1 reward token is in the oracle box and send it to the new owner along with oracle token. + +### Vote for update + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + +This is used by ballot token holders to vote for updating the pool box address. + +The input and output are ballot boxes such that the following holds as per the [smart contract](#ballot-contract): +- There is a group element in R4. + +The following (not enforced by the contract) must be done for a proper vote: +- R5 of type `Int` contains the creation height of the current update box. +- R6 of type `Coll[Byte]` contains the hash of the address of the new pool box. +- R7 of type `Coll[Byte]` contains the reward token id for the new pool box. +- R8 of type `Int` contains the reward token amount for the new pool box. + +### Update Pool box + +| Index | Input | Output | +|---|---|---| +| 0 | Pool | New Pool | +| 1 | Update | Update | +| 2 | Ballot 1 | Ballot 1 | +| 3 | Ballot 2 | Ballot 2 | +| 4 | Ballot 3 | Ballot 3 | +| ... | ... | ... | + +This updates the pool box (i.e., transfers the poolNFT to a new address). + +The ballots used in inputs must satisfy the following as per the [smart contract](#ballot-contract): +- There must be an output ballot box with the same public key in R4 and rest registers empty. + +The update box additionally ensures following as per the [smart contract](#update-contract): +- Each ballot box has R5 containing the hash of the new pool box script (address). +- Each ballot box has R6 containing this box's creation height. +- Each ballot box has R7 containing the new pool box's reward token id. +- Each ballot box has R8 containing the new pool box's reward token amount. +- The remaining registers (apart from creation height) and tokens in the old pool box are preserved in the new pool box. +- There is a copy of the update box in outputs with everything preserved but with larger creation height. +- There are at least a threshold number of votes. + +We store the update box's creation height in the ballot's R5 to ensure that a vote is actually for the current update box. + +#### Update Rules + +In order to keep the [pool contract](#pool-contract) as compact as possible, many parameters are hard-wired in the [refresh contract](#refresh-contract). +Depending on what is to be updated, we may need to update other items as well. In particular, the following rules apply: + +| To update | Also must update | Can preserve | +|---|---|---| +|Pool params
(like epoch length) | Refresh-NFT
Refresh box | Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Refresh-NFT | Refresh box | Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Update-NFT | Update box
Ballot tokens
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | +|Oracle tokens | Refresh-NFT
Refresh box
Oracle boxes
| Reward tokens
Update-NFT
Update box
Ballot tokens
Ballot boxes | +|Reward tokens | Pool box| Refresh-NFT
Refresh box
Update-NFT
Update box
Ballot tokens
Ballot boxes
Oracle tokens
Oracle boxes | +|Ballot tokens | Update-NFT
Update box
Ballot boxes | Refresh-NFT
Refresh box
Reward tokens
Oracle tokens
Oracle boxes | + +After an update, the older versions of the boxes will be left for garbage collection via storage-rent, and their tokens will become free for miners to take. +Hence, it is beneficial to update all the related tokens anyway. + + +### Transfer ballot token + +| Index | Input | Output | +|---|---|---| +| 0 | Ballot | Ballot | + +This is similar to the [transfer oracle token](#transfer-oracle-token) and [vote for update](#vote-for-update) transactions, except that it is used to transfer ownership of a ballot token to another public key.