Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-0023 Oracle pool 2.0 #41

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3efd0c6
EIP-0022 md file for oracle pool 2.0
scalahub Sep 1, 2021
ab5b37a
Rename EIP-22 to EIP-23
scalahub Sep 7, 2021
adc4544
Update eip-0023.md
scalahub Sep 7, 2021
ddbfbfc
Add EIP-23 to index of EIPs
scalahub Sep 7, 2021
52c467e
Add description of oracle pool 2.0
scalahub Sep 11, 2021
ee92d8c
Store participant public keys in R4
scalahub Sep 11, 2021
d35140e
Add counter to pool box and fix typos
scalahub Sep 12, 2021
95a2da0
Update the pool box instead of refresh box
scalahub Sep 13, 2021
0ba8625
Fix typos, streamline headings
scalahub Sep 13, 2021
2aecd3c
Add transaction to extract reward tokens
scalahub Sep 14, 2021
731e8ca
Add description of refresh and oracle tx
scalahub Sep 15, 2021
786cc9f
Keep epoch counter in R5 or oracle box
scalahub Sep 29, 2021
0faaa3a
Update eip-0023.md
scalahub Sep 30, 2021
3eb4c2b
Update box creation height in R5 of ballot
scalahub Oct 3, 2021
92e4bf3
Add description of the update mechanism
scalahub Oct 4, 2021
2d21b68
Fix typos, add cross-references to contracts
scalahub Oct 4, 2021
1e4748e
Fix description of update mechanism
scalahub Oct 4, 2021
7c3a2a4
Oracle can change reward tokens in oracle box
scalahub Oct 13, 2021
1a49f9e
Ensure creation height is preserved in update
scalahub Oct 25, 2021
d12e62a
Add prerequisites for running own pool
scalahub Oct 28, 2021
ac70adb
Fix type in epochLength symbol usage
scalahub Oct 28, 2021
bd7a609
Fix typo in eip-0023.md
scalahub Apr 19, 2022
196e89a
Store reward tokens in oracle pool box
scalahub Jun 16, 2022
80de6e9
Add table of contents based on headings
scalahub Jul 6, 2022
a9e08d5
Use ballot contract from testing version v2b
scalahub Jul 11, 2022
5d3c178
fix markdown rendering
greenhat Jul 28, 2022
23e2d40
properly fix md rendering;
greenhat Jul 28, 2022
abf27ef
Allow preserving pool script during update
scalahub Jul 29, 2022
ffb7a76
Update author list for EIP23 (add greenhat)
scalahub Jul 29, 2022
01782c7
Update eip-0023.md
scalahub Aug 4, 2022
dabfb0a
Split out EIP-23 contracts into separate files
kettlebell Aug 31, 2022
fec79b7
Add encoded contract hashes to EIP-23
kettlebell Aug 31, 2022
e485df2
Update eip-0023/eip-0023.md (add link to scastie)
kettlebell Aug 31, 2022
f3a32eb
Merge pull request #78 from kettlebell/eip23_separate_contract_files
scalahub Nov 23, 2022
c6f5425
Allow changing pool creation height in update
scalahub Nov 24, 2022
1757e00
Merge branch 'master' into eip23
scalahub Nov 24, 2022
cae50b7
Update eip-0023/eip-0023.md
scalahub Nov 24, 2022
b7138e7
in update contract check reward tokens are either preserved or voted …
greenhat Feb 16, 2023
f67124b
update update contract hash according to changes in https://github.co…
greenhat Feb 27, 2023
821fc1f
Fix link to EIP23 readme file
scalahub Apr 9, 2023
21be0b3
Fix link to main readme file
scalahub Apr 9, 2023
2e14357
Remove text on sample token exchange contract
scalahub Apr 9, 2023
2b24dc2
Merge pull request #89 from ergoplatform/eip23-update-contract-reward…
greenhat Jun 27, 2023
3bd226b
let ballot token owner (R4 in ballot box) always spend the ballot box;
greenhat Jun 28, 2023
9cbe17c
update ballot contract hash
greenhat Jun 28, 2023
997b99f
Merge pull request #94 from ergoplatform/eip23-owner-can-spend-ballot
greenhat Aug 10, 2023
2b4a2e4
Update eip-0023/eip-0023.md
kushti Nov 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| [EIP-0039](eip-0039.md) | Monotonic box creation height rule |
32 changes: 32 additions & 0 deletions eip-0023/contracts/ballot_contract.es
Original file line number Diff line number Diff line change
@@ -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)
}
49 changes: 49 additions & 0 deletions eip-0023/contracts/oracle_contract.es
Original file line number Diff line number Diff line change
@@ -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)
}
16 changes: 16 additions & 0 deletions eip-0023/contracts/pool_contract.es
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 76 additions & 0 deletions eip-0023/contracts/refresh_contract.es
Original file line number Diff line number Diff line change
@@ -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
}
61 changes: 61 additions & 0 deletions eip-0023/contracts/update_contract.es
Original file line number Diff line number Diff line change
@@ -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)
}