Skip to content

Conversation

@Intermarch3
Copy link

Description

In my journey to upskill on Gno.land, my first significant project is leveraging my knowledge of existing Ethereum smart contracts to implement an optimistic oracle, inspired by UMA Optimistic Oracle and their use in the well-known dApp Polymarket.
Here is a simplified implementation.

This was a previous pr on the gno repo (gnolang/gno#4681) that we moved here.

@Intermarch3
Copy link
Author

Intermarch3 commented Oct 9, 2025

Left to do :

  • Test by hand the entire flow to check if nothing was broken during the update.
  • Add Gno tests
  • fix exposed objects
  • create CLI tool to use the oracle easly
  • Enhance the render page

Copy link
Contributor

@leohhhn leohhhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving a few comments

Comment on lines 15 to 38
type Vote struct {
RequestId string
Voter address
TokenAmount int64
Hash string
Value int64
Revealed bool
}

type Voter struct {
HasVoted bool
VoteIndex int64
}

type Dispute struct {
RequestId string
Votes []Vote
NbResolvedVotes int64
Voters *avl.Tree
IsResolved bool
WinningValue int64
EndTime time.Time
EndRevealTime time.Time
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: could be in types.gno, since you already have a few.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6ed3498

}

var (
Disputes = avl.NewTree()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be exported?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not, could be used to follow initial request resolution on other contracts.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

methods on exposed objects allow anyone to modify this object, for example here, anyone can call goo.Disputes.Set("eve", data)

if you want to expose this data safely, you should exposed a safe version, like a rotree wrapper

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

31fc552
Fixed it by having this variable private, and the value is still accessible with getters.


## Architecture

The oracle is composed of three main contracts:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's one contract though, isn't it? You just listed 3 files

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad, wanted to say "3 main files"
6ed3498

Comment on lines 102 to 106
**2. Propose a Value**
```bash
# Propose "Yes" (value 1) (replace ID with the actual ID returned from the RequestData call)
gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "ProposeValue" -args "ID" -args "0" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "2000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" <proposer-key-name>
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nice addition would be to suggest (and maybe even build) a Go program to suggest values using gnoclient

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#20

Comment on lines 37 to 43
table = append(table, renderRequest(value.(DataRequest))...)
return false
})
msg += md.ColumnsN(table, 4, false)
if len(table) == 4 {
msg += md.Paragraph("No current requests.")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: you could probably add a newline here and there for readability

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6ed3498

Comment on lines 14 to 20
} else {
if request.YesNoQuestion {
return resolveYesNo(*dispute)
} else {
return resolveNumeric(*dispute)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else is useless, you can just put the logic in the function body after the error return above

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6ed3498

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this hasn't been fixed as far as i see

Comment on lines 48 to 50
func init() {
VoteToken = newOOToken("Gno Optimistic Oracle Token", "goot", 6)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you can init this at the var block above

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6ed3498

@Intermarch3 Intermarch3 requested a review from leohhhn October 19, 2025 09:55
@Intermarch3 Intermarch3 marked this pull request as ready for review October 25, 2025 09:22
@Intermarch3 Intermarch3 mentioned this pull request Nov 3, 2025
Copy link
Contributor

@leohhhn leohhhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

submitting some feedback

if _, exists := disputes.Get(id); exists {
panic("error: Dispute for this request already exists.")
}
dispute := Dispute{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could set this as a pointer so that later you can simply fetch and change the object in place without having to set it again

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1dd0262

// -- PUBLIC FUNCTIONS --

// BuyInitialVoteToken allows a user to buy their first vote token by sending voteTokenPrice amount of ugnot.
func BuyInitialVoteToken(_ realm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a heads up; gnot will be non-transferrable upon launch so you will not be able to use it to pay for things (usable only for gas)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its ok, we just need to set the price of the token to 0 ugnot at launch and change it after.

}

// BalanceOfVoteToken returns the number of vote tokens held by the caller.
func BalanceOfVoteToken(_ realm) int64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you check out GRC20 Tellers? they're exposable so you do not have to have this kind of wrapper function

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

Comment on lines 67 to 78
request := res.(DataRequest)
if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() {
panic("error: Proposer and Disputer cannot vote in this dispute.")
}
if dispute.IsResolved {
panic("error: Dispute is already resolved.")
}
if time.Now().After(dispute.EndTime) {
panic("error: Vote period has ended.")
}
amount := VoteToken.BalanceOf(runtime.PreviousRealm().Address())
if amount < 1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newlines for readability

Suggested change
request := res.(DataRequest)
if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() {
panic("error: Proposer and Disputer cannot vote in this dispute.")
}
if dispute.IsResolved {
panic("error: Dispute is already resolved.")
}
if time.Now().After(dispute.EndTime) {
panic("error: Vote period has ended.")
}
amount := VoteToken.BalanceOf(runtime.PreviousRealm().Address())
if amount < 1 {
request := res.(DataRequest)
if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() {
panic("error: Proposer and Disputer cannot vote in this dispute.")
}
if dispute.IsResolved {
panic("error: Dispute is already resolved.")
}
if time.Now().After(dispute.EndTime) {
panic("error: Vote period has ended.")
}
amount := VoteToken.BalanceOf(runtime.PreviousRealm().Address())
if amount < 1 {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

caller := runtime.OriginCaller()
coins := banker.OriginSend()
if len(coins) != 1 || coins.AmountOf("ugnot") != voteTokenPrice {
panic("error: Must send exactly " + strconv.Itoa(int(voteTokenPrice/1_000_000)) + " gnot to get a vote token.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be easier with ufmt

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

"chain"
"chain/runtime"
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you mind adding a short godoc?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

}
}

func resolveYesNo(dispute Dispute) int64 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above for godoc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

Comment on lines 34 to 35
var winningValue int64
var weight int64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be a var block

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

winningValue = 0
weight = noVotes
}
rewardAndSlachVoters(dispute, winningValue, weight)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: slach

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

Comment on lines 58 to 60
// Find the value with the most token weight
var winningValue int64
var maxWeight int64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var block

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

msg += md.HorizontalRule()

// List last 5 resolved requests and build table
msg += md.H2("Last 5 Resolved requests :")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msg += md.H2("Last 5 Resolved requests :")
msg += md.H2("Last 5 Resolved requests:")

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

Comment on lines 30 to 33
// RequestData allows a user to request data from the oracle.
// `requesterReward` value needs to be sent to the contract as a reward
// You need to ask a question that can be answered with a single number like a yes/no question (0 or 1) or a specific value (e.g. ETH/USD price).
func RequestData(_ realm, ancillaryData string, yesNoQuestion bool, deadline int64) string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to mention what the unit for the deadline is; seconds, a specific block, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe


id := idGenerator.Next().String()

request := DataRequest{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointers in AVL Trees in general are better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1dd0262


coins := banker.OriginSend()
if len(coins) != 1 || coins.AmountOf("ugnot") != requesterReward {
panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(requesterReward, 10) + " ugnot.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ufmt could be better

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3107cbe

Copy link
Contributor

@leohhhn leohhhn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few more comments, but overall this is good. Let's get the ball rolling on this more, it's super useful and really needed. Sorry for the slow turnaround on the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants