From 24677bab989c245e210674ace38c3d0299995705 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Thu, 9 Oct 2025 23:04:54 +0200 Subject: [PATCH 01/14] add: goo (gno optimistic oracle) --- packages/r/intermarch3/goo/README.md | 150 ++++++++++++ packages/r/intermarch3/goo/court.gno | 287 +++++++++++++++++++++++ packages/r/intermarch3/goo/gnomod.toml | 2 + packages/r/intermarch3/goo/oracle.gno | 294 ++++++++++++++++++++++++ packages/r/intermarch3/goo/render.gno | 188 +++++++++++++++ packages/r/intermarch3/goo/resolver.gno | 92 ++++++++ packages/r/intermarch3/goo/token.gno | 32 +++ 7 files changed, 1045 insertions(+) create mode 100644 packages/r/intermarch3/goo/README.md create mode 100644 packages/r/intermarch3/goo/court.gno create mode 100644 packages/r/intermarch3/goo/gnomod.toml create mode 100644 packages/r/intermarch3/goo/oracle.gno create mode 100644 packages/r/intermarch3/goo/render.gno create mode 100644 packages/r/intermarch3/goo/resolver.gno create mode 100644 packages/r/intermarch3/goo/token.gno diff --git a/packages/r/intermarch3/goo/README.md b/packages/r/intermarch3/goo/README.md new file mode 100644 index 0000000..1e863db --- /dev/null +++ b/packages/r/intermarch3/goo/README.md @@ -0,0 +1,150 @@ +# Gno Optimistic Oracle (GOO) + +An Optimistic Oracle (GOO) built on Gno.land. This system is designed to bring external data onto the blockchain by leveraging game-theoretic incentives. It assumes data is correct unless disputed, hence the term "optimistic." +This implementation is inspired by the [UMA Optimistic Oracle](https://uma.xyz/) but adapted for the Gno ecosystem. + +## Table of Contents +- [Core Concepts](#core-concepts) +- [How It Works: The Lifecycle of a Data Request](#how-it-works-the-lifecycle-of-a-data-request) +- [Architecture](#architecture) +- [User Roles](#user-roles) +- [Tokenomics](#tokenomics) +- [Usage Example](#usage-example) +- [Developer](#developer) + +## Core Concepts + +The Gno Optimistic Oracle operates on the principle that data proposed to the oracle is assumed to be true. A bond is required for any new proposition. This proposition enters a "liveness" period where anyone can dispute it by posting an equal bond. + +- **Happy Path**: If no one disputes the data within the liveness period, it is considered resolved and accepted as truth. The proposer's bond is returned along with a reward. +- **Unhappy Path (Dispute)**: If the data is disputed, Oracle Token Holders can vote on the correct outcome. This is handled by the `court.gno` contract. Token holders vote, and the outcome is decided by the total token weight backing each value. The winner's bond is returned, and they receive a portion of the loser's slashed bond. + +## How It Works: The Lifecycle of a Data Request + +The entire process, from requesting data to its final resolution, follows a clear, multi-step path. + +### 1. Data Request (`RequestData`) +A user or a contract initiates a request for data by calling `RequestData`. +- **Ancillary Data**: A clear, human-readable question (e.g., "What was the price of ETH/USD at block X?"). +- **Type**: The request can be a `Yes/No` question (represented by 0 and 1) or a `Numeric` value. +- **Reward**: The requester must lock a `RequesterReward` in GNOT to incentivize a proposer to provide the data. +- **Deadline**: The requester sets a deadline by which the data must be proposed, otherwise they can retrieve their locked reward. + +### 2. Value Proposal (`ProposeValue`) +A **Proposer** provides an answer to the request. +- They call `ProposeValue` with the proposed answer. +- They must post a `Bond` in GNOT, which is held in escrow. +- This action starts the **Resolution Time**, a liveness window during which the proposal can be disputed. + +### 3. The Liveness Period +Once a value is proposed, a countdown begins. During this period, anyone can challenge the proposed value. + +- **If Undisputed**: If the `ResolutionTime` expires without any disputes, the request is considered final. Anyone can call `ResolveRequest`. The `ProposedValue` becomes the `WinningValue`. The Proposer gets their bond back, plus the `RequesterReward`. +- **If Disputed**: If another user believes the proposed value is incorrect, they can challenge it. + +### 4. Dispute (`DisputeData`) +A **Disputer** can challenge the Proposer's value. +- They must call `DisputeData` before the `ResolutionTime` ends. +- They must also post a `Bond` equal to the Proposer's bond. +- This action pauses the request's resolution and initiates a formal dispute, handled by the `court.gno` contract. + +### 5. Voting (`VoteOnDispute`) +The dispute is now open for voting by all Oracle Soulbound Token holders. The system uses a **commit-reveal scheme** to prevent vote-copying. + +- **Commit Phase**: During the `DisputeDuration`, voters submit a hash of their vote (`SHA256(value + salt)`) by calling `VoteOnDispute`. They must also pay a small `VotePrice` fee. // todo +- **Reveal Phase**: After the commit phase ends, the `RevealDuration` begins. Voters must call `RevealVote`, submitting their original `value` and `salt`. The contract verifies that the hash matches the one submitted during the commit phase. + +### 6. Dispute Resolution (`ResolveDispute`) +Once the reveal period is over, anyone can call `ResolveDispute`. +- The `resolver.gno` contract tallies the votes. The winning value is the one with the highest cumulative token weight from voters. +- The `WinningValue` is set in the original `DataRequest`. +- **Slashing & Rewards**: The party (Proposer or Disputer) that lost the vote has their bond slashed. The winning party gets their bond back, and the slashed bond is distributed among the voters who voted for the winning outcome (voters who vote incorrectly lose 25% of their Oracle Token balance). + +## Architecture + +The oracle is composed of three main contracts: + +- `oracle.gno`: Manages the data request lifecycle (request, propose, dispute, resolve). It is the main entry point for users. +- `court.gno`: Handles the entire dispute resolution process, including the commit-reveal voting scheme. +- `resolver.gno`: Contains the business logic for tallying votes and determining the winning value of a dispute. It supports both Yes/No and Numeric resolutions. + +## User Roles + +- **Requester**: The user or contract that needs external data. They create the request and fund the reward. +- **Proposer**: The user who provides the initial answer to a data request and posts a bond. +- **Disputer**: A user who challenges a proposed value and posts a bond to initiate a vote. +- **Voter**: An Oracle Soulbound Token holder who participates in a dispute by voting on the correct outcome. + +## Tokenomics + +The Oracle Soulbound Token (OOT) is a non-transferable token that represents voting power in the oracle system. +- **Acquisition**: Users can acquire OOT by calling `BuyInitialVoteToken` and paying a fee in GNOT. This action mints one OOT to the caller's address (can only buy 1 initial token). +Vote token holders can participate in disputes and earn rewards by voting correctly. By voting correctly, they can earn a portion of the slashed bonds from losing parties and gain 2 Vote tokens, incentivizing accurate and honest participation in the oracle system and increasing their voting power. +- **GNOT Usage**: The reward and the Bond (in GNOT Token) need to be less than the bond to avoid trying to game the system by creating disputes just to earn tokens, as they would lose more from the bond than the reward. +This design is not final and can be adjusted based on community feedback and economic analysis. + +## Usage Example + +Here is a full workflow using `gnokey`. +**0. Buy Vote Token for Voter Role** +```bash +# Buy Oracle Soulbound Token (replace with your key name) +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "BuyInitialVoteToken" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + + +**1. Request Data** +```bash +# Ask a Yes/No question: "Will ETH be below $4000 ?" (replace DEADLINE_TIMESTAMP with a future unix timestamp more than 24h from now) +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "RequestData" -args "ETH below 4000$ ?" -args "true" -args "DEADLINE_TIMESTAMP" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +**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" +``` + +**If no one disputes within the liveness period, anyone can resolve the request:** +```bash +# Resolve the request (replace ID with the actual ID) +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "ResolveRequest" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +**3. Dispute the Value** +```bash +# Dispute the proposal (replace ID with the actual ID) +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "DisputeData" -args "ID" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "2000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +**4. Vote on the Dispute** +First, generate a hash locally. Let's vote "No" (value 0) with salt "mysecret". +Hash: `sha256("0" + "test")` -> `a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21` +```bash +# Commit the vote (replace ID with the actual ID) +ggnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "VoteOnDispute" -args "ID" -args "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +**5. Reveal the Vote** +```bash +# Reveal the vote after the voting period ends (replace ID with the actual ID) +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "RevealVote" -args "ID" -args "0" -args "test" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +**6. Resolve the Dispute** +```bash +# After the reveal period, anyone can trigger the final resolution (replace ID with the actual ID). +gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "ResolveDispute" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +``` + +When testing with `gnodev` locally, ensure to make transactions between waiting periods as `gnodev` only creates blocks when a transaction is made, and the oracle relies on current block timestamps. + +## Warning + +This is a simplified example for educational purposes. In a production environment, consider additional security measures, optimizations, and edge cases. + + +## Developer + +| [
Lucas Leclerc](https://github.com/intermarch3) | +| :---: | \ No newline at end of file diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno new file mode 100644 index 0000000..fc078f7 --- /dev/null +++ b/packages/r/intermarch3/goo/court.gno @@ -0,0 +1,287 @@ +package goo + +import ( + "chain" + "chain/banker" + "chain/runtime" + "gno.land/p/nt/avl" + "strconv" + "time" + + "crypto/sha256" + "encoding/hex" +) + +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 +} + +var ( + Disputes = avl.NewTree() + DisputeDuration = 2 * int64(time.Minute.Seconds()) + RevealDuration = 2 * int64(time.Minute.Seconds()) + VoteTokenPrice = 1 * int64(1_000_000) // in GNOT + VoteToken *OOT +) + +func init() { + VoteToken = newOOToken("Gno Optimistic Oracle Token", "goot", 6) +} + +func initiateDispute(id string) { + if _, exists := Disputes.Get(id); exists { + panic("error: Dispute for this request already exists.") + } + dispute := Dispute{ + RequestId: id, + Votes: []Vote{}, + Voters: avl.NewTree(), + IsResolved: false, + EndTime: time.Now().Add(time.Duration(DisputeDuration) * time.Second), + EndRevealTime: time.Now().Add(time.Duration(DisputeDuration+RevealDuration) * time.Second), + } + Disputes.Set(id, dispute) + chain.Emit("DisputeInitiated", "id", id) +} + +// -- PUBLIC FUNCTIONS -- + +// BuyInitialVoteToken allows a user to buy their first vote token by sending VoteTokenPrice amount of ugnot. +func BuyInitialVoteToken(_ realm) { + 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.") + } + + balance := VoteToken.BalanceOf(caller) + if balance > 0 { + panic("error: You already have a vote token.") + } + + VoteToken.Mint(caller, 1) + chain.Emit("VoteTokenPurchased", "voter", caller.String()) +} + +// BalanceOfVoteToken returns the number of vote tokens held by the caller. +func BalanceOfVoteToken(_ realm) int64 { + return VoteToken.balanceOf(runtime.PreviousRealm().Address()) +} + +// VoteOnDispute allows a user to commit a vote during a dispute. +// @param id - The ID of the dispute (same as Request ID). +// @param hash - The SHA256 hash of the vote value and a secret salt. +func VoteOnDispute(cur realm, id string, hash string) { + dispute := getDispute(id) + res, _ := Requests.Get(id) + 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 { + panic("error: You need at least 1 vote token to vote.") + } + + vote := Vote{ + RequestId: id, + Voter: runtime.PreviousRealm().Address(), + TokenAmount: amount, + Hash: hash, + Revealed: false, + } + voter, exist := dispute.Voters.Get(string(vote.Voter)) + if exist && voter.(*Voter).HasVoted { + panic("error: Voter has already voted in this dispute.") + } + dispute.Votes = append(dispute.Votes, vote) + dispute.Voters.Set(string(vote.Voter), Voter{HasVoted: true, VoteIndex: int64(len(dispute.Votes) - 1)}) + Disputes.Set(id, dispute) + chain.Emit("VoteSubmitted", "id", id, "voter", vote.Voter.String()) +} + +// RevealVote allows a user to reveal their vote after the voting period has ended. +// @param id - The ID of the dispute (same as Request ID). +// @param value - The actual value of the vote. +// @param salt - The secret salt used to generate the vote hash. +func RevealVote(cur realm, id string, value int64, salt string) { + dispute := getDispute(id) + if dispute.IsResolved { + panic("error: Dispute is resolved.") + } + if time.Now().Before(dispute.EndTime) { + panic("error: Vote period has not ended yet.") + } + if time.Now().After(dispute.EndRevealTime) { + panic("error: Reveal period has ended.") + } + voter, exist := dispute.Voters.Get(string(runtime.PreviousRealm().Address())) + if !exist || !voter.(*Voter).HasVoted { + panic("error: Voter did not participate in this dispute.") + } + vote := dispute.Votes[voter.(*Voter).VoteIndex] + if vote.Revealed { + panic("error: Vote already revealed.") + } + + // Verify the hash + res := sha256.Sum256([]byte(strconv.FormatInt(value, 10) + salt)) + expectedHash := hex.EncodeToString(res[:]) + if vote.Hash != expectedHash { + panic("error: Hash does not match the revealed value and salt.") + } + vote.Value = value + vote.Revealed = true + dispute.NbResolvedVotes += 1 + dispute.Votes[voter.(*Voter).VoteIndex] = vote + Disputes.Set(id, dispute) + chain.Emit("VoteRevealed", "id", id, "voter", vote.Voter.String(), "value", strconv.Itoa(int(value))) +} + +// ResolveDispute finalizes a dispute after the reveal period, tallying votes and setting the winning value. +// @param id - The ID of the dispute to resolve (same as Request ID). +func ResolveDispute(cur realm, id string) { + dispute := getDispute(id) + if dispute.IsResolved { + panic("error: Dispute is already resolved.") + } + if time.Now().Before(dispute.EndTime) { + panic("error: Dispute period has not ended yet.") + } + val := resolve(id) + dispute.WinningValue = val + dispute.IsResolved = true + Disputes.Set(id, dispute) + // Update the original request with the winning value + request := getRequest(id) + + request.ProposedValue = val + request.State = "Resolved" + Requests.Set(id, request) + chain.Emit("DisputeResolved", "id", id, "winningValue", strconv.Itoa(int(val))) + chain.Emit("RequestResolved", "id", id, "winningValue", strconv.Itoa(int(val))) + + var winner address + if val != request.ProposedValue { + // Refund + reward the disputer if the dispute changed the value + winner = request.Disputer + } else { + // Refund + reward the proposer if the dispute did not change the value + winner = request.Proposer + } + Bank.SendCoins(runtime.CurrentRealm().Address(), winner, chain.Coins{chain.Coin{Denom: "ugnot", Amount: Bond + RequesterReward}}) +} + +// -- admin functions -- + +// SetDisputeDuration sets the duration (in seconds) for the voting period. +// @param duration - The new dispute duration in seconds. +func SetDisputeDuration(_ realm, duration int64) { + if runtime.OriginCaller() == admin { + DisputeDuration = duration * int64(time.Second) + } else { + panic("error: Only admin can set dispute duration.") + } +} + +// SetRevealDuration sets the duration (in seconds) for the reveal period. +// @param duration - The new reveal duration in seconds. +func SetRevealDuration(_ realm, duration int64) { + if runtime.OriginCaller() == admin { + RevealDuration = duration * int64(time.Second) + } else { + panic("error: Only admin can set reveal duration.") + } +} + +// SetVoteTokenPrice sets the price (in ugnot) to cast a vote. +// @param price - The new vote price. +func SetVoteTokenPrice(_ realm, price int64) { + if runtime.OriginCaller() == admin { + VoteTokenPrice = price + } else { + panic("error: Only admin can set vote price.") + } +} + +// -- view functions -- + +// GetDispute returns the details of a specific dispute. +// @param id - The ID of the dispute (same as Request ID). +func GetDispute(_ realm, id string) Dispute { + return *getDispute(id) +} + +// GetDisputeDuration returns the current dispute duration. +func GetDisputeDuration(_ realm) int64 { + return DisputeDuration +} + +// GetVoteTokenPrice returns the current vote price. +func GetVoteTokenPrice(_ realm) int64 { + return VoteTokenPrice +} + +// GetDisputeEndTime returns the end time of the voting period for a specific dispute. +// @param id - The ID of the dispute (same as Request ID). +func GetDisputeEndTime(_ realm, id string) time.Time { + dispute := getDispute(id) + return dispute.EndTime +} + +// GetDisputeVotesAmount returns the total number of votes cast in a dispute. +// @param id - The ID of the dispute (same as Request ID). +func GetDisputeVotesAmount(_ realm, id string) int64 { + dispute := getDispute(id) + return int64(len(dispute.Votes)) +} + +// GetRevealEndTime returns the end time of the reveal period for a specific dispute. +// @param id - The ID of the dispute (same as Request ID). +func GetRevealEndTime(_ realm, id string) time.Time { + dispute := getDispute(id) + return dispute.EndRevealTime +} + +// GetRevealDuration returns the current reveal duration. +func GetRevealDuration(_ realm) int64 { + return RevealDuration +} + + +// Utils functions + +func getDispute(id string) *Dispute { + dispute, exists := Disputes.Get(id) + if !exists { + panic("error: Dispute with this ID does not exist.") + } + return dispute.(*Dispute) +} \ No newline at end of file diff --git a/packages/r/intermarch3/goo/gnomod.toml b/packages/r/intermarch3/goo/gnomod.toml new file mode 100644 index 0000000..8c11fe4 --- /dev/null +++ b/packages/r/intermarch3/goo/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/intermarch3/goo" +gno = "0.9" diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno new file mode 100644 index 0000000..be225a7 --- /dev/null +++ b/packages/r/intermarch3/goo/oracle.gno @@ -0,0 +1,294 @@ +package goo + +import ( + "chain" + "chain/banker" + "chain/runtime" + "strconv" + "time" + + "gno.land/p/nt/avl" + "gno.land/p/nt/seqid" +) + +type DataRequest struct { + Id string + Creator address + Timestamp time.Time + AncillaryData string + YesNoQuestion bool + ProposedValue int64 + Proposer address + ProposerBond int64 + Disputer address + DisputerBond int64 + ResolutionTime time.Time + WinningValue int64 + State string // "Requested", "Proposed", "Disputed", "Resolved" + Deadline time.Time +} + +var ( + admin = address("g1qdwemgqgqm7s42gy6xcj7f22uhfdzr82st3sy3") + ResolutionTime = 2 * int64(time.Minute.Seconds()) + RequesterReward = 1 * int64(1_000_000) // in GNOT + Bond = 2 * int64(1_000_000) // in GNOT + Requests = avl.NewTree() + Bank = banker.NewBanker(banker.BankerTypeRealmSend) + idGenerator seqid.ID +) + +// -- PUBLIC FUNCTIONS -- + +// 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). +// @param ancillaryData - The human-readable question for the oracle. +// @param yesNoQuestion - True if the question is a Yes/No question. +// @param deadline - A Unix timestamp for when the request should be fulfilled (need to be more than now + 24h). +func RequestData(cur realm, ancillaryData string, yesNoQuestion bool, deadline int64) string { + if ancillaryData == "" { + panic("error: Ancillary data cannot be empty.") + } + if deadline < time.Now().Add(24*time.Hour).Unix() { + panic("error: Deadline must be at least 24 hours in the future.") + } + + // TODO: check that x GNOT is sent as reward + coins := banker.OriginSend() + if len(coins) != 1 || coins.AmountOf("ugnot") != RequesterReward { + panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(RequesterReward, 10) + " ugnot.") + } + + id := idGenerator.Next().String() + + request := DataRequest{ + Id: id, + Timestamp: time.Now(), + AncillaryData: ancillaryData, + YesNoQuestion: yesNoQuestion, + State: "Requested", + Deadline: time.Unix(deadline, 0), + Creator: runtime.PreviousRealm().Address(), + } + Requests.Set(id, request) + chain.Emit("DataRequested", "id", id, "timestamp", request.Timestamp.String(), "ancillaryData", ancillaryData) + return id +} + +// ProposeValue allows a user to propose a value for a requested data point. +// `Bond` value needs to be sent to the contract as a bond. +// @param id - The ID of the data request. +// @param proposedValue - The proposed answer to the question. +func ProposeValue(cur realm, id string, proposedValue int64) { + request := getRequest(id) + if request.State != "Requested" { + panic("error: Request is not in 'Requested' state.") + } + if request.YesNoQuestion && proposedValue != 0 && proposedValue != 1 { + panic("error: Proposed value must be 0 or 1 for yes/no questions.") + } + if time.Now().After(request.Deadline) { + panic("error: Deadline for proposal has passed.") + } + + // TODO: check that proposerBond is x GNOT + coins := banker.OriginSend() + if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { + panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") + } + + request.ProposedValue = proposedValue + request.Proposer = runtime.PreviousRealm().Address() + request.ProposerBond = Bond + request.ResolutionTime = time.Now().Add(time.Duration(ResolutionTime) * time.Second) + request.State = "Proposed" + Requests.Set(id, request) + chain.Emit("ValueProposed", "id", id, "proposedValue", strconv.Itoa(int(proposedValue)), "proposer", request.Proposer.String(), "resolutionTime", request.ResolutionTime.String()) +} + +// DisputeData allows a user to dispute a proposed value. +// `Bond` value needs to be sent to the contract as a bond. +// @param id - The ID of the data request to dispute. +func DisputeData(cur realm, id string) string { + request := getRequest(id) + if request.Proposer == runtime.PreviousRealm().Address() { + panic("error: Proposer cannot dispute their own proposal.") + } + if request.State != "Proposed" { + panic("error: Request is not in 'Proposed' state.") + } + if time.Now().After(request.ResolutionTime) { + panic("error: Dispute period has ended.") + } + + // Vérifie que la caution a bien été envoyée. + coins := banker.OriginSend() + if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { + panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") + } + + request.Disputer = runtime.PreviousRealm().Address() + request.DisputerBond = Bond + request.State = "Disputed" + Requests.Set(id, request) + initiateDispute(id) + chain.Emit("DataDisputed", "id", id, "disputer", request.Disputer.String()) + return "Time: " + time.Now().String() +} + +// ResolveRequest finalizes an undisputed request after the resolution period has passed. +// @param id - The ID of the data request to resolve. +func ResolveRequest(cur realm, id string) { + request := getRequest(id) + if request.State == "Disputed" { + panic("error: Request is in 'Disputed' state.") + } + if request.State == "Proposed" && request.ResolutionTime.After(time.Now()) { + panic("error: Resolution period has not ended yet.") + } + if request.State == "Requested" { + panic("error: Request has not been proposed yet.") + } + if request.State == "Resolved" { + panic("error: Request is already resolved.") + } + request.State = "Resolved" + request.WinningValue = request.ProposedValue + Requests.Set(id, request) + + // TODO: Return bonds + reward to proposer + from := runtime.CurrentRealm().Address() + to := request.Proposer + totalPayout := request.ProposerBond + RequesterReward + payout := chain.Coins{chain.Coin{Denom: "ugnot", Amount: totalPayout}} + Bank.SendCoins(from, to, payout) + + chain.Emit("RequestResolved", "id", id, "winningValue", strconv.Itoa(int(request.WinningValue))) +} + +// RequestResult returns the winning value of a resolved request. +// @param id - The ID of the data request. +func RequestResult(cur realm, id string) int64 { + request := getRequest(id) + if request.State != "Resolved" { + panic("error: Request is not resolved.") + } + return request.WinningValue +} + +// RequesterRetreiveFund allows the original requester to get their reward back if the deadline passed without a proposal. +// @param id - The ID of the data request. +func RequesterRetreiveFund(cur realm, id string) { + request := getRequest(id) + if request.State != "Requested" { + panic("error: cannot retreive fund as requests fulfilled.") + } + if request.Creator != runtime.PreviousRealm().Address() { + panic("error: Only the creator of the request can retreive the fund.") + } + if request.Deadline.After(time.Now()) { + panic("error: Cannot retreive fund before the deadline.") + } + + from := runtime.CurrentRealm().Address() + to := request.Creator + refund := chain.Coins{chain.Coin{Denom: "ugnot", Amount: RequesterReward}} + Bank.SendCoins(from, to, refund) + + request.State = "Expired" + Requests.Set(id, request) + chain.Emit("RequestExpired", "id", id) +} + +// -- ADMIN FUNCTIONS -- + +// SetResolutionDuration sets the duration (in seconds) for the resolution period. +// @param duration - The new resolution duration in seconds. +func SetResolutionDuration(_ realm, duration int64) { + if runtime.OriginCaller() == admin { + ResolutionTime = duration + chain.Emit("ResolutionTimeSet", "duration", strconv.Itoa(int(duration))) + } else { + panic("error: Only the admin can set the resolution time.") + } +} + +// SetRequesterReward sets the reward amount for a successful proposal. +// @param reward - The new reward amount in ugnot. +func SetRequesterReward(_ realm, reward int64) { + if runtime.OriginCaller() == admin { + RequesterReward = reward + chain.Emit("RequesterRewardSet", "reward", strconv.Itoa(int(reward))) + } else { + panic("error: Only the admin can set the requester reward.") + } +} + +// SetBond sets the bond amount required for proposals and disputes. +// @param bond - The new bond amount in ugnot. +func SetBond(_ realm, bond int64) { + if runtime.OriginCaller() == admin { + Bond = bond + chain.Emit("BondSet", "bond", strconv.Itoa(int(bond))) + } else { + panic("error: Only the admin can set the proposer bond.") + } +} + +// ChangeAdmin transfers admin privileges to a new address. +// @param newAdmin - The address of the new admin. +func ChangeAdmin(_ realm, newAdmin address) { + if runtime.OriginCaller() == admin { + admin = newAdmin + chain.Emit("AdminChanged", "newAdmin", newAdmin.String()) + } else { + panic("error: Only the admin can change the admin.") + } +} + +// -- VIEW FUNCTIONS -- + +// GetRequest returns the details of a specific data request. +// @param id - The ID of the data request. +func GetRequest(_ realm, id string) DataRequest { + return *getRequest(id) +} + +// GetBond returns the current bond amount. +func GetBond(_ realm) int64 { + return Bond +} + +// GetResolutionTime returns the current resolution time duration. +func GetResolutionTime(_ realm) int64 { + return ResolutionTime +} + +// GetRequesterReward returns the current requester reward amount. +func GetRequesterReward(_ realm) int64 { + return RequesterReward +} + +// GetRequestState returns the current state of a specific data request. +// @param id - The ID of the data request. +func GetRequestState(_ realm, id string) string { + request := getRequest(id) + return request.State +} + +// GetAdmin returns the current admin address. +func GetAdmin(_ realm) address { + return admin +} + + +// Utils functions + +func getRequest(id string) *DataRequest { + request, exists := Requests.Get(id) + if !exists { + panic("error: Request with this ID does not exist.") + } + return request.(*DataRequest) +} \ No newline at end of file diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno new file mode 100644 index 0000000..e147b4d --- /dev/null +++ b/packages/r/intermarch3/goo/render.gno @@ -0,0 +1,188 @@ +package goo + +import ( + "chain/runtime" + "strconv" + "time" + + "gno.land/p/moul/md" + "gno.land/p/moul/realmpath" + "gno.land/p/moul/txlink" + "gno.land/p/nt/ufmt" +) + +const DateFormat = "January 2 2006, 03:04:04 PM" + +func Render(path string) string { + req := realmpath.Parse(path) + if req.Path == "" { + return renderHome() + } + return renderRequestPage(req.Path) +} + +func renderHome() string { + msg := md.H1("OO Home") + msg += md.Paragraph("Welcome to the first Optimistic Oracle on GnoLand! This project is developed by " + md.Link("@intermarch3", "/r/intermarch3/home")) + msg += md.Paragraph("For more information, visit the " + md.Link("Readme", runtime.CurrentRealm().PkgPath()[8:]+"$source") + ".") + msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(RequesterReward))).URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(RequesterReward/1_000_000)) + " GNOT to the proposer).\n\n" + msg += md.H2("Current Requests :") + // List current requests + var table []string + table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) + Requests.Iterate("", "", func(key string, value any) bool { + if value.(*DataRequest).State == "Resolved" { + return false + } + table = append(table, renderRequest(value.(DataRequest))...) + return false + }) + msg += md.ColumnsN(table, 4, false) + if len(table) == 4 { + msg += md.Paragraph("No current requests.") + } + + msg += md.HorizontalRule() + // List last 5 resolved requests + msg += md.H2("Last 5 Resolved Requests :") + var resolved []string + resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) + Requests.Iterate("", "", func(key string, value any) bool { + if value.(*DataRequest).State == "Resolved" { + return false + } + resolved = append(resolved, md.Paragraph(value.(*DataRequest).AncillaryData)) + resolved = append(resolved, md.Paragraph(rendervalue(value.(DataRequest), value.(*DataRequest).WinningValue))) + resolved = append(resolved, md.Paragraph(value.(*DataRequest).Proposer.String())) + resolved = append(resolved, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+value.(*DataRequest).Id)) + return false + }) + msg += md.ColumnsN(resolved, 4, false) + if len(resolved) == 4 { + msg += md.Paragraph("No resolved requests yet.") + } + return msg +} + +func renderRequestPage(id string) string { + res, exists := Requests.Get(id) + req := res.(*DataRequest) + if !exists { + return md.H1("error: No request with this ID exists.") + } + msg := md.H1("Question: " + req.AncillaryData) + msg += md.H2("Details:\n\n") + msg += md.H3("Request ID: "+req.Id) + "\n\n" + msg += md.H3("Requested at: "+req.Timestamp.Format(DateFormat)) + "\n\n" + if req.State == "Requested" && req.Deadline.Unix() < time.Now().Unix() { + msg += md.H3("Deadline Missed: requests not fulfilled") + "\n\n" + return msg + } + msg += md.H3("State: "+req.State) + "\n\n" + if req.State == "Requested" { + msg += md.Paragraph("This request has not been proposed yet.") + "\n\n" + msg += md.H3("The reward for the proposer is "+strconv.Itoa(int(RequesterReward/1_000_000))+" GNOT.") + "\n\n" + msg += renderAction(*req) + return msg + } + if req.State == "Resolved" { + msg += md.H3("Winning Value: "+rendervalue(*req, req.WinningValue)) + "\n\n" + dispute, exist := Disputes.Get(req.Id) + if !exist { + msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" + return msg + } + msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.(*Dispute).Votes)) + "\n\n") + msg += md.Paragraph("Reveal Votes: \n\n") + if dispute.(*Dispute).NbResolvedVotes > 0 { + msg += renderRevealedVotes(*dispute.(*Dispute)) + "\n\n" + } else { + msg += md.Paragraph("No votes revealed. Proposed value win by default\n\n") + } + return msg + } + if req.State == "Proposed" { + msg += md.Paragraph("Proposed by: "+req.Proposer.String()) + "\n\n" + msg += md.H3("Proposed Value: "+rendervalue(*req, req.ProposedValue)) + "\n\n" + msg += md.H3("End Resolution Time: "+req.ResolutionTime.Format(DateFormat)) + "\n\n" + } + if req.State == "Disputed" { + dispute, exists := Disputes.Get(req.Id) + if exists { + msg += md.H2("Dispute Details:\n\n") + msg += md.Paragraph("End of Voting Time: " + dispute.(*Dispute).EndTime.Format(DateFormat) + "\n\n") + msg += md.Paragraph("End of Reveal Time: " + dispute.(*Dispute).EndRevealTime.Format(DateFormat)) + msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.(*Dispute).Votes)) + "\n\n") + msg += md.Paragraph("Reveal Votes: \n\n") + if dispute.(*Dispute).NbResolvedVotes > 0 { + msg += renderRevealedVotes(*dispute.(*Dispute)) + "\n\n" + } else { + msg += md.Paragraph("No votes revealed yet.\n\n") + } + } else { + msg += md.Paragraph("No dispute details available.\n\n") + } + } + msg += renderAction(*req) + return msg +} + +func renderRevealedVotes(dispute Dispute) string { + var table []string + table = append(table, md.Bold("Voter"), md.Bold("Vote"), md.Bold("Token Amount")) + request := getRequest(dispute.RequestId) + for _, vote := range dispute.Votes { + if vote.Revealed { + table = append(table, md.Paragraph(vote.Voter.String()), md.Paragraph(rendervalue(*request, vote.Value)), md.Paragraph(strconv.Itoa(int(vote.TokenAmount)))) + } + } + return md.ColumnsN(table, 3, false) +} + +func rendervalue(req DataRequest, val int64) string { + if req.YesNoQuestion { + if val == 1 { + return "Yes" + } else if val == 0 { + return "No" + } else { + return "Invalid value for Yes/No question" + } + } else { + return strconv.Itoa(int(val)) + } +} + +func renderRequest(req DataRequest) []string { + var table []string + table = append(table, md.Paragraph(req.AncillaryData)) + if req.State == "Requested" { + table = append(table, md.Paragraph("N/A")) + } else { + table = append(table, md.Paragraph(rendervalue(req, req.ProposedValue))) + } + table = append(table, md.Paragraph(req.State)) + table = append(table, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+req.Id)) + return table +} + +func renderAction(req DataRequest) string { + action := md.H2("Actions:\n\n") + dispute, _ := Disputes.Get(req.Id) + if req.State == "Requested" { + action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeData").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(Bond))).URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + } else if req.State == "Proposed" { + action += md.Link("Dispute value", ufmt.Sprintf("%s", txlink.NewLink("DisputeData").AddArgs("id", req.Id).SetSend(strconv.Itoa(int(Bond))).URL())) + "\n\n(Note: Disputing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + } else if req.State == "Disputed" { + if req.ResolutionTime.Before(time.Now()) { + action += md.Link("Reveal vote", ufmt.Sprintf("%s", txlink.NewLink("RevealVote").AddArgs("id", req.Id, "value", "YOUR_VALUE_HERE", "salt", "YOUR_SALT_HERE").URL())) + "\n\n(Note: You must reveal your vote before the reveal period ends at " + dispute.(*Dispute).EndRevealTime.UTC().Format(time.UnixDate) + ")" + } else { + action += md.Link("Vote", ufmt.Sprintf("%s", txlink.NewLink("SubmitVote").AddArgs("id", req.Id, "hash", "YOUR_HASH_HERE").URL())) + "\n\n(Note: Voting need to hold a Vote Token and requires you to hash your vote with a salt. Use a tool to generate the sha256 hash and keep your salt safe to reveal your vote)" + } + } else if req.State == "Disputed" && dispute.(*Dispute).EndTime.Before(time.Now()) { + action += md.Link("Resolve dispute", ufmt.Sprintf("%s", txlink.NewLink("ResolveDispute").AddArgs("id", req.Id).URL())) + "\n\n" + } else if req.State == "Proposed" && req.ResolutionTime.Before(time.Now()) { + action += md.Link("Resolve request", ufmt.Sprintf("%s", txlink.NewLink("ResolveRequest").AddArgs("id", req.Id).URL())) + "\n\n" + } + return action +} diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno new file mode 100644 index 0000000..8dffee3 --- /dev/null +++ b/packages/r/intermarch3/goo/resolver.gno @@ -0,0 +1,92 @@ +package goo + +import ( + "chain" + "chain/runtime" +) + +func resolve(id string) int64 { + dispute := getDispute(id) + request := getRequest(id) + if dispute.NbResolvedVotes == 0 { + // If no one voted or reveal their vote, the proposed value wins by default + return request.ProposedValue + } else { + if request.YesNoQuestion { + return resolveYesNo(*dispute) + } else { + return resolveNumeric(*dispute) + } + } +} + +func resolveYesNo(dispute Dispute) int64 { + yesVotes := int64(0) + noVotes := int64(0) + for _, vote := range dispute.Votes { + if vote.Revealed { + if vote.Value == 1 { + yesVotes += vote.TokenAmount + } else if vote.Value == 0 { + noVotes += vote.TokenAmount + } + } + } + var winningValue int64 + var weight int64 + if yesVotes > noVotes { + winningValue = 1 + weight = yesVotes + } else { + winningValue = 0 + weight = noVotes + } + rewardAndSlachVoters(dispute, winningValue, weight) + return winningValue +} + +// resolveNumeric determines the winning value for numeric disputes based on the highest total token weight. +// Not the best algorithm, but gas effective and fast. +func resolveNumeric(dispute Dispute) int64 { + // Collect unique values and aggregate weights + valueWeights := make(map[int64]int64) + for _, vote := range dispute.Votes { + if vote.Revealed { + valueWeights[vote.Value] += vote.TokenAmount + } + } + + // Find the value with the most token weight + var winningValue int64 + var maxWeight int64 + + for value, weight := range valueWeights { + if weight > maxWeight { + maxWeight = weight + winningValue = value + } + } + rewardAndSlachVoters(dispute, winningValue, maxWeight) + return winningValue +} + +func rewardAndSlachVoters(dispute Dispute, winningValue, totalWeight int64) { + for _, vote := range dispute.Votes { + if vote.Revealed { + if vote.Value == winningValue { + // Reward winning voters + VoteToken.mint(vote.Voter, 2) + reward := Bond * (vote.TokenAmount / totalWeight) + if reward != 0 { + Bank.SendCoins(runtime.CurrentRealm().Address(), vote.Voter, chain.Coins{chain.Coin{Denom: "ugnot", Amount: reward}}) + } + chain.Emit("VoterRewarded", "voter", vote.Voter.String()) + } else { + // Slash losing voters + slash := vote.TokenAmount / 4 // 25% slash + VoteToken.burn(vote.Voter, slash) + chain.Emit("VoterSlashed", "voter", vote.Voter.String()) + } + } + } +} diff --git a/packages/r/intermarch3/goo/token.gno b/packages/r/intermarch3/goo/token.gno new file mode 100644 index 0000000..d619453 --- /dev/null +++ b/packages/r/intermarch3/goo/token.gno @@ -0,0 +1,32 @@ +package goo + +import ( + "gno.land/p/demo/tokens/grc20" +) + +type OOT struct { + *grc20.Token + *grc20.PrivateLedger +} + +func newOOToken(name, symbol string, decimals int) *OOT { + token := &OOT{} + t, adm := grc20.NewToken(name, symbol, decimals) + token.Token = t + token.PrivateLedger = adm + return token +} + +// Mint create new tokens to an address. +func (t *OOT) mint(to address, amount int64) { + t.PrivateLedger.Mint(to, amount) +} + +// Burn destroy tokens from an address. +func (t *OOT) burn(from address, amount int64) { + t.PrivateLedger.Burn(from, amount) +} + +func (t *OOT) balanceOf(addr address) int64 { + return t.Token.BalanceOf(addr) +} From 15d24a3146aa9279db9fab3b39be3db5bfab2cd7 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Thu, 9 Oct 2025 23:13:47 +0200 Subject: [PATCH 02/14] fix: remove natspec doc --- packages/r/intermarch3/goo/court.gno | 13 ------------- packages/r/intermarch3/goo/oracle.gno | 18 ------------------ 2 files changed, 31 deletions(-) diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index fc078f7..bdf978f 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -90,8 +90,6 @@ func BalanceOfVoteToken(_ realm) int64 { } // VoteOnDispute allows a user to commit a vote during a dispute. -// @param id - The ID of the dispute (same as Request ID). -// @param hash - The SHA256 hash of the vote value and a secret salt. func VoteOnDispute(cur realm, id string, hash string) { dispute := getDispute(id) res, _ := Requests.Get(id) @@ -128,9 +126,6 @@ func VoteOnDispute(cur realm, id string, hash string) { } // RevealVote allows a user to reveal their vote after the voting period has ended. -// @param id - The ID of the dispute (same as Request ID). -// @param value - The actual value of the vote. -// @param salt - The secret salt used to generate the vote hash. func RevealVote(cur realm, id string, value int64, salt string) { dispute := getDispute(id) if dispute.IsResolved { @@ -166,7 +161,6 @@ func RevealVote(cur realm, id string, value int64, salt string) { } // ResolveDispute finalizes a dispute after the reveal period, tallying votes and setting the winning value. -// @param id - The ID of the dispute to resolve (same as Request ID). func ResolveDispute(cur realm, id string) { dispute := getDispute(id) if dispute.IsResolved { @@ -202,7 +196,6 @@ func ResolveDispute(cur realm, id string) { // -- admin functions -- // SetDisputeDuration sets the duration (in seconds) for the voting period. -// @param duration - The new dispute duration in seconds. func SetDisputeDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { DisputeDuration = duration * int64(time.Second) @@ -212,7 +205,6 @@ func SetDisputeDuration(_ realm, duration int64) { } // SetRevealDuration sets the duration (in seconds) for the reveal period. -// @param duration - The new reveal duration in seconds. func SetRevealDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { RevealDuration = duration * int64(time.Second) @@ -222,7 +214,6 @@ func SetRevealDuration(_ realm, duration int64) { } // SetVoteTokenPrice sets the price (in ugnot) to cast a vote. -// @param price - The new vote price. func SetVoteTokenPrice(_ realm, price int64) { if runtime.OriginCaller() == admin { VoteTokenPrice = price @@ -234,7 +225,6 @@ func SetVoteTokenPrice(_ realm, price int64) { // -- view functions -- // GetDispute returns the details of a specific dispute. -// @param id - The ID of the dispute (same as Request ID). func GetDispute(_ realm, id string) Dispute { return *getDispute(id) } @@ -250,21 +240,18 @@ func GetVoteTokenPrice(_ realm) int64 { } // GetDisputeEndTime returns the end time of the voting period for a specific dispute. -// @param id - The ID of the dispute (same as Request ID). func GetDisputeEndTime(_ realm, id string) time.Time { dispute := getDispute(id) return dispute.EndTime } // GetDisputeVotesAmount returns the total number of votes cast in a dispute. -// @param id - The ID of the dispute (same as Request ID). func GetDisputeVotesAmount(_ realm, id string) int64 { dispute := getDispute(id) return int64(len(dispute.Votes)) } // GetRevealEndTime returns the end time of the reveal period for a specific dispute. -// @param id - The ID of the dispute (same as Request ID). func GetRevealEndTime(_ realm, id string) time.Time { dispute := getDispute(id) return dispute.EndRevealTime diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index be225a7..46d9451 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -43,9 +43,6 @@ var ( // 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). -// @param ancillaryData - The human-readable question for the oracle. -// @param yesNoQuestion - True if the question is a Yes/No question. -// @param deadline - A Unix timestamp for when the request should be fulfilled (need to be more than now + 24h). func RequestData(cur realm, ancillaryData string, yesNoQuestion bool, deadline int64) string { if ancillaryData == "" { panic("error: Ancillary data cannot be empty.") @@ -78,8 +75,6 @@ func RequestData(cur realm, ancillaryData string, yesNoQuestion bool, deadline i // ProposeValue allows a user to propose a value for a requested data point. // `Bond` value needs to be sent to the contract as a bond. -// @param id - The ID of the data request. -// @param proposedValue - The proposed answer to the question. func ProposeValue(cur realm, id string, proposedValue int64) { request := getRequest(id) if request.State != "Requested" { @@ -92,7 +87,6 @@ func ProposeValue(cur realm, id string, proposedValue int64) { panic("error: Deadline for proposal has passed.") } - // TODO: check that proposerBond is x GNOT coins := banker.OriginSend() if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") @@ -109,7 +103,6 @@ func ProposeValue(cur realm, id string, proposedValue int64) { // DisputeData allows a user to dispute a proposed value. // `Bond` value needs to be sent to the contract as a bond. -// @param id - The ID of the data request to dispute. func DisputeData(cur realm, id string) string { request := getRequest(id) if request.Proposer == runtime.PreviousRealm().Address() { @@ -122,7 +115,6 @@ func DisputeData(cur realm, id string) string { panic("error: Dispute period has ended.") } - // Vérifie que la caution a bien été envoyée. coins := banker.OriginSend() if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") @@ -138,7 +130,6 @@ func DisputeData(cur realm, id string) string { } // ResolveRequest finalizes an undisputed request after the resolution period has passed. -// @param id - The ID of the data request to resolve. func ResolveRequest(cur realm, id string) { request := getRequest(id) if request.State == "Disputed" { @@ -157,7 +148,6 @@ func ResolveRequest(cur realm, id string) { request.WinningValue = request.ProposedValue Requests.Set(id, request) - // TODO: Return bonds + reward to proposer from := runtime.CurrentRealm().Address() to := request.Proposer totalPayout := request.ProposerBond + RequesterReward @@ -168,7 +158,6 @@ func ResolveRequest(cur realm, id string) { } // RequestResult returns the winning value of a resolved request. -// @param id - The ID of the data request. func RequestResult(cur realm, id string) int64 { request := getRequest(id) if request.State != "Resolved" { @@ -178,7 +167,6 @@ func RequestResult(cur realm, id string) int64 { } // RequesterRetreiveFund allows the original requester to get their reward back if the deadline passed without a proposal. -// @param id - The ID of the data request. func RequesterRetreiveFund(cur realm, id string) { request := getRequest(id) if request.State != "Requested" { @@ -204,7 +192,6 @@ func RequesterRetreiveFund(cur realm, id string) { // -- ADMIN FUNCTIONS -- // SetResolutionDuration sets the duration (in seconds) for the resolution period. -// @param duration - The new resolution duration in seconds. func SetResolutionDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { ResolutionTime = duration @@ -215,7 +202,6 @@ func SetResolutionDuration(_ realm, duration int64) { } // SetRequesterReward sets the reward amount for a successful proposal. -// @param reward - The new reward amount in ugnot. func SetRequesterReward(_ realm, reward int64) { if runtime.OriginCaller() == admin { RequesterReward = reward @@ -226,7 +212,6 @@ func SetRequesterReward(_ realm, reward int64) { } // SetBond sets the bond amount required for proposals and disputes. -// @param bond - The new bond amount in ugnot. func SetBond(_ realm, bond int64) { if runtime.OriginCaller() == admin { Bond = bond @@ -237,7 +222,6 @@ func SetBond(_ realm, bond int64) { } // ChangeAdmin transfers admin privileges to a new address. -// @param newAdmin - The address of the new admin. func ChangeAdmin(_ realm, newAdmin address) { if runtime.OriginCaller() == admin { admin = newAdmin @@ -250,7 +234,6 @@ func ChangeAdmin(_ realm, newAdmin address) { // -- VIEW FUNCTIONS -- // GetRequest returns the details of a specific data request. -// @param id - The ID of the data request. func GetRequest(_ realm, id string) DataRequest { return *getRequest(id) } @@ -271,7 +254,6 @@ func GetRequesterReward(_ realm) int64 { } // GetRequestState returns the current state of a specific data request. -// @param id - The ID of the data request. func GetRequestState(_ realm, id string) string { request := getRequest(id) return request.State From 91e0309df4d0025c097fdfc3752e345e861b5000 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Wed, 15 Oct 2025 10:10:04 +0200 Subject: [PATCH 03/14] fix: render functions --- .gitignore | 3 ++- packages/r/intermarch3/goo/render.gno | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 723ef36..1f1025f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +.DS_Store \ No newline at end of file diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index e147b4d..0a1ed6f 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -25,13 +25,13 @@ func renderHome() string { msg := md.H1("OO Home") msg += md.Paragraph("Welcome to the first Optimistic Oracle on GnoLand! This project is developed by " + md.Link("@intermarch3", "/r/intermarch3/home")) msg += md.Paragraph("For more information, visit the " + md.Link("Readme", runtime.CurrentRealm().PkgPath()[8:]+"$source") + ".") - msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(RequesterReward))).URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(RequesterReward/1_000_000)) + " GNOT to the proposer).\n\n" + msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(RequesterReward))+"ugnot").URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(RequesterReward/1_000_000)) + " GNOT to the proposer).\n\n" msg += md.H2("Current Requests :") // List current requests var table []string table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) Requests.Iterate("", "", func(key string, value any) bool { - if value.(*DataRequest).State == "Resolved" { + if value.(DataRequest).State == "Resolved" { return false } table = append(table, renderRequest(value.(DataRequest))...) @@ -48,13 +48,13 @@ func renderHome() string { var resolved []string resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) Requests.Iterate("", "", func(key string, value any) bool { - if value.(*DataRequest).State == "Resolved" { + if value.(DataRequest).State != "Resolved" { return false } - resolved = append(resolved, md.Paragraph(value.(*DataRequest).AncillaryData)) - resolved = append(resolved, md.Paragraph(rendervalue(value.(DataRequest), value.(*DataRequest).WinningValue))) - resolved = append(resolved, md.Paragraph(value.(*DataRequest).Proposer.String())) - resolved = append(resolved, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+value.(*DataRequest).Id)) + resolved = append(resolved, md.Paragraph(value.(DataRequest).AncillaryData)) + resolved = append(resolved, md.Paragraph(rendervalue(value.(DataRequest), value.(DataRequest).WinningValue))) + resolved = append(resolved, md.Paragraph(value.(DataRequest).Proposer.String())) + resolved = append(resolved, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+value.(DataRequest).Id)) return false }) msg += md.ColumnsN(resolved, 4, false) @@ -66,7 +66,7 @@ func renderHome() string { func renderRequestPage(id string) string { res, exists := Requests.Get(id) - req := res.(*DataRequest) + req := res.(DataRequest) if !exists { return md.H1("error: No request with this ID exists.") } @@ -82,11 +82,11 @@ func renderRequestPage(id string) string { if req.State == "Requested" { msg += md.Paragraph("This request has not been proposed yet.") + "\n\n" msg += md.H3("The reward for the proposer is "+strconv.Itoa(int(RequesterReward/1_000_000))+" GNOT.") + "\n\n" - msg += renderAction(*req) + msg += renderAction(req) return msg } if req.State == "Resolved" { - msg += md.H3("Winning Value: "+rendervalue(*req, req.WinningValue)) + "\n\n" + msg += md.H3("Winning Value: "+rendervalue(req, req.WinningValue)) + "\n\n" dispute, exist := Disputes.Get(req.Id) if !exist { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" @@ -103,7 +103,7 @@ func renderRequestPage(id string) string { } if req.State == "Proposed" { msg += md.Paragraph("Proposed by: "+req.Proposer.String()) + "\n\n" - msg += md.H3("Proposed Value: "+rendervalue(*req, req.ProposedValue)) + "\n\n" + msg += md.H3("Proposed Value: "+rendervalue(req, req.ProposedValue)) + "\n\n" msg += md.H3("End Resolution Time: "+req.ResolutionTime.Format(DateFormat)) + "\n\n" } if req.State == "Disputed" { @@ -123,7 +123,7 @@ func renderRequestPage(id string) string { msg += md.Paragraph("No dispute details available.\n\n") } } - msg += renderAction(*req) + msg += renderAction(req) return msg } From b7a517629c2aedd780f043196ced4edf8bb3253f Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Wed, 15 Oct 2025 11:01:17 +0200 Subject: [PATCH 04/14] fix: var type errors --- packages/r/intermarch3/goo/oracle.gno | 6 +++--- packages/r/intermarch3/goo/render.gno | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index 46d9451..e4e076c 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -235,7 +235,7 @@ func ChangeAdmin(_ realm, newAdmin address) { // GetRequest returns the details of a specific data request. func GetRequest(_ realm, id string) DataRequest { - return *getRequest(id) + return getRequest(id) } // GetBond returns the current bond amount. @@ -267,10 +267,10 @@ func GetAdmin(_ realm) address { // Utils functions -func getRequest(id string) *DataRequest { +func getRequest(id string) DataRequest { request, exists := Requests.Get(id) if !exists { panic("error: Request with this ID does not exist.") } - return request.(*DataRequest) + return request.(DataRequest) } \ No newline at end of file diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index 0a1ed6f..bd93d19 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -133,7 +133,7 @@ func renderRevealedVotes(dispute Dispute) string { request := getRequest(dispute.RequestId) for _, vote := range dispute.Votes { if vote.Revealed { - table = append(table, md.Paragraph(vote.Voter.String()), md.Paragraph(rendervalue(*request, vote.Value)), md.Paragraph(strconv.Itoa(int(vote.TokenAmount)))) + table = append(table, md.Paragraph(vote.Voter.String()), md.Paragraph(rendervalue(request, vote.Value)), md.Paragraph(strconv.Itoa(int(vote.TokenAmount)))) } } return md.ColumnsN(table, 3, false) From 6ed3498db6b0062f649cc11275d2b94bdbb25777 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sun, 19 Oct 2025 11:49:25 +0200 Subject: [PATCH 05/14] fix: errors and Leon's comment --- packages/r/intermarch3/goo/README.md | 18 +++---- packages/r/intermarch3/goo/court.gno | 48 ++++------------- packages/r/intermarch3/goo/oracle.gno | 17 ------ packages/r/intermarch3/goo/render.gno | 70 ++++++++++++++++++------- packages/r/intermarch3/goo/resolver.gno | 9 ++-- packages/r/intermarch3/goo/token.gno | 5 -- packages/r/intermarch3/goo/types.gno | 58 ++++++++++++++++++++ 7 files changed, 131 insertions(+), 94 deletions(-) create mode 100644 packages/r/intermarch3/goo/types.gno diff --git a/packages/r/intermarch3/goo/README.md b/packages/r/intermarch3/goo/README.md index 1e863db..54f3f8c 100644 --- a/packages/r/intermarch3/goo/README.md +++ b/packages/r/intermarch3/goo/README.md @@ -62,7 +62,7 @@ Once the reveal period is over, anyone can call `ResolveDispute`. ## Architecture -The oracle is composed of three main contracts: +The oracle contract is composed of three main files : - `oracle.gno`: Manages the data request lifecycle (request, propose, dispute, resolve). It is the main entry point for users. - `court.gno`: Handles the entire dispute resolution process, including the commit-reveal voting scheme. @@ -89,32 +89,32 @@ Here is a full workflow using `gnokey`. **0. Buy Vote Token for Voter Role** ```bash # Buy Oracle Soulbound Token (replace with your key name) -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "BuyInitialVoteToken" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "BuyInitialVoteToken" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **1. Request Data** ```bash # Ask a Yes/No question: "Will ETH be below $4000 ?" (replace DEADLINE_TIMESTAMP with a future unix timestamp more than 24h from now) -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "RequestData" -args "ETH below 4000$ ?" -args "true" -args "DEADLINE_TIMESTAMP" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "RequestData" -args "ETH below 4000$ ?" -args "true" -args "DEADLINE_TIMESTAMP" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **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" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "ProposeValue" -args "ID" -args "0" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "2000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **If no one disputes within the liveness period, anyone can resolve the request:** ```bash # Resolve the request (replace ID with the actual ID) -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "ResolveRequest" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "ResolveRequest" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **3. Dispute the Value** ```bash # Dispute the proposal (replace ID with the actual ID) -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "DisputeData" -args "ID" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "2000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "DisputeData" -args "ID" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "2000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **4. Vote on the Dispute** @@ -122,19 +122,19 @@ First, generate a hash locally. Let's vote "No" (value 0) with salt "mysecret". Hash: `sha256("0" + "test")` -> `a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21` ```bash # Commit the vote (replace ID with the actual ID) -ggnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "VoteOnDispute" -args "ID" -args "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +ggnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "VoteOnDispute" -args "ID" -args "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21" -gas-fee 1000000ugnot -gas-wanted 5000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **5. Reveal the Vote** ```bash # Reveal the vote after the voting period ends (replace ID with the actual ID) -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "RevealVote" -args "ID" -args "0" -args "test" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "RevealVote" -args "ID" -args "0" -args "test" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` **6. Resolve the Dispute** ```bash # After the reveal period, anyone can trigger the final resolution (replace ID with the actual ID). -gnokey maketx call -pkgpath "gno.land/r/intermarch3/oo" -func "ResolveDispute" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" +gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "ResolveDispute" -args "ID" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" ``` When testing with `gnodev` locally, ensure to make transactions between waiting periods as `gnodev` only creates blocks when a transaction is made, and the oracle relies on current block timestamps. diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index bdf978f..1635dd4 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -4,51 +4,23 @@ import ( "chain" "chain/banker" "chain/runtime" - "gno.land/p/nt/avl" "strconv" "time" - "crypto/sha256" "encoding/hex" -) -type Vote struct { - RequestId string - Voter address - TokenAmount int64 - Hash string - Value int64 - Revealed bool -} - -type Voter struct { - HasVoted bool - VoteIndex int64 -} + "gno.land/p/nt/avl" +) -type Dispute struct { - RequestId string - Votes []Vote - NbResolvedVotes int64 - Voters *avl.Tree - IsResolved bool - WinningValue int64 - EndTime time.Time - EndRevealTime time.Time -} var ( Disputes = avl.NewTree() DisputeDuration = 2 * int64(time.Minute.Seconds()) RevealDuration = 2 * int64(time.Minute.Seconds()) VoteTokenPrice = 1 * int64(1_000_000) // in GNOT - VoteToken *OOT + VoteToken = newOOToken("Gno Optimistic Oracle Token", "goot", 6) ) -func init() { - VoteToken = newOOToken("Gno Optimistic Oracle Token", "goot", 6) -} - func initiateDispute(id string) { if _, exists := Disputes.Get(id); exists { panic("error: Dispute for this request already exists.") @@ -93,7 +65,7 @@ func BalanceOfVoteToken(_ realm) int64 { func VoteOnDispute(cur realm, id string, hash string) { dispute := getDispute(id) res, _ := Requests.Get(id) - request := res.(*DataRequest) + request := res.(DataRequest) if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() { panic("error: Proposer and Disputer cannot vote in this dispute.") } @@ -138,10 +110,10 @@ func RevealVote(cur realm, id string, value int64, salt string) { panic("error: Reveal period has ended.") } voter, exist := dispute.Voters.Get(string(runtime.PreviousRealm().Address())) - if !exist || !voter.(*Voter).HasVoted { + if !exist || !voter.(Voter).HasVoted { panic("error: Voter did not participate in this dispute.") } - vote := dispute.Votes[voter.(*Voter).VoteIndex] + vote := dispute.Votes[voter.(Voter).VoteIndex] if vote.Revealed { panic("error: Vote already revealed.") } @@ -155,7 +127,7 @@ func RevealVote(cur realm, id string, value int64, salt string) { vote.Value = value vote.Revealed = true dispute.NbResolvedVotes += 1 - dispute.Votes[voter.(*Voter).VoteIndex] = vote + dispute.Votes[voter.(Voter).VoteIndex] = vote Disputes.Set(id, dispute) chain.Emit("VoteRevealed", "id", id, "voter", vote.Voter.String(), "value", strconv.Itoa(int(value))) } @@ -226,7 +198,7 @@ func SetVoteTokenPrice(_ realm, price int64) { // GetDispute returns the details of a specific dispute. func GetDispute(_ realm, id string) Dispute { - return *getDispute(id) + return getDispute(id) } // GetDisputeDuration returns the current dispute duration. @@ -265,10 +237,10 @@ func GetRevealDuration(_ realm) int64 { // Utils functions -func getDispute(id string) *Dispute { +func getDispute(id string) Dispute { dispute, exists := Disputes.Get(id) if !exists { panic("error: Dispute with this ID does not exist.") } - return dispute.(*Dispute) + return dispute.(Dispute) } \ No newline at end of file diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index e4e076c..e5ab82e 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -11,23 +11,6 @@ import ( "gno.land/p/nt/seqid" ) -type DataRequest struct { - Id string - Creator address - Timestamp time.Time - AncillaryData string - YesNoQuestion bool - ProposedValue int64 - Proposer address - ProposerBond int64 - Disputer address - DisputerBond int64 - ResolutionTime time.Time - WinningValue int64 - State string // "Requested", "Proposed", "Disputed", "Resolved" - Deadline time.Time -} - var ( admin = address("g1qdwemgqgqm7s42gy6xcj7f22uhfdzr82st3sy3") ResolutionTime = 2 * int64(time.Minute.Seconds()) diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index bd93d19..17cb629 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -22,14 +22,15 @@ func Render(path string) string { } func renderHome() string { - msg := md.H1("OO Home") + msg := md.H1("GOO Home") msg += md.Paragraph("Welcome to the first Optimistic Oracle on GnoLand! This project is developed by " + md.Link("@intermarch3", "/r/intermarch3/home")) msg += md.Paragraph("For more information, visit the " + md.Link("Readme", runtime.CurrentRealm().PkgPath()[8:]+"$source") + ".") msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(RequesterReward))+"ugnot").URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(RequesterReward/1_000_000)) + " GNOT to the proposer).\n\n" msg += md.H2("Current Requests :") - // List current requests + // List current requests and build table var table []string table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) + Requests.Iterate("", "", func(key string, value any) bool { if value.(DataRequest).State == "Resolved" { return false @@ -38,15 +39,19 @@ func renderHome() string { return false }) msg += md.ColumnsN(table, 4, false) + + // If no current requests, show a message if len(table) == 4 { msg += md.Paragraph("No current requests.") } msg += md.HorizontalRule() - // List last 5 resolved requests + + // List last 5 resolved requests and build table msg += md.H2("Last 5 Resolved Requests :") var resolved []string resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) + Requests.Iterate("", "", func(key string, value any) bool { if value.(DataRequest).State != "Resolved" { return false @@ -58,26 +63,34 @@ func renderHome() string { return false }) msg += md.ColumnsN(resolved, 4, false) + + // If no resolved requests, show a message if len(resolved) == 4 { msg += md.Paragraph("No resolved requests yet.") } return msg } +// renderRequestPage renders the page for a specific request func renderRequestPage(id string) string { res, exists := Requests.Get(id) req := res.(DataRequest) if !exists { return md.H1("error: No request with this ID exists.") } + msg := md.H1("Question: " + req.AncillaryData) msg += md.H2("Details:\n\n") msg += md.H3("Request ID: "+req.Id) + "\n\n" + + // check if the request is past the deadline msg += md.H3("Requested at: "+req.Timestamp.Format(DateFormat)) + "\n\n" if req.State == "Requested" && req.Deadline.Unix() < time.Now().Unix() { msg += md.H3("Deadline Missed: requests not fulfilled") + "\n\n" return msg } + + // show the state of the request msg += md.H3("State: "+req.State) + "\n\n" if req.State == "Requested" { msg += md.Paragraph("This request has not been proposed yet.") + "\n\n" @@ -85,37 +98,45 @@ func renderRequestPage(id string) string { msg += renderAction(req) return msg } + + // if the request is resolved, show the winning value and the number of votes if req.State == "Resolved" { msg += md.H3("Winning Value: "+rendervalue(req, req.WinningValue)) + "\n\n" - dispute, exist := Disputes.Get(req.Id) + res, exist := Disputes.Get(req.Id) + dispute := res.(Dispute) if !exist { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" return msg } - msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.(*Dispute).Votes)) + "\n\n") + msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") msg += md.Paragraph("Reveal Votes: \n\n") - if dispute.(*Dispute).NbResolvedVotes > 0 { - msg += renderRevealedVotes(*dispute.(*Dispute)) + "\n\n" + if dispute.NbResolvedVotes > 0 { + msg += renderRevealedVotes(dispute) + "\n\n" } else { msg += md.Paragraph("No votes revealed. Proposed value win by default\n\n") } return msg } + + // if the request is proposed, show the proposed value and the end resolution time if req.State == "Proposed" { msg += md.Paragraph("Proposed by: "+req.Proposer.String()) + "\n\n" msg += md.H3("Proposed Value: "+rendervalue(req, req.ProposedValue)) + "\n\n" msg += md.H3("End Resolution Time: "+req.ResolutionTime.Format(DateFormat)) + "\n\n" } + + // if the request is disputed, show the dispute details if req.State == "Disputed" { - dispute, exists := Disputes.Get(req.Id) - if exists { + res, exist := Disputes.Get(req.Id) + dispute := res.(Dispute) + if exist { msg += md.H2("Dispute Details:\n\n") - msg += md.Paragraph("End of Voting Time: " + dispute.(*Dispute).EndTime.Format(DateFormat) + "\n\n") - msg += md.Paragraph("End of Reveal Time: " + dispute.(*Dispute).EndRevealTime.Format(DateFormat)) - msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.(*Dispute).Votes)) + "\n\n") - msg += md.Paragraph("Reveal Votes: \n\n") - if dispute.(*Dispute).NbResolvedVotes > 0 { - msg += renderRevealedVotes(*dispute.(*Dispute)) + "\n\n" + msg += md.Paragraph("End of Voting Time: " + dispute.EndTime.Format(DateFormat) + "\n\n") + msg += md.Paragraph("End of Reveal Time: " + dispute.EndRevealTime.Format(DateFormat)) + msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") + msg += md.Paragraph("Revealed Votes: \n\n") + if dispute.NbResolvedVotes > 0 { + msg += renderRevealedVotes(dispute) + "\n\n" } else { msg += md.Paragraph("No votes revealed yet.\n\n") } @@ -123,10 +144,12 @@ func renderRequestPage(id string) string { msg += md.Paragraph("No dispute details available.\n\n") } } + msg += renderAction(req) return msg } +// create table of the votes that have been revealed func renderRevealedVotes(dispute Dispute) string { var table []string table = append(table, md.Bold("Voter"), md.Bold("Vote"), md.Bold("Token Amount")) @@ -166,22 +189,29 @@ func renderRequest(req DataRequest) []string { return table } +// return appropriate action link based on the state of the request func renderAction(req DataRequest) string { action := md.H2("Actions:\n\n") dispute, _ := Disputes.Get(req.Id) if req.State == "Requested" { - action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeData").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(Bond))).URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + // tx to propose a value + action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeValue").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(Bond))+"ugnot").URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" } else if req.State == "Proposed" { - action += md.Link("Dispute value", ufmt.Sprintf("%s", txlink.NewLink("DisputeData").AddArgs("id", req.Id).SetSend(strconv.Itoa(int(Bond))).URL())) + "\n\n(Note: Disputing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + // tx to dispute a value + action += md.Link("Dispute value", ufmt.Sprintf("%s", txlink.NewLink("DisputeData").AddArgs("id", req.Id).SetSend(strconv.Itoa(int(Bond))+"ugnot").URL())) + "\n\n(Note: Disputing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" } else if req.State == "Disputed" { if req.ResolutionTime.Before(time.Now()) { - action += md.Link("Reveal vote", ufmt.Sprintf("%s", txlink.NewLink("RevealVote").AddArgs("id", req.Id, "value", "YOUR_VALUE_HERE", "salt", "YOUR_SALT_HERE").URL())) + "\n\n(Note: You must reveal your vote before the reveal period ends at " + dispute.(*Dispute).EndRevealTime.UTC().Format(time.UnixDate) + ")" + // tx to reveal a vote + action += md.Link("Reveal vote", ufmt.Sprintf("%s", txlink.NewLink("RevealVote").AddArgs("id", req.Id, "value", "YOUR_VALUE_HERE", "salt", "YOUR_SALT_HERE").URL())) + "\n\n(Note: You must reveal your vote before the reveal period ends at " + dispute.(Dispute).EndRevealTime.UTC().Format(time.UnixDate) + ")" } else { - action += md.Link("Vote", ufmt.Sprintf("%s", txlink.NewLink("SubmitVote").AddArgs("id", req.Id, "hash", "YOUR_HASH_HERE").URL())) + "\n\n(Note: Voting need to hold a Vote Token and requires you to hash your vote with a salt. Use a tool to generate the sha256 hash and keep your salt safe to reveal your vote)" + // tx to vote on a dispute + action += md.Link("Vote", ufmt.Sprintf("%s", txlink.NewLink("VoteOnDispute").AddArgs("id", req.Id, "hash", "YOUR_HASH_HERE").URL())) + "\n\n(Note: Voting need to hold a Vote Token and requires you to hash your vote with a salt. Use a tool to generate the sha256 hash and keep your salt safe to reveal your vote)" } - } else if req.State == "Disputed" && dispute.(*Dispute).EndTime.Before(time.Now()) { + } else if req.State == "Disputed" && dispute.(Dispute).EndTime.Before(time.Now()) { + // tx to resolve a dispute action += md.Link("Resolve dispute", ufmt.Sprintf("%s", txlink.NewLink("ResolveDispute").AddArgs("id", req.Id).URL())) + "\n\n" } else if req.State == "Proposed" && req.ResolutionTime.Before(time.Now()) { + // tx to resolve a request action += md.Link("Resolve request", ufmt.Sprintf("%s", txlink.NewLink("ResolveRequest").AddArgs("id", req.Id).URL())) + "\n\n" } return action diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno index 8dffee3..505b511 100644 --- a/packages/r/intermarch3/goo/resolver.gno +++ b/packages/r/intermarch3/goo/resolver.gno @@ -11,12 +11,11 @@ func resolve(id string) int64 { if dispute.NbResolvedVotes == 0 { // If no one voted or reveal their vote, the proposed value wins by default return request.ProposedValue + } + if request.YesNoQuestion { + return resolveYesNo(dispute) } else { - if request.YesNoQuestion { - return resolveYesNo(*dispute) - } else { - return resolveNumeric(*dispute) - } + return resolveNumeric(dispute) } } diff --git a/packages/r/intermarch3/goo/token.gno b/packages/r/intermarch3/goo/token.gno index d619453..3a72296 100644 --- a/packages/r/intermarch3/goo/token.gno +++ b/packages/r/intermarch3/goo/token.gno @@ -4,11 +4,6 @@ import ( "gno.land/p/demo/tokens/grc20" ) -type OOT struct { - *grc20.Token - *grc20.PrivateLedger -} - func newOOToken(name, symbol string, decimals int) *OOT { token := &OOT{} t, adm := grc20.NewToken(name, symbol, decimals) diff --git a/packages/r/intermarch3/goo/types.gno b/packages/r/intermarch3/goo/types.gno new file mode 100644 index 0000000..4c68b1b --- /dev/null +++ b/packages/r/intermarch3/goo/types.gno @@ -0,0 +1,58 @@ +package goo + +import ( + "time" + + "gno.land/p/nt/avl" + "gno.land/p/demo/tokens/grc20" +) + +// court.gno +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 +} + +// oracle.gno +type DataRequest struct { + Id string + Creator address + Timestamp time.Time + AncillaryData string + YesNoQuestion bool + ProposedValue int64 + Proposer address + ProposerBond int64 + Disputer address + DisputerBond int64 + ResolutionTime time.Time + WinningValue int64 + State string // "Requested", "Proposed", "Disputed", "Resolved" + Deadline time.Time +} + +// token.gno +type OOT struct { + *grc20.Token + *grc20.PrivateLedger +} \ No newline at end of file From 69790d9755fde4483918513dd0190afdefdadab7 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sun, 19 Oct 2025 11:53:31 +0200 Subject: [PATCH 06/14] fix: format --- packages/r/intermarch3/goo/court.gno | 8 +++----- packages/r/intermarch3/goo/oracle.gno | 3 +-- packages/r/intermarch3/goo/types.gno | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index 1635dd4..4c98dd6 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -4,15 +4,14 @@ import ( "chain" "chain/banker" "chain/runtime" - "strconv" - "time" "crypto/sha256" "encoding/hex" + "strconv" + "time" "gno.land/p/nt/avl" ) - var ( Disputes = avl.NewTree() DisputeDuration = 2 * int64(time.Minute.Seconds()) @@ -234,7 +233,6 @@ func GetRevealDuration(_ realm) int64 { return RevealDuration } - // Utils functions func getDispute(id string) Dispute { @@ -243,4 +241,4 @@ func getDispute(id string) Dispute { panic("error: Dispute with this ID does not exist.") } return dispute.(Dispute) -} \ No newline at end of file +} diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index e5ab82e..70425de 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -247,7 +247,6 @@ func GetAdmin(_ realm) address { return admin } - // Utils functions func getRequest(id string) DataRequest { @@ -256,4 +255,4 @@ func getRequest(id string) DataRequest { panic("error: Request with this ID does not exist.") } return request.(DataRequest) -} \ No newline at end of file +} diff --git a/packages/r/intermarch3/goo/types.gno b/packages/r/intermarch3/goo/types.gno index 4c68b1b..7c34491 100644 --- a/packages/r/intermarch3/goo/types.gno +++ b/packages/r/intermarch3/goo/types.gno @@ -3,8 +3,8 @@ package goo import ( "time" - "gno.land/p/nt/avl" "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/avl" ) // court.gno @@ -55,4 +55,4 @@ type DataRequest struct { type OOT struct { *grc20.Token *grc20.PrivateLedger -} \ No newline at end of file +} From 618501af4a4cb67d1cb124f09d664893b4d72dcf Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sun, 19 Oct 2025 15:20:23 +0200 Subject: [PATCH 07/14] test: add a part of the oracle tests --- packages/r/intermarch3/goo/README.md | 2 +- packages/r/intermarch3/goo/oracle.gno | 1 - packages/r/intermarch3/goo/oracle_test.gno | 173 +++++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/r/intermarch3/goo/oracle_test.gno diff --git a/packages/r/intermarch3/goo/README.md b/packages/r/intermarch3/goo/README.md index 54f3f8c..c160759 100644 --- a/packages/r/intermarch3/goo/README.md +++ b/packages/r/intermarch3/goo/README.md @@ -51,7 +51,7 @@ A **Disputer** can challenge the Proposer's value. ### 5. Voting (`VoteOnDispute`) The dispute is now open for voting by all Oracle Soulbound Token holders. The system uses a **commit-reveal scheme** to prevent vote-copying. -- **Commit Phase**: During the `DisputeDuration`, voters submit a hash of their vote (`SHA256(value + salt)`) by calling `VoteOnDispute`. They must also pay a small `VotePrice` fee. // todo +- **Commit Phase**: During the `DisputeDuration`, voters submit a hash of their vote (`SHA256(value + salt)`) by calling `VoteOnDispute`. They must also pay a small `VotePrice` fee. - **Reveal Phase**: After the commit phase ends, the `RevealDuration` begins. Voters must call `RevealVote`, submitting their original `value` and `salt`. The contract verifies that the hash matches the one submitted during the commit phase. ### 6. Dispute Resolution (`ResolveDispute`) diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index 70425de..65dd54c 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -34,7 +34,6 @@ func RequestData(cur realm, ancillaryData string, yesNoQuestion bool, deadline i panic("error: Deadline must be at least 24 hours in the future.") } - // TODO: check that x GNOT is sent as reward coins := banker.OriginSend() if len(coins) != 1 || coins.AmountOf("ugnot") != RequesterReward { panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(RequesterReward, 10) + " ugnot.") diff --git a/packages/r/intermarch3/goo/oracle_test.gno b/packages/r/intermarch3/goo/oracle_test.gno new file mode 100644 index 0000000..1dfecba --- /dev/null +++ b/packages/r/intermarch3/goo/oracle_test.gno @@ -0,0 +1,173 @@ +package goo + +import ( + "time" + "chain" + "testing" + "strconv" + + "gno.land/p/nt/testutils" + "gno.land/p/nt/urequire" +) + +var ( + user1 = testutils.TestAddress("user1") + user2 = testutils.TestAddress("user2") + user3 = testutils.TestAddress("user3") +) + +func TestRequestData(t *testing.T) { + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Incorrect reward amount sent. Required: " + strconv.FormatInt(RequesterReward, 10) + " ugnot.", func() { + RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + }, "user should not be able to request data without sending the requester reward") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + urequire.AbortsWithMessage(t, "error: Ancillary data cannot be empty.", func() { + RequestData(cross, "", true, time.Now().Add(24*time.Hour).Unix()) + }, "user should not be able to request data with an empty ancillary data") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + urequire.AbortsWithMessage(t, "error: Deadline must be at least 24 hours in the future.", func() { + RequestData(cross, "test", true, time.Now().Unix()) + }, "user should not be able to request data with a deadline less than 24 hours in the future") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + urequire.NotPanics(t, func() { + RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + }, "user should be able to request data") +} + +func TestProposeValue(t *testing.T) { + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Request with this ID does not exist.", func() { + ProposeValue(cross, "test", 0) + }, "request id does not exist but didn't revert") + + // create a request + now := time.Now() + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.AbortsWithMessage(t, "error: Proposed value must be 0 or 1 for yes/no questions.", func() { + ProposeValue(cross, id, 9) + }, "user should not be able to propose a value other than 0 or 1 for a yes/no question") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot", func() { + ProposeValue(cross, id, 0) + }, "user should not be able to propose a value without sending the bond") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + setTime(time.Now().Add(25*time.Hour)) + urequire.AbortsWithMessage(t, "error: Deadline for proposal has passed.", func() { + ProposeValue(cross, id, 0) + }, "user should not be able to propose a value after the deadline") + + testing.SetRealm(testing.NewUserRealm(user1)) + setTime(now.Add(time.Hour)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.NotPanics(t, func() { + ProposeValue(cross, id, 0) + }, "user should be able to propose a value") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.AbortsWithMessage(t, "error: Request is not in 'Requested' state.", func() { + ProposeValue(cross, id, 0) + }, "user should not be able to propose a value if the request is not in 'Requested' state") +} + +func TestDisputeData(t *testing.T) { + // setup: create request and propose a value + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + now := time.Now() + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + ProposeValue(cross, id, 0) + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.AbortsWithMessage(t, "error: Request with this ID does not exist.", func() { + DisputeData(cross, "test") + }, "request id does not exist but didn't revert") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.AbortsWithMessage(t, "error: Proposer cannot dispute their own proposal.", func() { + DisputeData(cross, id) + }, "proposer cannot dispute their own proposal") + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot", func() { + DisputeData(cross, id) + }, "user should not be able to dispute a value without sending the bond") + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + setTime(time.Now().Add(time.Duration(ResolutionTime) * time.Second + time.Second)) + urequire.AbortsWithMessage(t, "error: Dispute period has ended.", func() { + DisputeData(cross, id) + }, "user should not be able to dispute a value after the dispute period has ended") + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + setTime(now.Add(time.Second)) + urequire.NotPanics(t, func() { + DisputeData(cross, id) + }, "user should be able to dispute a value") + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + urequire.AbortsWithMessage(t, "error: Request is not in 'Proposed' state.", func() { + DisputeData(cross, id) + }, "user should not be able to dispute a value if the request is not in 'Proposed' state") +} + +func TestResolveRequest(t *testing.T) { + // setup: create request + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + now := time.Now() + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Request has not been proposed yet.", func() { + ResolveRequest(cross, id) + }, "user should not be able to resolve a request if it has not been proposed yet") + + // propose a value + testing.SetRealm(testing.NewUserRealm(user1)) + testing.IssueCoins(user1, chain.Coins{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: Bond}}) + ProposeValue(cross, id, 0) + + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Resolution period has not ended yet.", func() { + ResolveRequest(cross, id) + }, "user should not be able to resolve a request if the resolution period has not ended yet") + + setTime(now.Add(time.Duration(ResolutionTime) * time.Second + time.Second)) + urequire.NotPanics(t, func() { + ResolveRequest(cross, id) + }, "user should be able to resolve a request") +} + +// Helper functions + +func setTime(newTime time.Time) { + ctx := testing.GetContext() + ctx.Time = newTime + testing.SetContext(ctx) +} \ No newline at end of file From c7a7d93b5b5f19dc6fd6e9bfbced9566c21e3b86 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sat, 25 Oct 2025 00:41:05 +0200 Subject: [PATCH 08/14] add: oracle unit tests --- packages/r/intermarch3/goo/court.gno | 8 +- packages/r/intermarch3/goo/court_test.gno | 148 +++++++++++++++++++++ packages/r/intermarch3/goo/oracle.gno | 16 +-- packages/r/intermarch3/goo/oracle_test.gno | 32 ++++- packages/r/intermarch3/goo/render.gno | 8 +- 5 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 packages/r/intermarch3/goo/court_test.gno diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index 4c98dd6..1d99e92 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -61,7 +61,7 @@ func BalanceOfVoteToken(_ realm) int64 { } // VoteOnDispute allows a user to commit a vote during a dispute. -func VoteOnDispute(cur realm, id string, hash string) { +func VoteOnDispute(_ realm, id string, hash string) { dispute := getDispute(id) res, _ := Requests.Get(id) request := res.(DataRequest) @@ -87,7 +87,7 @@ func VoteOnDispute(cur realm, id string, hash string) { Revealed: false, } voter, exist := dispute.Voters.Get(string(vote.Voter)) - if exist && voter.(*Voter).HasVoted { + if exist && voter.(Voter).HasVoted { panic("error: Voter has already voted in this dispute.") } dispute.Votes = append(dispute.Votes, vote) @@ -97,7 +97,7 @@ func VoteOnDispute(cur realm, id string, hash string) { } // RevealVote allows a user to reveal their vote after the voting period has ended. -func RevealVote(cur realm, id string, value int64, salt string) { +func RevealVote(_ realm, id string, value int64, salt string) { dispute := getDispute(id) if dispute.IsResolved { panic("error: Dispute is resolved.") @@ -132,7 +132,7 @@ func RevealVote(cur realm, id string, value int64, salt string) { } // ResolveDispute finalizes a dispute after the reveal period, tallying votes and setting the winning value. -func ResolveDispute(cur realm, id string) { +func ResolveDispute(DEADLINE_TIMESTAMP realm, id string) { dispute := getDispute(id) if dispute.IsResolved { panic("error: Dispute is already resolved.") diff --git a/packages/r/intermarch3/goo/court_test.gno b/packages/r/intermarch3/goo/court_test.gno new file mode 100644 index 0000000..5277b0c --- /dev/null +++ b/packages/r/intermarch3/goo/court_test.gno @@ -0,0 +1,148 @@ +package goo + +import ( + "chain" + "testing" + "strconv" + "time" + + "gno.land/p/nt/urequire" + "gno.land/p/nt/testutils" +) + +var user4 = testutils.TestAddress("user4") + +func TestBuyInitialVoteToken(t *testing.T) { + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Must send exactly " + strconv.Itoa(int(VoteTokenPrice/1_000_000)) + " gnot to get a vote token.", func() { + BuyInitialVoteToken(cross) + }, "user should not be able to buy a vote token without sending the correct amount") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + urequire.NotPanics(t, func() { + BuyInitialVoteToken(cross) + }, "user should be able to buy a vote token by sending the correct amount") + amount := BalanceOfVoteToken(cross) + urequire.Equal(t, int64(1), amount, "user should have received a vote token") + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + urequire.AbortsWithMessage(t, "error: You already have a vote token.", func() { + BuyInitialVoteToken(cross) + }, "user should not be able to buy a second vote token") +} + +func TestVoteOnDispute(t *testing.T) { + // setup: create request and dispute + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + ProposeValue(cross, id, 0) + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + DisputeData(cross, id) + + // buy vote token + testing.SetRealm(testing.NewUserRealm(user3)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + BuyInitialVoteToken(cross) + + // vote on dispute + testing.SetRealm(testing.NewUserRealm(user3)) + urequire.NotPanics(t, func() { + VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") + }, "user should be able to vote on dispute") + + testing.SetRealm(testing.NewUserRealm(user4)) + urequire.AbortsWithMessage(t, "error: You need at least 1 vote token to vote.", func() { + VoteOnDispute(cross, id, "hash") + }, "user should not be able to vote on dispute without a vote token") + + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Proposer and Disputer cannot vote in this dispute.", func() { + VoteOnDispute(cross, id, "hash") + }, "user should not be able to vote on dispute if they are the proposer or disputer") + + testing.SetRealm(testing.NewUserRealm(user3)) + urequire.AbortsWithMessage(t, "error: Voter has already voted in this dispute.", func() { + VoteOnDispute(cross, id, "hash") + }, "user should not be able to vote on dispute if they have already voted") + + testing.SetRealm(testing.NewUserRealm(user4)) + setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + urequire.AbortsWithMessage(t, "error: Vote period has ended.", func() { + VoteOnDispute(cross, id, "hash") + }, "user should not be able to vote on dispute after the voting period has ended") +} + +func TestRevealVote(t *testing.T) { + // setup: create request and dispute + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + ProposeValue(cross, id, 0) + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + DisputeData(cross, id) + + // vote on dispute + testing.SetRealm(testing.NewUserRealm(user3)) + VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") + + testing.SetRealm(testing.NewUserRealm(user3)) + setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + urequire.AbortsWithMessage(t, "error: Hash does not match the revealed value and salt.", func() { + RevealVote(cross, id, 1, "mysalt") + }, "vote reveal with incorrect value and salt should fail") + + // reveal vote + testing.SetRealm(testing.NewUserRealm(user3)) + urequire.NotPanics(t, func() { + RevealVote(cross, id, 0, "test") + }, "user should be able to reveal their vote") + + testing.SetRealm(testing.NewUserRealm(user3)) + urequire.AbortsWithMessage(t, "error: Vote already revealed.", func() { + RevealVote(cross, id, 1, "mysalt") + }, "user should not be able to reveal their vote again") +} + +func TestResolveDispute(t *testing.T) { + // setup: create request and dispute + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) + + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + ProposeValue(cross, id, 0) + + testing.SetRealm(testing.NewUserRealm(user2)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + DisputeData(cross, id) + + // vote on dispute + testing.SetRealm(testing.NewUserRealm(user3)) + VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") + setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + RevealVote(cross, id, 0, "test") + + setTime(time.Now().Add(time.Duration(RevealDuration) * time.Second + time.Second)) + CreateGnotCoins(cross, (Bond * 2) + RequesterReward) + urequire.NotPanics(t, func() { + ResolveDispute(cross, id) + }, "user should be able to resolve dispute after the reveal period has ended") + + urequire.AbortsWithMessage(t, "error: Dispute is already resolved.", func() { + ResolveDispute(cross, id) + }, "user should not be able to resolve dispute before the reveal period has ended") +} \ No newline at end of file diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index 65dd54c..48023b8 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -26,7 +26,7 @@ var ( // 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(cur realm, ancillaryData string, yesNoQuestion bool, deadline int64) string { +func RequestData(_ realm, ancillaryData string, yesNoQuestion bool, deadline int64) string { if ancillaryData == "" { panic("error: Ancillary data cannot be empty.") } @@ -57,7 +57,7 @@ func RequestData(cur realm, ancillaryData string, yesNoQuestion bool, deadline i // ProposeValue allows a user to propose a value for a requested data point. // `Bond` value needs to be sent to the contract as a bond. -func ProposeValue(cur realm, id string, proposedValue int64) { +func ProposeValue(_ realm, id string, proposedValue int64) { request := getRequest(id) if request.State != "Requested" { panic("error: Request is not in 'Requested' state.") @@ -85,7 +85,7 @@ func ProposeValue(cur realm, id string, proposedValue int64) { // DisputeData allows a user to dispute a proposed value. // `Bond` value needs to be sent to the contract as a bond. -func DisputeData(cur realm, id string) string { +func DisputeData(_ realm, id string) string { request := getRequest(id) if request.Proposer == runtime.PreviousRealm().Address() { panic("error: Proposer cannot dispute their own proposal.") @@ -112,7 +112,7 @@ func DisputeData(cur realm, id string) string { } // ResolveRequest finalizes an undisputed request after the resolution period has passed. -func ResolveRequest(cur realm, id string) { +func ResolveRequest(_ realm, id string) { request := getRequest(id) if request.State == "Disputed" { panic("error: Request is in 'Disputed' state.") @@ -140,7 +140,7 @@ func ResolveRequest(cur realm, id string) { } // RequestResult returns the winning value of a resolved request. -func RequestResult(cur realm, id string) int64 { +func RequestResult(_ realm, id string) int64 { request := getRequest(id) if request.State != "Resolved" { panic("error: Request is not resolved.") @@ -149,16 +149,16 @@ func RequestResult(cur realm, id string) int64 { } // RequesterRetreiveFund allows the original requester to get their reward back if the deadline passed without a proposal. -func RequesterRetreiveFund(cur realm, id string) { +func RequesterRetreiveFund(_ realm, id string) { request := getRequest(id) if request.State != "Requested" { panic("error: cannot retreive fund as requests fulfilled.") } if request.Creator != runtime.PreviousRealm().Address() { - panic("error: Only the creator of the request can retreive the fund.") + panic("error: Only the creator of the request can retrieve the fund.") } if request.Deadline.After(time.Now()) { - panic("error: Cannot retreive fund before the deadline.") + panic("error: Cannot retrieve fund before the deadline.") } from := runtime.CurrentRealm().Address() diff --git a/packages/r/intermarch3/goo/oracle_test.gno b/packages/r/intermarch3/goo/oracle_test.gno index 1dfecba..85cf19a 100644 --- a/packages/r/intermarch3/goo/oracle_test.gno +++ b/packages/r/intermarch3/goo/oracle_test.gno @@ -3,6 +3,7 @@ package goo import ( "time" "chain" + "chain/runtime" "testing" "strconv" @@ -149,7 +150,6 @@ func TestResolveRequest(t *testing.T) { // propose a value testing.SetRealm(testing.NewUserRealm(user1)) - testing.IssueCoins(user1, chain.Coins{{Denom: "ugnot", Amount: Bond}}) testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: Bond}}) ProposeValue(cross, id, 0) @@ -159,9 +159,35 @@ func TestResolveRequest(t *testing.T) { }, "user should not be able to resolve a request if the resolution period has not ended yet") setTime(now.Add(time.Duration(ResolutionTime) * time.Second + time.Second)) + CreateGnotCoins(cross, Bond + RequesterReward) urequire.NotPanics(t, func() { ResolveRequest(cross, id) }, "user should be able to resolve a request") + + testing.SetRealm(testing.NewUserRealm(user1)) + urequire.AbortsWithMessage(t, "error: Request is already resolved.", func() { + ResolveRequest(cross, id) + }, "user should not be able to resolve a request if it is already resolved") +} + +func TestRequesterRetreiveFund(t *testing.T) { + // setup: create request + testing.SetRealm(testing.NewUserRealm(user1)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + duration := time.Now().Add(24*time.Hour) + id := RequestData(cross, "test", true, duration.Unix()) + + testing.SetRealm(testing.NewUserRealm(user2)) + urequire.AbortsWithMessage(t, "error: Only the creator of the request can retrieve the fund.", func() { + RequesterRetreiveFund(cross, id) + }, "only requester should be able to retrieve funds") + + testing.SetRealm(testing.NewUserRealm(user1)) + setTime(duration.Add(time.Hour)) + CreateGnotCoins(cross, RequesterReward) + urequire.NotPanics(t, func() { + RequesterRetreiveFund(cross, id) + }, "requester should be able to retrieve funds") } // Helper functions @@ -170,4 +196,8 @@ func setTime(newTime time.Time) { ctx := testing.GetContext() ctx.Time = newTime testing.SetContext(ctx) +} + +func CreateGnotCoins(_ realm, amount int64) { + testing.IssueCoins(runtime.CurrentRealm().Address(), chain.Coins{{Denom: "ugnot", Amount: amount}}) } \ No newline at end of file diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index 17cb629..888bcbc 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -74,10 +74,10 @@ func renderHome() string { // renderRequestPage renders the page for a specific request func renderRequestPage(id string) string { res, exists := Requests.Get(id) - req := res.(DataRequest) if !exists { return md.H1("error: No request with this ID exists.") } + req := res.(DataRequest) msg := md.H1("Question: " + req.AncillaryData) msg += md.H2("Details:\n\n") @@ -102,12 +102,12 @@ func renderRequestPage(id string) string { // if the request is resolved, show the winning value and the number of votes if req.State == "Resolved" { msg += md.H3("Winning Value: "+rendervalue(req, req.WinningValue)) + "\n\n" - res, exist := Disputes.Get(req.Id) - dispute := res.(Dispute) - if !exist { + res, exists = Disputes.Get(req.Id) + if !exists { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" return msg } + dispute := res.(Dispute) msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") msg += md.Paragraph("Reveal Votes: \n\n") if dispute.NbResolvedVotes > 0 { From 561610f8bcda830ee5b48295d930fa61d5074cfe Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sat, 25 Oct 2025 00:43:38 +0200 Subject: [PATCH 09/14] fix: format and more --- packages/r/intermarch3/goo/court.gno | 2 +- packages/r/intermarch3/goo/court_test.gno | 18 +++++++++--------- packages/r/intermarch3/goo/oracle_test.gno | 22 +++++++++++----------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index 1d99e92..e9abf7f 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -132,7 +132,7 @@ func RevealVote(_ realm, id string, value int64, salt string) { } // ResolveDispute finalizes a dispute after the reveal period, tallying votes and setting the winning value. -func ResolveDispute(DEADLINE_TIMESTAMP realm, id string) { +func ResolveDispute(_ realm, id string) { dispute := getDispute(id) if dispute.IsResolved { panic("error: Dispute is already resolved.") diff --git a/packages/r/intermarch3/goo/court_test.gno b/packages/r/intermarch3/goo/court_test.gno index 5277b0c..01bf9c4 100644 --- a/packages/r/intermarch3/goo/court_test.gno +++ b/packages/r/intermarch3/goo/court_test.gno @@ -2,19 +2,19 @@ package goo import ( "chain" - "testing" "strconv" + "testing" "time" - "gno.land/p/nt/urequire" "gno.land/p/nt/testutils" + "gno.land/p/nt/urequire" ) var user4 = testutils.TestAddress("user4") func TestBuyInitialVoteToken(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) - urequire.AbortsWithMessage(t, "error: Must send exactly " + strconv.Itoa(int(VoteTokenPrice/1_000_000)) + " gnot to get a vote token.", func() { + urequire.AbortsWithMessage(t, "error: Must send exactly "+strconv.Itoa(int(VoteTokenPrice/1_000_000))+" gnot to get a vote token.", func() { BuyInitialVoteToken(cross) }, "user should not be able to buy a vote token without sending the correct amount") @@ -74,7 +74,7 @@ func TestVoteOnDispute(t *testing.T) { }, "user should not be able to vote on dispute if they have already voted") testing.SetRealm(testing.NewUserRealm(user4)) - setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Vote period has ended.", func() { VoteOnDispute(cross, id, "hash") }, "user should not be able to vote on dispute after the voting period has ended") @@ -99,7 +99,7 @@ func TestRevealVote(t *testing.T) { VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") testing.SetRealm(testing.NewUserRealm(user3)) - setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Hash does not match the revealed value and salt.", func() { RevealVote(cross, id, 1, "mysalt") }, "vote reveal with incorrect value and salt should fail") @@ -133,11 +133,11 @@ func TestResolveDispute(t *testing.T) { // vote on dispute testing.SetRealm(testing.NewUserRealm(user3)) VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") - setTime(time.Now().Add(time.Duration(DisputeDuration) * time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) RevealVote(cross, id, 0, "test") - setTime(time.Now().Add(time.Duration(RevealDuration) * time.Second + time.Second)) - CreateGnotCoins(cross, (Bond * 2) + RequesterReward) + setTime(time.Now().Add(time.Duration(RevealDuration)*time.Second + time.Second)) + CreateGnotCoins(cross, (Bond*2)+RequesterReward) urequire.NotPanics(t, func() { ResolveDispute(cross, id) }, "user should be able to resolve dispute after the reveal period has ended") @@ -145,4 +145,4 @@ func TestResolveDispute(t *testing.T) { urequire.AbortsWithMessage(t, "error: Dispute is already resolved.", func() { ResolveDispute(cross, id) }, "user should not be able to resolve dispute before the reveal period has ended") -} \ No newline at end of file +} diff --git a/packages/r/intermarch3/goo/oracle_test.gno b/packages/r/intermarch3/goo/oracle_test.gno index 85cf19a..517042f 100644 --- a/packages/r/intermarch3/goo/oracle_test.gno +++ b/packages/r/intermarch3/goo/oracle_test.gno @@ -1,11 +1,11 @@ package goo import ( - "time" "chain" "chain/runtime" - "testing" "strconv" + "testing" + "time" "gno.land/p/nt/testutils" "gno.land/p/nt/urequire" @@ -19,7 +19,7 @@ var ( func TestRequestData(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) - urequire.AbortsWithMessage(t, "error: Incorrect reward amount sent. Required: " + strconv.FormatInt(RequesterReward, 10) + " ugnot.", func() { + urequire.AbortsWithMessage(t, "error: Incorrect reward amount sent. Required: "+strconv.FormatInt(RequesterReward, 10)+" ugnot.", func() { RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) }, "user should not be able to request data without sending the requester reward") @@ -62,13 +62,13 @@ func TestProposeValue(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) - urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot", func() { + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(Bond, 10)+" ugnot", func() { ProposeValue(cross, id, 0) }, "user should not be able to propose a value without sending the bond") testing.SetRealm(testing.NewUserRealm(user1)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) - setTime(time.Now().Add(25*time.Hour)) + setTime(time.Now().Add(25 * time.Hour)) urequire.AbortsWithMessage(t, "error: Deadline for proposal has passed.", func() { ProposeValue(cross, id, 0) }, "user should not be able to propose a value after the deadline") @@ -111,13 +111,13 @@ func TestDisputeData(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user2)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) - urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot", func() { + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(Bond, 10)+" ugnot", func() { DisputeData(cross, id) }, "user should not be able to dispute a value without sending the bond") testing.SetRealm(testing.NewUserRealm(user2)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) - setTime(time.Now().Add(time.Duration(ResolutionTime) * time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(ResolutionTime)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Dispute period has ended.", func() { DisputeData(cross, id) }, "user should not be able to dispute a value after the dispute period has ended") @@ -158,8 +158,8 @@ func TestResolveRequest(t *testing.T) { ResolveRequest(cross, id) }, "user should not be able to resolve a request if the resolution period has not ended yet") - setTime(now.Add(time.Duration(ResolutionTime) * time.Second + time.Second)) - CreateGnotCoins(cross, Bond + RequesterReward) + setTime(now.Add(time.Duration(ResolutionTime)*time.Second + time.Second)) + CreateGnotCoins(cross, Bond+RequesterReward) urequire.NotPanics(t, func() { ResolveRequest(cross, id) }, "user should be able to resolve a request") @@ -174,7 +174,7 @@ func TestRequesterRetreiveFund(t *testing.T) { // setup: create request testing.SetRealm(testing.NewUserRealm(user1)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) - duration := time.Now().Add(24*time.Hour) + duration := time.Now().Add(24 * time.Hour) id := RequestData(cross, "test", true, duration.Unix()) testing.SetRealm(testing.NewUserRealm(user2)) @@ -200,4 +200,4 @@ func setTime(newTime time.Time) { func CreateGnotCoins(_ realm, amount int64) { testing.IssueCoins(runtime.CurrentRealm().Address(), chain.Coins{{Denom: "ugnot", Amount: amount}}) -} \ No newline at end of file +} From 31fc552738c7cfb5e624e29ed3ae4ad5e293795e Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sat, 25 Oct 2025 11:15:33 +0200 Subject: [PATCH 10/14] fix: exposed vars security issue --- packages/r/intermarch3/goo/court.gno | 52 ++++++------- packages/r/intermarch3/goo/court_test.gno | 36 ++++----- packages/r/intermarch3/goo/oracle.gno | 90 +++++++++++----------- packages/r/intermarch3/goo/oracle_test.gno | 50 ++++++------ packages/r/intermarch3/goo/render.gno | 24 +++--- packages/r/intermarch3/goo/resolver.gno | 4 +- packages/r/intermarch3/goo/token.gno | 2 +- 7 files changed, 131 insertions(+), 127 deletions(-) diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index e9abf7f..648583c 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -13,15 +13,15 @@ import ( ) var ( - Disputes = avl.NewTree() - DisputeDuration = 2 * int64(time.Minute.Seconds()) - RevealDuration = 2 * int64(time.Minute.Seconds()) - VoteTokenPrice = 1 * int64(1_000_000) // in GNOT + disputes = avl.NewTree() + disputeDuration = 2 * int64(time.Minute.Seconds()) + revealDuration = 2 * int64(time.Minute.Seconds()) + voteTokenPrice = 1 * int64(1_000_000) // in GNOT VoteToken = newOOToken("Gno Optimistic Oracle Token", "goot", 6) ) func initiateDispute(id string) { - if _, exists := Disputes.Get(id); exists { + if _, exists := disputes.Get(id); exists { panic("error: Dispute for this request already exists.") } dispute := Dispute{ @@ -29,21 +29,21 @@ func initiateDispute(id string) { Votes: []Vote{}, Voters: avl.NewTree(), IsResolved: false, - EndTime: time.Now().Add(time.Duration(DisputeDuration) * time.Second), - EndRevealTime: time.Now().Add(time.Duration(DisputeDuration+RevealDuration) * time.Second), + EndTime: time.Now().Add(time.Duration(disputeDuration) * time.Second), + EndRevealTime: time.Now().Add(time.Duration(disputeDuration+revealDuration) * time.Second), } - Disputes.Set(id, dispute) + disputes.Set(id, dispute) chain.Emit("DisputeInitiated", "id", id) } // -- PUBLIC FUNCTIONS -- -// BuyInitialVoteToken allows a user to buy their first vote token by sending VoteTokenPrice amount of ugnot. +// BuyInitialVoteToken allows a user to buy their first vote token by sending voteTokenPrice amount of ugnot. func BuyInitialVoteToken(_ realm) { 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.") + 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.") } balance := VoteToken.BalanceOf(caller) @@ -51,19 +51,19 @@ func BuyInitialVoteToken(_ realm) { panic("error: You already have a vote token.") } - VoteToken.Mint(caller, 1) + VoteToken.mint(caller, 1) chain.Emit("VoteTokenPurchased", "voter", caller.String()) } // BalanceOfVoteToken returns the number of vote tokens held by the caller. func BalanceOfVoteToken(_ realm) int64 { - return VoteToken.balanceOf(runtime.PreviousRealm().Address()) + return VoteToken.BalanceOf(runtime.PreviousRealm().Address()) } // VoteOnDispute allows a user to commit a vote during a dispute. func VoteOnDispute(_ realm, id string, hash string) { dispute := getDispute(id) - res, _ := Requests.Get(id) + res, _ := requests.Get(id) request := res.(DataRequest) if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() { panic("error: Proposer and Disputer cannot vote in this dispute.") @@ -92,7 +92,7 @@ func VoteOnDispute(_ realm, id string, hash string) { } dispute.Votes = append(dispute.Votes, vote) dispute.Voters.Set(string(vote.Voter), Voter{HasVoted: true, VoteIndex: int64(len(dispute.Votes) - 1)}) - Disputes.Set(id, dispute) + disputes.Set(id, dispute) chain.Emit("VoteSubmitted", "id", id, "voter", vote.Voter.String()) } @@ -127,7 +127,7 @@ func RevealVote(_ realm, id string, value int64, salt string) { vote.Revealed = true dispute.NbResolvedVotes += 1 dispute.Votes[voter.(Voter).VoteIndex] = vote - Disputes.Set(id, dispute) + disputes.Set(id, dispute) chain.Emit("VoteRevealed", "id", id, "voter", vote.Voter.String(), "value", strconv.Itoa(int(value))) } @@ -143,13 +143,13 @@ func ResolveDispute(_ realm, id string) { val := resolve(id) dispute.WinningValue = val dispute.IsResolved = true - Disputes.Set(id, dispute) + disputes.Set(id, dispute) // Update the original request with the winning value request := getRequest(id) request.ProposedValue = val request.State = "Resolved" - Requests.Set(id, request) + requests.Set(id, request) chain.Emit("DisputeResolved", "id", id, "winningValue", strconv.Itoa(int(val))) chain.Emit("RequestResolved", "id", id, "winningValue", strconv.Itoa(int(val))) @@ -161,7 +161,7 @@ func ResolveDispute(_ realm, id string) { // Refund + reward the proposer if the dispute did not change the value winner = request.Proposer } - Bank.SendCoins(runtime.CurrentRealm().Address(), winner, chain.Coins{chain.Coin{Denom: "ugnot", Amount: Bond + RequesterReward}}) + bank.SendCoins(runtime.CurrentRealm().Address(), winner, chain.Coins{chain.Coin{Denom: "ugnot", Amount: bond + requesterReward}}) } // -- admin functions -- @@ -169,7 +169,7 @@ func ResolveDispute(_ realm, id string) { // SetDisputeDuration sets the duration (in seconds) for the voting period. func SetDisputeDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { - DisputeDuration = duration * int64(time.Second) + disputeDuration = duration * int64(time.Second) } else { panic("error: Only admin can set dispute duration.") } @@ -178,7 +178,7 @@ func SetDisputeDuration(_ realm, duration int64) { // SetRevealDuration sets the duration (in seconds) for the reveal period. func SetRevealDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { - RevealDuration = duration * int64(time.Second) + revealDuration = duration * int64(time.Second) } else { panic("error: Only admin can set reveal duration.") } @@ -187,7 +187,7 @@ func SetRevealDuration(_ realm, duration int64) { // SetVoteTokenPrice sets the price (in ugnot) to cast a vote. func SetVoteTokenPrice(_ realm, price int64) { if runtime.OriginCaller() == admin { - VoteTokenPrice = price + voteTokenPrice = price } else { panic("error: Only admin can set vote price.") } @@ -202,12 +202,12 @@ func GetDispute(_ realm, id string) Dispute { // GetDisputeDuration returns the current dispute duration. func GetDisputeDuration(_ realm) int64 { - return DisputeDuration + return disputeDuration } // GetVoteTokenPrice returns the current vote price. func GetVoteTokenPrice(_ realm) int64 { - return VoteTokenPrice + return voteTokenPrice } // GetDisputeEndTime returns the end time of the voting period for a specific dispute. @@ -230,13 +230,13 @@ func GetRevealEndTime(_ realm, id string) time.Time { // GetRevealDuration returns the current reveal duration. func GetRevealDuration(_ realm) int64 { - return RevealDuration + return revealDuration } // Utils functions func getDispute(id string) Dispute { - dispute, exists := Disputes.Get(id) + dispute, exists := disputes.Get(id) if !exists { panic("error: Dispute with this ID does not exist.") } diff --git a/packages/r/intermarch3/goo/court_test.gno b/packages/r/intermarch3/goo/court_test.gno index 01bf9c4..dcf4d83 100644 --- a/packages/r/intermarch3/goo/court_test.gno +++ b/packages/r/intermarch3/goo/court_test.gno @@ -14,12 +14,12 @@ var user4 = testutils.TestAddress("user4") func TestBuyInitialVoteToken(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) - urequire.AbortsWithMessage(t, "error: Must send exactly "+strconv.Itoa(int(VoteTokenPrice/1_000_000))+" gnot to get a vote token.", func() { + urequire.AbortsWithMessage(t, "error: Must send exactly "+strconv.Itoa(int(voteTokenPrice/1_000_000))+" gnot to get a vote token.", func() { BuyInitialVoteToken(cross) }, "user should not be able to buy a vote token without sending the correct amount") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: voteTokenPrice}}) urequire.NotPanics(t, func() { BuyInitialVoteToken(cross) }, "user should be able to buy a vote token by sending the correct amount") @@ -27,7 +27,7 @@ func TestBuyInitialVoteToken(t *testing.T) { urequire.Equal(t, int64(1), amount, "user should have received a vote token") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: voteTokenPrice}}) urequire.AbortsWithMessage(t, "error: You already have a vote token.", func() { BuyInitialVoteToken(cross) }, "user should not be able to buy a second vote token") @@ -36,20 +36,20 @@ func TestBuyInitialVoteToken(t *testing.T) { func TestVoteOnDispute(t *testing.T) { // setup: create request and dispute testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) ProposeValue(cross, id, 0) testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) DisputeData(cross, id) // buy vote token testing.SetRealm(testing.NewUserRealm(user3)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: VoteTokenPrice}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: voteTokenPrice}}) BuyInitialVoteToken(cross) // vote on dispute @@ -74,7 +74,7 @@ func TestVoteOnDispute(t *testing.T) { }, "user should not be able to vote on dispute if they have already voted") testing.SetRealm(testing.NewUserRealm(user4)) - setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(disputeDuration)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Vote period has ended.", func() { VoteOnDispute(cross, id, "hash") }, "user should not be able to vote on dispute after the voting period has ended") @@ -83,15 +83,15 @@ func TestVoteOnDispute(t *testing.T) { func TestRevealVote(t *testing.T) { // setup: create request and dispute testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) ProposeValue(cross, id, 0) testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) DisputeData(cross, id) // vote on dispute @@ -99,7 +99,7 @@ func TestRevealVote(t *testing.T) { VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") testing.SetRealm(testing.NewUserRealm(user3)) - setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(disputeDuration)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Hash does not match the revealed value and salt.", func() { RevealVote(cross, id, 1, "mysalt") }, "vote reveal with incorrect value and salt should fail") @@ -119,25 +119,25 @@ func TestRevealVote(t *testing.T) { func TestResolveDispute(t *testing.T) { // setup: create request and dispute testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) ProposeValue(cross, id, 0) testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) DisputeData(cross, id) // vote on dispute testing.SetRealm(testing.NewUserRealm(user3)) VoteOnDispute(cross, id, "a96e0beb59a16b085a7d2b3b5ffd6e5971870aa2903c6df86f26fa908ded2e21") - setTime(time.Now().Add(time.Duration(DisputeDuration)*time.Second + time.Second)) + setTime(time.Now().Add(time.Duration(disputeDuration)*time.Second + time.Second)) RevealVote(cross, id, 0, "test") - setTime(time.Now().Add(time.Duration(RevealDuration)*time.Second + time.Second)) - CreateGnotCoins(cross, (Bond*2)+RequesterReward) + setTime(time.Now().Add(time.Duration(revealDuration)*time.Second + time.Second)) + CreateGnotCoins(cross, (bond*2)+requesterReward) urequire.NotPanics(t, func() { ResolveDispute(cross, id) }, "user should be able to resolve dispute after the reveal period has ended") diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index 48023b8..d6ba9aa 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -12,19 +12,23 @@ import ( ) var ( - admin = address("g1qdwemgqgqm7s42gy6xcj7f22uhfdzr82st3sy3") - ResolutionTime = 2 * int64(time.Minute.Seconds()) - RequesterReward = 1 * int64(1_000_000) // in GNOT - Bond = 2 * int64(1_000_000) // in GNOT - Requests = avl.NewTree() - Bank = banker.NewBanker(banker.BankerTypeRealmSend) + admin address + resolutionTime = 2 * int64(time.Minute.Seconds()) + requesterReward = 1 * int64(1_000_000) // in GNOT + bond = 2 * int64(1_000_000) // in GNOT + requests = avl.NewTree() + bank = banker.NewBanker(banker.BankerTypeRealmSend) idGenerator seqid.ID ) +func init() { + admin = runtime.CurrentRealm().Address() +} + // -- PUBLIC FUNCTIONS -- // RequestData allows a user to request data from the oracle. -// `RequesterReward` value needs to be sent to the contract as a reward +// `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 { if ancillaryData == "" { @@ -35,8 +39,8 @@ func RequestData(_ realm, ancillaryData string, yesNoQuestion bool, deadline int } coins := banker.OriginSend() - if len(coins) != 1 || coins.AmountOf("ugnot") != RequesterReward { - panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(RequesterReward, 10) + " ugnot.") + if len(coins) != 1 || coins.AmountOf("ugnot") != requesterReward { + panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(requesterReward, 10) + " ugnot.") } id := idGenerator.Next().String() @@ -50,13 +54,13 @@ func RequestData(_ realm, ancillaryData string, yesNoQuestion bool, deadline int Deadline: time.Unix(deadline, 0), Creator: runtime.PreviousRealm().Address(), } - Requests.Set(id, request) + requests.Set(id, request) chain.Emit("DataRequested", "id", id, "timestamp", request.Timestamp.String(), "ancillaryData", ancillaryData) return id } // ProposeValue allows a user to propose a value for a requested data point. -// `Bond` value needs to be sent to the contract as a bond. +// `bond` value needs to be sent to the contract as a bond. func ProposeValue(_ realm, id string, proposedValue int64) { request := getRequest(id) if request.State != "Requested" { @@ -70,21 +74,21 @@ func ProposeValue(_ realm, id string, proposedValue int64) { } coins := banker.OriginSend() - if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { - panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") + if len(coins) != 1 || coins.AmountOf("ugnot") != bond { + panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(bond, 10) + " ugnot") } request.ProposedValue = proposedValue request.Proposer = runtime.PreviousRealm().Address() - request.ProposerBond = Bond - request.ResolutionTime = time.Now().Add(time.Duration(ResolutionTime) * time.Second) + request.ProposerBond = bond + request.ResolutionTime = time.Now().Add(time.Duration(resolutionTime) * time.Second) request.State = "Proposed" - Requests.Set(id, request) + requests.Set(id, request) chain.Emit("ValueProposed", "id", id, "proposedValue", strconv.Itoa(int(proposedValue)), "proposer", request.Proposer.String(), "resolutionTime", request.ResolutionTime.String()) } // DisputeData allows a user to dispute a proposed value. -// `Bond` value needs to be sent to the contract as a bond. +// `bond` value needs to be sent to the contract as a bond. func DisputeData(_ realm, id string) string { request := getRequest(id) if request.Proposer == runtime.PreviousRealm().Address() { @@ -98,14 +102,14 @@ func DisputeData(_ realm, id string) string { } coins := banker.OriginSend() - if len(coins) != 1 || coins.AmountOf("ugnot") != Bond { - panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(Bond, 10) + " ugnot") + if len(coins) != 1 || coins.AmountOf("ugnot") != bond { + panic("error: Incorrect bond amount sent. Required: " + strconv.FormatInt(bond, 10) + " ugnot") } request.Disputer = runtime.PreviousRealm().Address() - request.DisputerBond = Bond + request.DisputerBond = bond request.State = "Disputed" - Requests.Set(id, request) + requests.Set(id, request) initiateDispute(id) chain.Emit("DataDisputed", "id", id, "disputer", request.Disputer.String()) return "Time: " + time.Now().String() @@ -128,13 +132,13 @@ func ResolveRequest(_ realm, id string) { } request.State = "Resolved" request.WinningValue = request.ProposedValue - Requests.Set(id, request) + requests.Set(id, request) from := runtime.CurrentRealm().Address() to := request.Proposer - totalPayout := request.ProposerBond + RequesterReward + totalPayout := request.ProposerBond + requesterReward payout := chain.Coins{chain.Coin{Denom: "ugnot", Amount: totalPayout}} - Bank.SendCoins(from, to, payout) + bank.SendCoins(from, to, payout) chain.Emit("RequestResolved", "id", id, "winningValue", strconv.Itoa(int(request.WinningValue))) } @@ -163,11 +167,11 @@ func RequesterRetreiveFund(_ realm, id string) { from := runtime.CurrentRealm().Address() to := request.Creator - refund := chain.Coins{chain.Coin{Denom: "ugnot", Amount: RequesterReward}} - Bank.SendCoins(from, to, refund) + refund := chain.Coins{chain.Coin{Denom: "ugnot", Amount: requesterReward}} + bank.SendCoins(from, to, refund) request.State = "Expired" - Requests.Set(id, request) + requests.Set(id, request) chain.Emit("RequestExpired", "id", id) } @@ -176,28 +180,28 @@ func RequesterRetreiveFund(_ realm, id string) { // SetResolutionDuration sets the duration (in seconds) for the resolution period. func SetResolutionDuration(_ realm, duration int64) { if runtime.OriginCaller() == admin { - ResolutionTime = duration - chain.Emit("ResolutionTimeSet", "duration", strconv.Itoa(int(duration))) + resolutionTime = duration + chain.Emit("resolutionTimeSet", "duration", strconv.Itoa(int(duration))) } else { panic("error: Only the admin can set the resolution time.") } } -// SetRequesterReward sets the reward amount for a successful proposal. -func SetRequesterReward(_ realm, reward int64) { +// SetrequesterReward sets the reward amount for a successful proposal. +func SetrequesterReward(_ realm, reward int64) { if runtime.OriginCaller() == admin { - RequesterReward = reward - chain.Emit("RequesterRewardSet", "reward", strconv.Itoa(int(reward))) + requesterReward = reward + chain.Emit("requesterRewardSet", "reward", strconv.Itoa(int(reward))) } else { panic("error: Only the admin can set the requester reward.") } } - + // SetBond sets the bond amount required for proposals and disputes. -func SetBond(_ realm, bond int64) { +func SetBond(_ realm, newBond int64) { if runtime.OriginCaller() == admin { - Bond = bond - chain.Emit("BondSet", "bond", strconv.Itoa(int(bond))) + bond = newBond + chain.Emit("bondSet", "bond", strconv.Itoa(int(bond))) } else { panic("error: Only the admin can set the proposer bond.") } @@ -222,21 +226,21 @@ func GetRequest(_ realm, id string) DataRequest { // GetBond returns the current bond amount. func GetBond(_ realm) int64 { - return Bond + return bond } // GetResolutionTime returns the current resolution time duration. func GetResolutionTime(_ realm) int64 { - return ResolutionTime + return resolutionTime } // GetRequesterReward returns the current requester reward amount. func GetRequesterReward(_ realm) int64 { - return RequesterReward + return requesterReward } -// GetRequestState returns the current state of a specific data request. -func GetRequestState(_ realm, id string) string { +// GetRequeststate returns the current state of a specific data request. +func GetRequeststate(_ realm, id string) string { request := getRequest(id) return request.State } @@ -249,7 +253,7 @@ func GetAdmin(_ realm) address { // Utils functions func getRequest(id string) DataRequest { - request, exists := Requests.Get(id) + request, exists := requests.Get(id) if !exists { panic("error: Request with this ID does not exist.") } diff --git a/packages/r/intermarch3/goo/oracle_test.gno b/packages/r/intermarch3/goo/oracle_test.gno index 517042f..d79cf6b 100644 --- a/packages/r/intermarch3/goo/oracle_test.gno +++ b/packages/r/intermarch3/goo/oracle_test.gno @@ -19,24 +19,24 @@ var ( func TestRequestData(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) - urequire.AbortsWithMessage(t, "error: Incorrect reward amount sent. Required: "+strconv.FormatInt(RequesterReward, 10)+" ugnot.", func() { + urequire.AbortsWithMessage(t, "error: Incorrect reward amount sent. Required: "+strconv.FormatInt(requesterReward, 10)+" ugnot.", func() { RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) }, "user should not be able to request data without sending the requester reward") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) urequire.AbortsWithMessage(t, "error: Ancillary data cannot be empty.", func() { RequestData(cross, "", true, time.Now().Add(24*time.Hour).Unix()) }, "user should not be able to request data with an empty ancillary data") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) urequire.AbortsWithMessage(t, "error: Deadline must be at least 24 hours in the future.", func() { RequestData(cross, "test", true, time.Now().Unix()) }, "user should not be able to request data with a deadline less than 24 hours in the future") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) urequire.NotPanics(t, func() { RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) }, "user should be able to request data") @@ -51,23 +51,23 @@ func TestProposeValue(t *testing.T) { // create a request now := time.Now() testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.AbortsWithMessage(t, "error: Proposed value must be 0 or 1 for yes/no questions.", func() { ProposeValue(cross, id, 9) }, "user should not be able to propose a value other than 0 or 1 for a yes/no question") testing.SetRealm(testing.NewUserRealm(user1)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) - urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(Bond, 10)+" ugnot", func() { + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(bond, 10)+" ugnot", func() { ProposeValue(cross, id, 0) }, "user should not be able to propose a value without sending the bond") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) setTime(time.Now().Add(25 * time.Hour)) urequire.AbortsWithMessage(t, "error: Deadline for proposal has passed.", func() { ProposeValue(cross, id, 0) @@ -75,13 +75,13 @@ func TestProposeValue(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) setTime(now.Add(time.Hour)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.NotPanics(t, func() { ProposeValue(cross, id, 0) }, "user should be able to propose a value") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.AbortsWithMessage(t, "error: Request is not in 'Requested' state.", func() { ProposeValue(cross, id, 0) }, "user should not be able to propose a value if the request is not in 'Requested' state") @@ -90,47 +90,47 @@ func TestProposeValue(t *testing.T) { func TestDisputeData(t *testing.T) { // setup: create request and propose a value testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) now := time.Now() id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) ProposeValue(cross, id, 0) testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.AbortsWithMessage(t, "error: Request with this ID does not exist.", func() { DisputeData(cross, "test") }, "request id does not exist but didn't revert") testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.AbortsWithMessage(t, "error: Proposer cannot dispute their own proposal.", func() { DisputeData(cross, id) }, "proposer cannot dispute their own proposal") testing.SetRealm(testing.NewUserRealm(user2)) testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: 0}}) - urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(Bond, 10)+" ugnot", func() { + urequire.AbortsWithMessage(t, "error: Incorrect bond amount sent. Required: "+strconv.FormatInt(bond, 10)+" ugnot", func() { DisputeData(cross, id) }, "user should not be able to dispute a value without sending the bond") testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) - setTime(time.Now().Add(time.Duration(ResolutionTime)*time.Second + time.Second)) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) + setTime(time.Now().Add(time.Duration(resolutionTime)*time.Second + time.Second)) urequire.AbortsWithMessage(t, "error: Dispute period has ended.", func() { DisputeData(cross, id) }, "user should not be able to dispute a value after the dispute period has ended") testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) setTime(now.Add(time.Second)) urequire.NotPanics(t, func() { DisputeData(cross, id) }, "user should be able to dispute a value") testing.SetRealm(testing.NewUserRealm(user2)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: bond}}) urequire.AbortsWithMessage(t, "error: Request is not in 'Proposed' state.", func() { DisputeData(cross, id) }, "user should not be able to dispute a value if the request is not in 'Proposed' state") @@ -139,7 +139,7 @@ func TestDisputeData(t *testing.T) { func TestResolveRequest(t *testing.T) { // setup: create request testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) now := time.Now() id := RequestData(cross, "test", true, time.Now().Add(24*time.Hour).Unix()) @@ -150,7 +150,7 @@ func TestResolveRequest(t *testing.T) { // propose a value testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: Bond}}) + testing.SetOriginSend(chain.Coins{{Denom: "ugnot", Amount: bond}}) ProposeValue(cross, id, 0) testing.SetRealm(testing.NewUserRealm(user1)) @@ -158,8 +158,8 @@ func TestResolveRequest(t *testing.T) { ResolveRequest(cross, id) }, "user should not be able to resolve a request if the resolution period has not ended yet") - setTime(now.Add(time.Duration(ResolutionTime)*time.Second + time.Second)) - CreateGnotCoins(cross, Bond+RequesterReward) + setTime(now.Add(time.Duration(resolutionTime)*time.Second + time.Second)) + CreateGnotCoins(cross, bond+requesterReward) urequire.NotPanics(t, func() { ResolveRequest(cross, id) }, "user should be able to resolve a request") @@ -173,7 +173,7 @@ func TestResolveRequest(t *testing.T) { func TestRequesterRetreiveFund(t *testing.T) { // setup: create request testing.SetRealm(testing.NewUserRealm(user1)) - testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: RequesterReward}}) + testing.SetOriginSend([]chain.Coin{{Denom: "ugnot", Amount: requesterReward}}) duration := time.Now().Add(24 * time.Hour) id := RequestData(cross, "test", true, duration.Unix()) @@ -184,7 +184,7 @@ func TestRequesterRetreiveFund(t *testing.T) { testing.SetRealm(testing.NewUserRealm(user1)) setTime(duration.Add(time.Hour)) - CreateGnotCoins(cross, RequesterReward) + CreateGnotCoins(cross, requesterReward) urequire.NotPanics(t, func() { RequesterRetreiveFund(cross, id) }, "requester should be able to retrieve funds") diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index 888bcbc..36d5058 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -25,13 +25,13 @@ func renderHome() string { msg := md.H1("GOO Home") msg += md.Paragraph("Welcome to the first Optimistic Oracle on GnoLand! This project is developed by " + md.Link("@intermarch3", "/r/intermarch3/home")) msg += md.Paragraph("For more information, visit the " + md.Link("Readme", runtime.CurrentRealm().PkgPath()[8:]+"$source") + ".") - msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(RequesterReward))+"ugnot").URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(RequesterReward/1_000_000)) + " GNOT to the proposer).\n\n" - msg += md.H2("Current Requests :") + msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(requesterReward))+"ugnot").URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(requesterReward/1_000_000)) + " GNOT to the proposer).\n\n" + msg += md.H2("Current requests :") // List current requests and build table var table []string table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) - Requests.Iterate("", "", func(key string, value any) bool { + requests.Iterate("", "", func(key string, value any) bool { if value.(DataRequest).State == "Resolved" { return false } @@ -48,11 +48,11 @@ func renderHome() string { msg += md.HorizontalRule() // List last 5 resolved requests and build table - msg += md.H2("Last 5 Resolved Requests :") + msg += md.H2("Last 5 Resolved requests :") var resolved []string resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) - Requests.Iterate("", "", func(key string, value any) bool { + requests.Iterate("", "", func(key string, value any) bool { if value.(DataRequest).State != "Resolved" { return false } @@ -73,7 +73,7 @@ func renderHome() string { // renderRequestPage renders the page for a specific request func renderRequestPage(id string) string { - res, exists := Requests.Get(id) + res, exists := requests.Get(id) if !exists { return md.H1("error: No request with this ID exists.") } @@ -94,7 +94,7 @@ func renderRequestPage(id string) string { msg += md.H3("State: "+req.State) + "\n\n" if req.State == "Requested" { msg += md.Paragraph("This request has not been proposed yet.") + "\n\n" - msg += md.H3("The reward for the proposer is "+strconv.Itoa(int(RequesterReward/1_000_000))+" GNOT.") + "\n\n" + msg += md.H3("The reward for the proposer is "+strconv.Itoa(int(requesterReward/1_000_000))+" GNOT.") + "\n\n" msg += renderAction(req) return msg } @@ -102,7 +102,7 @@ func renderRequestPage(id string) string { // if the request is resolved, show the winning value and the number of votes if req.State == "Resolved" { msg += md.H3("Winning Value: "+rendervalue(req, req.WinningValue)) + "\n\n" - res, exists = Disputes.Get(req.Id) + res, exists = disputes.Get(req.Id) if !exists { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" return msg @@ -127,7 +127,7 @@ func renderRequestPage(id string) string { // if the request is disputed, show the dispute details if req.State == "Disputed" { - res, exist := Disputes.Get(req.Id) + res, exist := disputes.Get(req.Id) dispute := res.(Dispute) if exist { msg += md.H2("Dispute Details:\n\n") @@ -192,13 +192,13 @@ func renderRequest(req DataRequest) []string { // return appropriate action link based on the state of the request func renderAction(req DataRequest) string { action := md.H2("Actions:\n\n") - dispute, _ := Disputes.Get(req.Id) + dispute, _ := disputes.Get(req.Id) if req.State == "Requested" { // tx to propose a value - action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeValue").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(Bond))+"ugnot").URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeValue").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(bond))+"ugnot").URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(bond/1_000_000)) + " GNOT)" } else if req.State == "Proposed" { // tx to dispute a value - action += md.Link("Dispute value", ufmt.Sprintf("%s", txlink.NewLink("DisputeData").AddArgs("id", req.Id).SetSend(strconv.Itoa(int(Bond))+"ugnot").URL())) + "\n\n(Note: Disputing need to bond " + strconv.Itoa(int(Bond/1_000_000)) + " GNOT)" + action += md.Link("Dispute value", ufmt.Sprintf("%s", txlink.NewLink("DisputeData").AddArgs("id", req.Id).SetSend(strconv.Itoa(int(bond))+"ugnot").URL())) + "\n\n(Note: Disputing need to bond " + strconv.Itoa(int(bond/1_000_000)) + " GNOT)" } else if req.State == "Disputed" { if req.ResolutionTime.Before(time.Now()) { // tx to reveal a vote diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno index 505b511..37d46bd 100644 --- a/packages/r/intermarch3/goo/resolver.gno +++ b/packages/r/intermarch3/goo/resolver.gno @@ -75,9 +75,9 @@ func rewardAndSlachVoters(dispute Dispute, winningValue, totalWeight int64) { if vote.Value == winningValue { // Reward winning voters VoteToken.mint(vote.Voter, 2) - reward := Bond * (vote.TokenAmount / totalWeight) + reward := bond * (vote.TokenAmount / totalWeight) if reward != 0 { - Bank.SendCoins(runtime.CurrentRealm().Address(), vote.Voter, chain.Coins{chain.Coin{Denom: "ugnot", Amount: reward}}) + bank.SendCoins(runtime.CurrentRealm().Address(), vote.Voter, chain.Coins{chain.Coin{Denom: "ugnot", Amount: reward}}) } chain.Emit("VoterRewarded", "voter", vote.Voter.String()) } else { diff --git a/packages/r/intermarch3/goo/token.gno b/packages/r/intermarch3/goo/token.gno index 3a72296..51431fa 100644 --- a/packages/r/intermarch3/goo/token.gno +++ b/packages/r/intermarch3/goo/token.gno @@ -22,6 +22,6 @@ func (t *OOT) burn(from address, amount int64) { t.PrivateLedger.Burn(from, amount) } -func (t *OOT) balanceOf(addr address) int64 { +func (t *OOT) BalanceOf(addr address) int64 { return t.Token.BalanceOf(addr) } From 3b197cb7d6e854eed7c55c63a74b643e2d9f0505 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Sat, 25 Oct 2025 11:19:06 +0200 Subject: [PATCH 11/14] fix: format --- packages/r/intermarch3/goo/oracle.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index d6ba9aa..ec429dc 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -196,7 +196,7 @@ func SetrequesterReward(_ realm, reward int64) { panic("error: Only the admin can set the requester reward.") } } - + // SetBond sets the bond amount required for proposals and disputes. func SetBond(_ realm, newBond int64) { if runtime.OriginCaller() == admin { From 3107cbe7d69290fd44bf0f5eb7aa42677b0c3e59 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 25 Nov 2025 15:25:59 +0100 Subject: [PATCH 12/14] fix: review comments --- packages/r/intermarch3/goo/README.md | 13 +++-- packages/r/intermarch3/goo/court.gno | 36 +++++++----- packages/r/intermarch3/goo/court_test.gno | 2 +- packages/r/intermarch3/goo/oracle.gno | 27 ++++++--- packages/r/intermarch3/goo/render.gno | 9 ++- packages/r/intermarch3/goo/resolver.gno | 71 ++++++++++++++--------- 6 files changed, 103 insertions(+), 55 deletions(-) diff --git a/packages/r/intermarch3/goo/README.md b/packages/r/intermarch3/goo/README.md index c160759..eef7a66 100644 --- a/packages/r/intermarch3/goo/README.md +++ b/packages/r/intermarch3/goo/README.md @@ -17,7 +17,8 @@ This implementation is inspired by the [UMA Optimistic Oracle](https://uma.xyz/) The Gno Optimistic Oracle operates on the principle that data proposed to the oracle is assumed to be true. A bond is required for any new proposition. This proposition enters a "liveness" period where anyone can dispute it by posting an equal bond. - **Happy Path**: If no one disputes the data within the liveness period, it is considered resolved and accepted as truth. The proposer's bond is returned along with a reward. -- **Unhappy Path (Dispute)**: If the data is disputed, Oracle Token Holders can vote on the correct outcome. This is handled by the `court.gno` contract. Token holders vote, and the outcome is decided by the total token weight backing each value. The winner's bond is returned, and they receive a portion of the loser's slashed bond. +- **Unhappy Path (Dispute)**: If the data is disputed, OOT (Optimistic Oracle Token) Holders can vote on the correct outcome. This is handled by the `court.gno` contract. Token holders vote, and the outcome is decided by the total token weight backing each value. The winner's bond is returned, and they receive a portion of the loser's slashed bond. +See [Tokenomics](#tokenomics) to understand how to get an OOT. ## How It Works: The Lifecycle of a Data Request @@ -49,7 +50,7 @@ A **Disputer** can challenge the Proposer's value. - This action pauses the request's resolution and initiates a formal dispute, handled by the `court.gno` contract. ### 5. Voting (`VoteOnDispute`) -The dispute is now open for voting by all Oracle Soulbound Token holders. The system uses a **commit-reveal scheme** to prevent vote-copying. +The dispute is now open for voting by all OOT holders. The system uses a **commit-reveal scheme** to prevent vote-copying. - **Commit Phase**: During the `DisputeDuration`, voters submit a hash of their vote (`SHA256(value + salt)`) by calling `VoteOnDispute`. They must also pay a small `VotePrice` fee. - **Reveal Phase**: After the commit phase ends, the `RevealDuration` begins. Voters must call `RevealVote`, submitting their original `value` and `salt`. The contract verifies that the hash matches the one submitted during the commit phase. @@ -62,7 +63,7 @@ Once the reveal period is over, anyone can call `ResolveDispute`. ## Architecture -The oracle contract is composed of three main files : +The oracle contract is composed of three main files: - `oracle.gno`: Manages the data request lifecycle (request, propose, dispute, resolve). It is the main entry point for users. - `court.gno`: Handles the entire dispute resolution process, including the commit-reveal voting scheme. @@ -77,16 +78,16 @@ The oracle contract is composed of three main files : ## Tokenomics -The Oracle Soulbound Token (OOT) is a non-transferable token that represents voting power in the oracle system. +The token (OOT) is a non-transferable token that represents voting power in the oracle system. - **Acquisition**: Users can acquire OOT by calling `BuyInitialVoteToken` and paying a fee in GNOT. This action mints one OOT to the caller's address (can only buy 1 initial token). -Vote token holders can participate in disputes and earn rewards by voting correctly. By voting correctly, they can earn a portion of the slashed bonds from losing parties and gain 2 Vote tokens, incentivizing accurate and honest participation in the oracle system and increasing their voting power. +OOT holders can participate in disputes and earn rewards by voting correctly. By voting correctly, they can earn a portion of the slashed bonds from losing parties and gain 2 Vote tokens, incentivizing accurate and honest participation in the oracle system and increasing their voting power. - **GNOT Usage**: The reward and the Bond (in GNOT Token) need to be less than the bond to avoid trying to game the system by creating disputes just to earn tokens, as they would lose more from the bond than the reward. This design is not final and can be adjusted based on community feedback and economic analysis. ## Usage Example Here is a full workflow using `gnokey`. -**0. Buy Vote Token for Voter Role** +**0. Buy OOT for Voter Role (one time action)** ```bash # Buy Oracle Soulbound Token (replace with your key name) gnokey maketx call -pkgpath "gno.land/r/intermarch3/goo" -func "BuyInitialVoteToken" -gas-fee 1000000ugnot -gas-wanted 10000000 -send "1000000ugnot" -broadcast -chainid "dev" -remote "tcp://127.0.0.1:26657" diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index 648583c..cfb2adf 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -10,6 +10,7 @@ import ( "time" "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" ) var ( @@ -43,7 +44,7 @@ func BuyInitialVoteToken(_ realm) { 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.") + panic(ufmt.Sprintf("error: Must send exactly %d gnot to get a vote token.", voteTokenPrice/1_000_000)) } balance := VoteToken.BalanceOf(caller) @@ -55,25 +56,24 @@ func BuyInitialVoteToken(_ realm) { chain.Emit("VoteTokenPurchased", "voter", caller.String()) } -// BalanceOfVoteToken returns the number of vote tokens held by the caller. -func BalanceOfVoteToken(_ realm) int64 { - return VoteToken.BalanceOf(runtime.PreviousRealm().Address()) -} - // VoteOnDispute allows a user to commit a vote during a dispute. func VoteOnDispute(_ realm, id string, hash string) { dispute := getDispute(id) res, _ := requests.Get(id) 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 { panic("error: You need at least 1 vote token to vote.") @@ -86,10 +86,12 @@ func VoteOnDispute(_ realm, id string, hash string) { Hash: hash, Revealed: false, } + voter, exist := dispute.Voters.Get(string(vote.Voter)) if exist && voter.(Voter).HasVoted { panic("error: Voter has already voted in this dispute.") } + dispute.Votes = append(dispute.Votes, vote) dispute.Voters.Set(string(vote.Voter), Voter{HasVoted: true, VoteIndex: int64(len(dispute.Votes) - 1)}) disputes.Set(id, dispute) @@ -99,19 +101,24 @@ func VoteOnDispute(_ realm, id string, hash string) { // RevealVote allows a user to reveal their vote after the voting period has ended. func RevealVote(_ realm, id string, value int64, salt string) { dispute := getDispute(id) + if dispute.IsResolved { panic("error: Dispute is resolved.") } + if time.Now().Before(dispute.EndTime) { panic("error: Vote period has not ended yet.") } + if time.Now().After(dispute.EndRevealTime) { panic("error: Reveal period has ended.") } + voter, exist := dispute.Voters.Get(string(runtime.PreviousRealm().Address())) if !exist || !voter.(Voter).HasVoted { panic("error: Voter did not participate in this dispute.") } + vote := dispute.Votes[voter.(Voter).VoteIndex] if vote.Revealed { panic("error: Vote already revealed.") @@ -123,6 +130,7 @@ func RevealVote(_ realm, id string, value int64, salt string) { if vote.Hash != expectedHash { panic("error: Hash does not match the revealed value and salt.") } + vote.Value = value vote.Revealed = true dispute.NbResolvedVotes += 1 @@ -137,9 +145,11 @@ func ResolveDispute(_ realm, id string) { if dispute.IsResolved { panic("error: Dispute is already resolved.") } + if time.Now().Before(dispute.EndTime) { panic("error: Dispute period has not ended yet.") } + val := resolve(id) dispute.WinningValue = val dispute.IsResolved = true @@ -196,40 +206,40 @@ func SetVoteTokenPrice(_ realm, price int64) { // -- view functions -- // GetDispute returns the details of a specific dispute. -func GetDispute(_ realm, id string) Dispute { +func GetDispute(id string) Dispute { return getDispute(id) } // GetDisputeDuration returns the current dispute duration. -func GetDisputeDuration(_ realm) int64 { +func GetDisputeDuration() int64 { return disputeDuration } // GetVoteTokenPrice returns the current vote price. -func GetVoteTokenPrice(_ realm) int64 { +func GetVoteTokenPrice() int64 { return voteTokenPrice } // GetDisputeEndTime returns the end time of the voting period for a specific dispute. -func GetDisputeEndTime(_ realm, id string) time.Time { +func GetDisputeEndTime(id string) time.Time { dispute := getDispute(id) return dispute.EndTime } // GetDisputeVotesAmount returns the total number of votes cast in a dispute. -func GetDisputeVotesAmount(_ realm, id string) int64 { +func GetDisputeVotesAmount(id string) int64 { dispute := getDispute(id) return int64(len(dispute.Votes)) } // GetRevealEndTime returns the end time of the reveal period for a specific dispute. -func GetRevealEndTime(_ realm, id string) time.Time { +func GetRevealEndTime(id string) time.Time { dispute := getDispute(id) return dispute.EndRevealTime } // GetRevealDuration returns the current reveal duration. -func GetRevealDuration(_ realm) int64 { +func GetRevealDuration() int64 { return revealDuration } diff --git a/packages/r/intermarch3/goo/court_test.gno b/packages/r/intermarch3/goo/court_test.gno index dcf4d83..a49f72b 100644 --- a/packages/r/intermarch3/goo/court_test.gno +++ b/packages/r/intermarch3/goo/court_test.gno @@ -23,7 +23,7 @@ func TestBuyInitialVoteToken(t *testing.T) { urequire.NotPanics(t, func() { BuyInitialVoteToken(cross) }, "user should be able to buy a vote token by sending the correct amount") - amount := BalanceOfVoteToken(cross) + amount := VoteToken.BalanceOf(user1) urequire.Equal(t, int64(1), amount, "user should have received a vote token") testing.SetRealm(testing.NewUserRealm(user1)) diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index ec429dc..737bf9c 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -9,6 +9,7 @@ import ( "gno.land/p/nt/avl" "gno.land/p/nt/seqid" + "gno.land/p/nt/ufmt" ) var ( @@ -29,18 +30,20 @@ func init() { // RequestData allows a user to request data from the oracle. // `requesterReward` value needs to be sent to the contract as a reward +// `deadline`is the timestamp in unix format for the deadline of the request. // 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 { if ancillaryData == "" { panic("error: Ancillary data cannot be empty.") } + if deadline < time.Now().Add(24*time.Hour).Unix() { panic("error: Deadline must be at least 24 hours in the future.") } coins := banker.OriginSend() if len(coins) != 1 || coins.AmountOf("ugnot") != requesterReward { - panic("error: Incorrect reward amount sent. Required: " + strconv.FormatInt(requesterReward, 10) + " ugnot.") + panic(ufmt.Sprintf("error: Incorrect reward amount sent. Required: %d ugnot.", requesterReward)) } id := idGenerator.Next().String() @@ -66,9 +69,11 @@ func ProposeValue(_ realm, id string, proposedValue int64) { if request.State != "Requested" { panic("error: Request is not in 'Requested' state.") } + if request.YesNoQuestion && proposedValue != 0 && proposedValue != 1 { panic("error: Proposed value must be 0 or 1 for yes/no questions.") } + if time.Now().After(request.Deadline) { panic("error: Deadline for proposal has passed.") } @@ -94,9 +99,11 @@ func DisputeData(_ realm, id string) string { if request.Proposer == runtime.PreviousRealm().Address() { panic("error: Proposer cannot dispute their own proposal.") } + if request.State != "Proposed" { panic("error: Request is not in 'Proposed' state.") } + if time.Now().After(request.ResolutionTime) { panic("error: Dispute period has ended.") } @@ -121,15 +128,19 @@ func ResolveRequest(_ realm, id string) { if request.State == "Disputed" { panic("error: Request is in 'Disputed' state.") } + if request.State == "Proposed" && request.ResolutionTime.After(time.Now()) { panic("error: Resolution period has not ended yet.") } + if request.State == "Requested" { panic("error: Request has not been proposed yet.") } + if request.State == "Resolved" { panic("error: Request is already resolved.") } + request.State = "Resolved" request.WinningValue = request.ProposedValue requests.Set(id, request) @@ -158,9 +169,11 @@ func RequesterRetreiveFund(_ realm, id string) { if request.State != "Requested" { panic("error: cannot retreive fund as requests fulfilled.") } + if request.Creator != runtime.PreviousRealm().Address() { panic("error: Only the creator of the request can retrieve the fund.") } + if request.Deadline.After(time.Now()) { panic("error: Cannot retrieve fund before the deadline.") } @@ -220,33 +233,33 @@ func ChangeAdmin(_ realm, newAdmin address) { // -- VIEW FUNCTIONS -- // GetRequest returns the details of a specific data request. -func GetRequest(_ realm, id string) DataRequest { +func GetRequest(id string) DataRequest { return getRequest(id) } // GetBond returns the current bond amount. -func GetBond(_ realm) int64 { +func GetBond() int64 { return bond } // GetResolutionTime returns the current resolution time duration. -func GetResolutionTime(_ realm) int64 { +func GetResolutionTime() int64 { return resolutionTime } // GetRequesterReward returns the current requester reward amount. -func GetRequesterReward(_ realm) int64 { +func GetRequesterReward() int64 { return requesterReward } // GetRequeststate returns the current state of a specific data request. -func GetRequeststate(_ realm, id string) string { +func GetRequeststate(id string) string { request := getRequest(id) return request.State } // GetAdmin returns the current admin address. -func GetAdmin(_ realm) address { +func GetAdmin() address { return admin } diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index 36d5058..1c3d6a3 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -27,6 +27,7 @@ func renderHome() string { msg += md.Paragraph("For more information, visit the " + md.Link("Readme", runtime.CurrentRealm().PkgPath()[8:]+"$source") + ".") msg += md.Link("Request Data", ufmt.Sprintf("%s", txlink.NewLink("RequestData").AddArgs("ancillaryData", "YOUR_QUESTION", "yesNoQuestion", "true or false", "deadline", "DEADLINE_TIMESTAMP").SetSend(strconv.Itoa(int(requesterReward))+"ugnot").URL())) + "\n\n(Note: Requesting data need to pay a reward of " + strconv.Itoa(int(requesterReward/1_000_000)) + " GNOT to the proposer).\n\n" msg += md.H2("Current requests :") + // List current requests and build table var table []string table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) @@ -48,7 +49,7 @@ func renderHome() string { msg += md.HorizontalRule() // List last 5 resolved requests and build table - msg += md.H2("Last 5 Resolved requests :") + msg += md.H2("Last 5 Resolved requests:") var resolved []string resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) @@ -107,6 +108,7 @@ func renderRequestPage(id string) string { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" return msg } + dispute := res.(Dispute) msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") msg += md.Paragraph("Reveal Votes: \n\n") @@ -135,6 +137,7 @@ func renderRequestPage(id string) string { msg += md.Paragraph("End of Reveal Time: " + dispute.EndRevealTime.Format(DateFormat)) msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") msg += md.Paragraph("Revealed Votes: \n\n") + if dispute.NbResolvedVotes > 0 { msg += renderRevealedVotes(dispute) + "\n\n" } else { @@ -152,8 +155,10 @@ func renderRequestPage(id string) string { // create table of the votes that have been revealed func renderRevealedVotes(dispute Dispute) string { var table []string + table = append(table, md.Bold("Voter"), md.Bold("Vote"), md.Bold("Token Amount")) request := getRequest(dispute.RequestId) + for _, vote := range dispute.Votes { if vote.Revealed { table = append(table, md.Paragraph(vote.Voter.String()), md.Paragraph(rendervalue(request, vote.Value)), md.Paragraph(strconv.Itoa(int(vote.TokenAmount)))) @@ -178,6 +183,7 @@ func rendervalue(req DataRequest, val int64) string { func renderRequest(req DataRequest) []string { var table []string + table = append(table, md.Paragraph(req.AncillaryData)) if req.State == "Requested" { table = append(table, md.Paragraph("N/A")) @@ -193,6 +199,7 @@ func renderRequest(req DataRequest) []string { func renderAction(req DataRequest) string { action := md.H2("Actions:\n\n") dispute, _ := disputes.Get(req.Id) + if req.State == "Requested" { // tx to propose a value action += md.Link("Propose a value", ufmt.Sprintf("%s", txlink.NewLink("ProposeValue").AddArgs("id", req.Id, "proposedValue", "YOUR_VALUE_HERE").SetSend(strconv.Itoa(int(bond))+"ugnot").URL())) + "\n\n(Note: Proposing need to bond " + strconv.Itoa(int(bond/1_000_000)) + " GNOT)" diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno index 37d46bd..720225f 100644 --- a/packages/r/intermarch3/goo/resolver.gno +++ b/packages/r/intermarch3/goo/resolver.gno @@ -1,3 +1,6 @@ +// Package goo contains the resolver logic for the GOO. +// It supports both Yes/No and Numeric resolutions. +// It is used to determine the winning value of a dispute and reward the voters. package goo import ( @@ -8,20 +11,28 @@ import ( func resolve(id string) int64 { dispute := getDispute(id) request := getRequest(id) + if dispute.NbResolvedVotes == 0 { // If no one voted or reveal their vote, the proposed value wins by default return request.ProposedValue } + if request.YesNoQuestion { return resolveYesNo(dispute) - } else { - return resolveNumeric(dispute) } + return resolveNumeric(dispute) } +// resolveYesNo determines the winning value for yes/no disputes based on the highest total token weight. +// If 0 votes or equal votes, the proposed value wins by default. func resolveYesNo(dispute Dispute) int64 { - yesVotes := int64(0) - noVotes := int64(0) + var ( + winningValue int64 + weight int64 + yesVotes int64 + noVotes int64 + ) + for _, vote := range dispute.Votes { if vote.Revealed { if vote.Value == 1 { @@ -31,8 +42,7 @@ func resolveYesNo(dispute Dispute) int64 { } } } - var winningValue int64 - var weight int64 + if yesVotes > noVotes { winningValue = 1 weight = yesVotes @@ -40,13 +50,20 @@ func resolveYesNo(dispute Dispute) int64 { winningValue = 0 weight = noVotes } - rewardAndSlachVoters(dispute, winningValue, weight) + + rewardAndSlashVoters(dispute, winningValue, weight) return winningValue } // resolveNumeric determines the winning value for numeric disputes based on the highest total token weight. // Not the best algorithm, but gas effective and fast. func resolveNumeric(dispute Dispute) int64 { + // Find the value with the most token weight + var ( + winningValue int64 + maxWeight int64 + ) + // Collect unique values and aggregate weights valueWeights := make(map[int64]int64) for _, vote := range dispute.Votes { @@ -55,37 +72,37 @@ func resolveNumeric(dispute Dispute) int64 { } } - // Find the value with the most token weight - var winningValue int64 - var maxWeight int64 - for value, weight := range valueWeights { if weight > maxWeight { maxWeight = weight winningValue = value } } - rewardAndSlachVoters(dispute, winningValue, maxWeight) + + rewardAndSlashVoters(dispute, winningValue, maxWeight) return winningValue } -func rewardAndSlachVoters(dispute Dispute, winningValue, totalWeight int64) { +func rewardAndSlashVoters(dispute Dispute, winningValue, totalWeight int64) { for _, vote := range dispute.Votes { - if vote.Revealed { - if vote.Value == winningValue { - // Reward winning voters - VoteToken.mint(vote.Voter, 2) - reward := bond * (vote.TokenAmount / totalWeight) - if reward != 0 { - bank.SendCoins(runtime.CurrentRealm().Address(), vote.Voter, chain.Coins{chain.Coin{Denom: "ugnot", Amount: reward}}) - } - chain.Emit("VoterRewarded", "voter", vote.Voter.String()) - } else { - // Slash losing voters - slash := vote.TokenAmount / 4 // 25% slash - VoteToken.burn(vote.Voter, slash) - chain.Emit("VoterSlashed", "voter", vote.Voter.String()) + if !vote.Revealed { + continue + } + + if vote.Value == winningValue { + // Reward winning voters + VoteToken.mint(vote.Voter, 2) + reward := bond * (vote.TokenAmount / totalWeight) + + if reward != 0 { + bank.SendCoins(runtime.CurrentRealm().Address(), vote.Voter, chain.Coins{chain.Coin{Denom: "ugnot", Amount: reward}}) } + chain.Emit("VoterRewarded", "voter", vote.Voter.String()) + } else { + // Slash losing voters + slash := vote.TokenAmount / 4 // 25% slash + VoteToken.burn(vote.Voter, slash) + chain.Emit("VoterSlashed", "voter", vote.Voter.String()) } } } From 1dd026288a16ffb6852748b728a0f8e1f8999e21 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 25 Nov 2025 15:54:07 +0100 Subject: [PATCH 13/14] refacto: use pointer in avl tree --- packages/r/intermarch3/goo/court.gno | 15 ++++------ packages/r/intermarch3/goo/oracle.gno | 12 +++----- packages/r/intermarch3/goo/render.gno | 40 +++++++++++-------------- packages/r/intermarch3/goo/resolver.gno | 6 ++-- 4 files changed, 30 insertions(+), 43 deletions(-) diff --git a/packages/r/intermarch3/goo/court.gno b/packages/r/intermarch3/goo/court.gno index cfb2adf..b9a05db 100644 --- a/packages/r/intermarch3/goo/court.gno +++ b/packages/r/intermarch3/goo/court.gno @@ -25,7 +25,7 @@ func initiateDispute(id string) { if _, exists := disputes.Get(id); exists { panic("error: Dispute for this request already exists.") } - dispute := Dispute{ + dispute := &Dispute{ RequestId: id, Votes: []Vote{}, Voters: avl.NewTree(), @@ -59,8 +59,7 @@ func BuyInitialVoteToken(_ realm) { // VoteOnDispute allows a user to commit a vote during a dispute. func VoteOnDispute(_ realm, id string, hash string) { dispute := getDispute(id) - res, _ := requests.Get(id) - request := res.(DataRequest) + request := getRequest(id) if request.Proposer == runtime.PreviousRealm().Address() || request.Disputer == runtime.PreviousRealm().Address() { panic("error: Proposer and Disputer cannot vote in this dispute.") @@ -94,7 +93,6 @@ func VoteOnDispute(_ realm, id string, hash string) { dispute.Votes = append(dispute.Votes, vote) dispute.Voters.Set(string(vote.Voter), Voter{HasVoted: true, VoteIndex: int64(len(dispute.Votes) - 1)}) - disputes.Set(id, dispute) chain.Emit("VoteSubmitted", "id", id, "voter", vote.Voter.String()) } @@ -135,7 +133,6 @@ func RevealVote(_ realm, id string, value int64, salt string) { vote.Revealed = true dispute.NbResolvedVotes += 1 dispute.Votes[voter.(Voter).VoteIndex] = vote - disputes.Set(id, dispute) chain.Emit("VoteRevealed", "id", id, "voter", vote.Voter.String(), "value", strconv.Itoa(int(value))) } @@ -153,13 +150,11 @@ func ResolveDispute(_ realm, id string) { val := resolve(id) dispute.WinningValue = val dispute.IsResolved = true - disputes.Set(id, dispute) // Update the original request with the winning value request := getRequest(id) request.ProposedValue = val request.State = "Resolved" - requests.Set(id, request) chain.Emit("DisputeResolved", "id", id, "winningValue", strconv.Itoa(int(val))) chain.Emit("RequestResolved", "id", id, "winningValue", strconv.Itoa(int(val))) @@ -206,7 +201,7 @@ func SetVoteTokenPrice(_ realm, price int64) { // -- view functions -- // GetDispute returns the details of a specific dispute. -func GetDispute(id string) Dispute { +func GetDispute(id string) *Dispute { return getDispute(id) } @@ -245,10 +240,10 @@ func GetRevealDuration() int64 { // Utils functions -func getDispute(id string) Dispute { +func getDispute(id string) *Dispute { dispute, exists := disputes.Get(id) if !exists { panic("error: Dispute with this ID does not exist.") } - return dispute.(Dispute) + return dispute.(*Dispute) } diff --git a/packages/r/intermarch3/goo/oracle.gno b/packages/r/intermarch3/goo/oracle.gno index 737bf9c..00dfdaa 100644 --- a/packages/r/intermarch3/goo/oracle.gno +++ b/packages/r/intermarch3/goo/oracle.gno @@ -48,7 +48,7 @@ func RequestData(_ realm, ancillaryData string, yesNoQuestion bool, deadline int id := idGenerator.Next().String() - request := DataRequest{ + request := &DataRequest{ Id: id, Timestamp: time.Now(), AncillaryData: ancillaryData, @@ -88,7 +88,6 @@ func ProposeValue(_ realm, id string, proposedValue int64) { request.ProposerBond = bond request.ResolutionTime = time.Now().Add(time.Duration(resolutionTime) * time.Second) request.State = "Proposed" - requests.Set(id, request) chain.Emit("ValueProposed", "id", id, "proposedValue", strconv.Itoa(int(proposedValue)), "proposer", request.Proposer.String(), "resolutionTime", request.ResolutionTime.String()) } @@ -116,7 +115,6 @@ func DisputeData(_ realm, id string) string { request.Disputer = runtime.PreviousRealm().Address() request.DisputerBond = bond request.State = "Disputed" - requests.Set(id, request) initiateDispute(id) chain.Emit("DataDisputed", "id", id, "disputer", request.Disputer.String()) return "Time: " + time.Now().String() @@ -143,7 +141,6 @@ func ResolveRequest(_ realm, id string) { request.State = "Resolved" request.WinningValue = request.ProposedValue - requests.Set(id, request) from := runtime.CurrentRealm().Address() to := request.Proposer @@ -184,7 +181,6 @@ func RequesterRetreiveFund(_ realm, id string) { bank.SendCoins(from, to, refund) request.State = "Expired" - requests.Set(id, request) chain.Emit("RequestExpired", "id", id) } @@ -233,7 +229,7 @@ func ChangeAdmin(_ realm, newAdmin address) { // -- VIEW FUNCTIONS -- // GetRequest returns the details of a specific data request. -func GetRequest(id string) DataRequest { +func GetRequest(id string) *DataRequest { return getRequest(id) } @@ -265,10 +261,10 @@ func GetAdmin() address { // Utils functions -func getRequest(id string) DataRequest { +func getRequest(id string) *DataRequest { request, exists := requests.Get(id) if !exists { panic("error: Request with this ID does not exist.") } - return request.(DataRequest) + return request.(*DataRequest) } diff --git a/packages/r/intermarch3/goo/render.gno b/packages/r/intermarch3/goo/render.gno index 1c3d6a3..b04328c 100644 --- a/packages/r/intermarch3/goo/render.gno +++ b/packages/r/intermarch3/goo/render.gno @@ -33,10 +33,10 @@ func renderHome() string { table = append(table, md.Bold("Question"), md.Bold("Proposed Value"), md.Bold("State"), md.Bold("See more")) requests.Iterate("", "", func(key string, value any) bool { - if value.(DataRequest).State == "Resolved" { + if value.(*DataRequest).State == "Resolved" { return false } - table = append(table, renderRequest(value.(DataRequest))...) + table = append(table, renderRequest(value.(*DataRequest))...) return false }) msg += md.ColumnsN(table, 4, false) @@ -54,13 +54,13 @@ func renderHome() string { resolved = append(resolved, md.Bold("Question"), md.Bold("Winning Value"), md.Bold("Proposer"), md.Bold("See more")) requests.Iterate("", "", func(key string, value any) bool { - if value.(DataRequest).State != "Resolved" { + if value.(*DataRequest).State != "Resolved" { return false } - resolved = append(resolved, md.Paragraph(value.(DataRequest).AncillaryData)) - resolved = append(resolved, md.Paragraph(rendervalue(value.(DataRequest), value.(DataRequest).WinningValue))) - resolved = append(resolved, md.Paragraph(value.(DataRequest).Proposer.String())) - resolved = append(resolved, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+value.(DataRequest).Id)) + resolved = append(resolved, md.Paragraph(value.(*DataRequest).AncillaryData)) + resolved = append(resolved, md.Paragraph(rendervalue(value.(*DataRequest), value.(*DataRequest).WinningValue))) + resolved = append(resolved, md.Paragraph(value.(*DataRequest).Proposer.String())) + resolved = append(resolved, md.Link("Click here", runtime.CurrentRealm().PkgPath()[8:]+":"+value.(*DataRequest).Id)) return false }) msg += md.ColumnsN(resolved, 4, false) @@ -74,11 +74,7 @@ func renderHome() string { // renderRequestPage renders the page for a specific request func renderRequestPage(id string) string { - res, exists := requests.Get(id) - if !exists { - return md.H1("error: No request with this ID exists.") - } - req := res.(DataRequest) + req := getRequest(id) msg := md.H1("Question: " + req.AncillaryData) msg += md.H2("Details:\n\n") @@ -103,17 +99,17 @@ func renderRequestPage(id string) string { // if the request is resolved, show the winning value and the number of votes if req.State == "Resolved" { msg += md.H3("Winning Value: "+rendervalue(req, req.WinningValue)) + "\n\n" - res, exists = disputes.Get(req.Id) + res, exists := disputes.Get(req.Id) if !exists { msg += md.Paragraph("Value Proposed by: "+req.Proposer.String()) + "\n\n" return msg } - dispute := res.(Dispute) + dispute := res.(*Dispute) msg += md.Paragraph("Number of Votes: " + strconv.Itoa(len(dispute.Votes)) + "\n\n") msg += md.Paragraph("Reveal Votes: \n\n") if dispute.NbResolvedVotes > 0 { - msg += renderRevealedVotes(dispute) + "\n\n" + msg += renderRevealedVotes(*dispute) + "\n\n" } else { msg += md.Paragraph("No votes revealed. Proposed value win by default\n\n") } @@ -130,8 +126,8 @@ func renderRequestPage(id string) string { // if the request is disputed, show the dispute details if req.State == "Disputed" { res, exist := disputes.Get(req.Id) - dispute := res.(Dispute) if exist { + dispute := res.(*Dispute) msg += md.H2("Dispute Details:\n\n") msg += md.Paragraph("End of Voting Time: " + dispute.EndTime.Format(DateFormat) + "\n\n") msg += md.Paragraph("End of Reveal Time: " + dispute.EndRevealTime.Format(DateFormat)) @@ -139,7 +135,7 @@ func renderRequestPage(id string) string { msg += md.Paragraph("Revealed Votes: \n\n") if dispute.NbResolvedVotes > 0 { - msg += renderRevealedVotes(dispute) + "\n\n" + msg += renderRevealedVotes(*dispute) + "\n\n" } else { msg += md.Paragraph("No votes revealed yet.\n\n") } @@ -167,7 +163,7 @@ func renderRevealedVotes(dispute Dispute) string { return md.ColumnsN(table, 3, false) } -func rendervalue(req DataRequest, val int64) string { +func rendervalue(req *DataRequest, val int64) string { if req.YesNoQuestion { if val == 1 { return "Yes" @@ -181,7 +177,7 @@ func rendervalue(req DataRequest, val int64) string { } } -func renderRequest(req DataRequest) []string { +func renderRequest(req *DataRequest) []string { var table []string table = append(table, md.Paragraph(req.AncillaryData)) @@ -196,7 +192,7 @@ func renderRequest(req DataRequest) []string { } // return appropriate action link based on the state of the request -func renderAction(req DataRequest) string { +func renderAction(req *DataRequest) string { action := md.H2("Actions:\n\n") dispute, _ := disputes.Get(req.Id) @@ -209,12 +205,12 @@ func renderAction(req DataRequest) string { } else if req.State == "Disputed" { if req.ResolutionTime.Before(time.Now()) { // tx to reveal a vote - action += md.Link("Reveal vote", ufmt.Sprintf("%s", txlink.NewLink("RevealVote").AddArgs("id", req.Id, "value", "YOUR_VALUE_HERE", "salt", "YOUR_SALT_HERE").URL())) + "\n\n(Note: You must reveal your vote before the reveal period ends at " + dispute.(Dispute).EndRevealTime.UTC().Format(time.UnixDate) + ")" + action += md.Link("Reveal vote", ufmt.Sprintf("%s", txlink.NewLink("RevealVote").AddArgs("id", req.Id, "value", "YOUR_VALUE_HERE", "salt", "YOUR_SALT_HERE").URL())) + "\n\n(Note: You must reveal your vote before the reveal period ends at " + dispute.(*Dispute).EndRevealTime.UTC().Format(time.UnixDate) + ")" } else { // tx to vote on a dispute action += md.Link("Vote", ufmt.Sprintf("%s", txlink.NewLink("VoteOnDispute").AddArgs("id", req.Id, "hash", "YOUR_HASH_HERE").URL())) + "\n\n(Note: Voting need to hold a Vote Token and requires you to hash your vote with a salt. Use a tool to generate the sha256 hash and keep your salt safe to reveal your vote)" } - } else if req.State == "Disputed" && dispute.(Dispute).EndTime.Before(time.Now()) { + } else if req.State == "Disputed" && dispute.(*Dispute).EndTime.Before(time.Now()) { // tx to resolve a dispute action += md.Link("Resolve dispute", ufmt.Sprintf("%s", txlink.NewLink("ResolveDispute").AddArgs("id", req.Id).URL())) + "\n\n" } else if req.State == "Proposed" && req.ResolutionTime.Before(time.Now()) { diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno index 720225f..70aaa64 100644 --- a/packages/r/intermarch3/goo/resolver.gno +++ b/packages/r/intermarch3/goo/resolver.gno @@ -25,7 +25,7 @@ func resolve(id string) int64 { // resolveYesNo determines the winning value for yes/no disputes based on the highest total token weight. // If 0 votes or equal votes, the proposed value wins by default. -func resolveYesNo(dispute Dispute) int64 { +func resolveYesNo(dispute *Dispute) int64 { var ( winningValue int64 weight int64 @@ -57,7 +57,7 @@ func resolveYesNo(dispute Dispute) int64 { // resolveNumeric determines the winning value for numeric disputes based on the highest total token weight. // Not the best algorithm, but gas effective and fast. -func resolveNumeric(dispute Dispute) int64 { +func resolveNumeric(dispute *Dispute) int64 { // Find the value with the most token weight var ( winningValue int64 @@ -83,7 +83,7 @@ func resolveNumeric(dispute Dispute) int64 { return winningValue } -func rewardAndSlashVoters(dispute Dispute, winningValue, totalWeight int64) { +func rewardAndSlashVoters(dispute *Dispute, winningValue, totalWeight int64) { for _, vote := range dispute.Votes { if !vote.Revealed { continue From 681a1efa3ad9dee5de7f749f08f23f36fb974567 Mon Sep 17 00:00:00 2001 From: Lucas Leclerc Date: Tue, 25 Nov 2025 19:03:50 +0100 Subject: [PATCH 14/14] fix: format --- packages/r/intermarch3/goo/resolver.gno | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/r/intermarch3/goo/resolver.gno b/packages/r/intermarch3/goo/resolver.gno index 70aaa64..0ed2b71 100644 --- a/packages/r/intermarch3/goo/resolver.gno +++ b/packages/r/intermarch3/goo/resolver.gno @@ -28,9 +28,9 @@ func resolve(id string) int64 { func resolveYesNo(dispute *Dispute) int64 { var ( winningValue int64 - weight int64 - yesVotes int64 - noVotes int64 + weight int64 + yesVotes int64 + noVotes int64 ) for _, vote := range dispute.Votes { @@ -61,7 +61,7 @@ func resolveNumeric(dispute *Dispute) int64 { // Find the value with the most token weight var ( winningValue int64 - maxWeight int64 + maxWeight int64 ) // Collect unique values and aggregate weights