667 lines
29 KiB
TypeScript
667 lines
29 KiB
TypeScript
import { expect } from "chai";
|
|
import { ethers } from "hardhat";
|
|
import { OptimisticOracle, Decider } from "../typechain-types";
|
|
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
|
|
describe("OptimisticOracle", function () {
|
|
before(async () => {
|
|
await ethers.provider.send("evm_setAutomine", [true]);
|
|
await ethers.provider.send("evm_setIntervalMining", [0]);
|
|
});
|
|
|
|
let optimisticOracle: OptimisticOracle;
|
|
let deciderContract: Decider;
|
|
let owner: HardhatEthersSigner;
|
|
let asserter: HardhatEthersSigner;
|
|
let proposer: HardhatEthersSigner;
|
|
let disputer: HardhatEthersSigner;
|
|
let otherUser: HardhatEthersSigner;
|
|
|
|
const contractAddress = process.env.CONTRACT_ADDRESS;
|
|
|
|
let contractArtifact: string;
|
|
if (contractAddress) {
|
|
// For the autograder
|
|
contractArtifact = `contracts/download-${contractAddress}.sol:OptimisticOracle`;
|
|
} else {
|
|
contractArtifact = "contracts/02_Optimistic/OptimisticOracle.sol:OptimisticOracle";
|
|
}
|
|
|
|
// Enum for state
|
|
const State = {
|
|
Invalid: 0n,
|
|
Asserted: 1n,
|
|
Proposed: 2n,
|
|
Disputed: 3n,
|
|
Settled: 4n,
|
|
Expired: 5n,
|
|
};
|
|
|
|
beforeEach(async function () {
|
|
[owner, asserter, proposer, disputer, otherUser] = await ethers.getSigners();
|
|
|
|
// Deploy OptimisticOracle with temporary decider (owner)
|
|
const OptimisticOracleFactory = await ethers.getContractFactory(contractArtifact);
|
|
optimisticOracle = (await OptimisticOracleFactory.deploy(owner.address)) as OptimisticOracle;
|
|
|
|
// Deploy Decider
|
|
const DeciderFactory = await ethers.getContractFactory("Decider");
|
|
deciderContract = await DeciderFactory.deploy(optimisticOracle.target);
|
|
|
|
// Set the decider in the oracle
|
|
await optimisticOracle.setDecider(deciderContract.target);
|
|
});
|
|
describe("Checkpoint4", function () {
|
|
describe("Deployment", function () {
|
|
it("Should deploy successfully", function () {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
expect(optimisticOracle.target).to.not.be.undefined;
|
|
});
|
|
|
|
it("Should set the correct owner", async function () {
|
|
const contractOwner = await optimisticOracle.owner();
|
|
expect(contractOwner).to.equal(owner.address);
|
|
});
|
|
|
|
it("Should have correct constants", async function () {
|
|
const minimumAssertionWindow = await optimisticOracle.MINIMUM_ASSERTION_WINDOW();
|
|
const disputeWindow = await optimisticOracle.DISPUTE_WINDOW();
|
|
|
|
expect(minimumAssertionWindow).to.equal(180n); // 3 minutes
|
|
expect(disputeWindow).to.equal(180n); // 3 minutes
|
|
});
|
|
|
|
it("Should start with nextAssertionId at 1", async function () {
|
|
const nextAssertionId = await optimisticOracle.nextAssertionId();
|
|
expect(nextAssertionId).to.equal(1n);
|
|
});
|
|
|
|
it("Should return correct assertionId for first assertion", async function () {
|
|
const description = "Will Bitcoin reach $1m by end of 2026?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
// Get the assertionId from the event
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
expect(assertionId).to.equal(1n);
|
|
});
|
|
});
|
|
|
|
describe("Event Assertion", function () {
|
|
it("Should allow users to assert events with reward", async function () {
|
|
const description = "Will Bitcoin reach $1m by end of 2026?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
|
|
// Get the assertionId from the event
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
expect(tx)
|
|
.to.emit(optimisticOracle, "EventAsserted")
|
|
.withArgs(assertionId, asserter.address, description, reward);
|
|
});
|
|
|
|
it("Should reject assertions with insufficient reward", async function () {
|
|
const description = "Will Bitcoin reach $1m by end of 2026?";
|
|
const insufficientReward = ethers.parseEther("0.0");
|
|
|
|
await expect(
|
|
optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: insufficientReward }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidValue");
|
|
});
|
|
});
|
|
|
|
describe("Outcome Proposal", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
// Get the assertionId from the event
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
});
|
|
|
|
it("Should allow proposing outcomes with correct bond", async function () {
|
|
const bond = reward * 2n;
|
|
const outcome = true;
|
|
|
|
const tx = await optimisticOracle.connect(proposer).proposeOutcome(assertionId, outcome, { value: bond });
|
|
|
|
expect(tx).to.emit(optimisticOracle, "OutcomeProposed").withArgs(assertionId, proposer.address, outcome);
|
|
});
|
|
|
|
it("Should reject proposals with incorrect bond", async function () {
|
|
const wrongBond = ethers.parseEther("0.05");
|
|
const outcome = true;
|
|
|
|
await expect(
|
|
optimisticOracle.connect(proposer).proposeOutcome(assertionId, outcome, { value: wrongBond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidValue");
|
|
});
|
|
|
|
it("Should reject duplicate proposals", async function () {
|
|
const bond = reward * 2n;
|
|
const outcome = true;
|
|
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, outcome, { value: bond });
|
|
|
|
await expect(
|
|
optimisticOracle.connect(otherUser).proposeOutcome(assertionId, !outcome, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "AssertionProposed");
|
|
});
|
|
});
|
|
|
|
describe("Outcome Dispute", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
});
|
|
|
|
it("Should allow disputing outcomes with correct bond", async function () {
|
|
const bond = reward * 2n;
|
|
|
|
const tx = await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
|
|
expect(tx).to.emit(optimisticOracle, "OutcomeDisputed").withArgs(assertionId, disputer.address);
|
|
});
|
|
|
|
it("Should reject disputes with incorrect bond", async function () {
|
|
const wrongBond = ethers.parseEther("0.05");
|
|
|
|
await expect(
|
|
optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: wrongBond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidValue");
|
|
});
|
|
|
|
it("Should reject disputes after deadline", async function () {
|
|
// Fast forward time past dispute window
|
|
await ethers.provider.send("evm_increaseTime", [181]); // 3 minutes + 1 second
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const bond = reward * 2n;
|
|
await expect(
|
|
optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidTime");
|
|
});
|
|
|
|
it("Should reject duplicate disputes", async function () {
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
|
|
await expect(
|
|
optimisticOracle.connect(otherUser).disputeOutcome(assertionId, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "ProposalDisputed");
|
|
});
|
|
});
|
|
|
|
describe("Start and End Time Logic", function () {
|
|
it("Should not allow proposal before startTime", async function () {
|
|
const reward = ethers.parseEther("1");
|
|
const now = (await ethers.provider.getBlock("latest"))!.timestamp;
|
|
const start = now + 1000;
|
|
const end = start + 1000;
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent("future event", start, end, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
if (!parsedEvent) throw new Error("Event not found");
|
|
const assertionId = parsedEvent.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await expect(
|
|
optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidTime");
|
|
});
|
|
|
|
it("Should not allow proposal after endTime", async function () {
|
|
const reward = ethers.parseEther("1");
|
|
const now = (await ethers.provider.getBlock("latest"))!.timestamp;
|
|
const start = now + 1; // Start time must be in the future
|
|
const end = now + 200; // 200 seconds, which is more than DISPUTE_WINDOW (180 seconds)
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent("short event", start, end, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
if (!parsedEvent) throw new Error("Event not found");
|
|
const assertionId = parsedEvent.args[0];
|
|
|
|
// Wait until after endTime
|
|
await ethers.provider.send("evm_increaseTime", [201]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const bond = reward * 2n;
|
|
await expect(
|
|
optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidTime");
|
|
});
|
|
|
|
it("Should allow proposal only within [startTime, endTime]", async function () {
|
|
const reward = ethers.parseEther("1");
|
|
const now = (await ethers.provider.getBlock("latest"))!.timestamp;
|
|
const start = now + 10; // Start time in the future
|
|
const end = start + 200; // Ensure endTime is far enough in the future
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent("window event", start, end, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
if (!parsedEvent) throw new Error("Event not found");
|
|
const assertionId = parsedEvent.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
|
|
// Before startTime - should fail
|
|
await expect(
|
|
optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond }),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidTime");
|
|
|
|
// Move to startTime
|
|
await ethers.provider.send("evm_increaseTime", [10]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
// Now it should work
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Checkpoint5", function () {
|
|
describe("Undisputed Reward Claiming", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
});
|
|
|
|
it("Should allow claiming undisputed rewards after deadline", async function () {
|
|
// Fast forward time past dispute window
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const initialBalance = await ethers.provider.getBalance(proposer.address);
|
|
const tx = await optimisticOracle.connect(proposer).claimUndisputedReward(assertionId);
|
|
const receipt = await tx.wait();
|
|
const finalBalance = await ethers.provider.getBalance(proposer.address);
|
|
|
|
// Check that proposer received the reward (reward + bond - gas costs)
|
|
const expectedReward = reward + reward * 2n;
|
|
const gasCost = receipt!.gasUsed * receipt!.gasPrice!;
|
|
expect(finalBalance - initialBalance + gasCost).to.equal(expectedReward);
|
|
});
|
|
|
|
it("Should reject claiming before deadline", async function () {
|
|
await expect(
|
|
optimisticOracle.connect(proposer).claimUndisputedReward(assertionId),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "InvalidTime");
|
|
});
|
|
|
|
it("Should reject claiming disputed assertions", async function () {
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
|
|
await expect(
|
|
optimisticOracle.connect(proposer).claimUndisputedReward(assertionId),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "ProposalDisputed");
|
|
});
|
|
|
|
it("Should reject claiming already claimed rewards", async function () {
|
|
// Fast forward time and claim
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
await optimisticOracle.connect(proposer).claimUndisputedReward(assertionId);
|
|
|
|
await expect(
|
|
optimisticOracle.connect(proposer).claimUndisputedReward(assertionId),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "AlreadyClaimed");
|
|
});
|
|
});
|
|
|
|
describe("Disputed Reward Claiming", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
});
|
|
|
|
it("Should allow winner to claim disputed rewards after settlement", async function () {
|
|
// Settle with proposer winning
|
|
await deciderContract.connect(owner).settleDispute(assertionId, true);
|
|
|
|
const initialBalance = await ethers.provider.getBalance(proposer.address);
|
|
const tx = await optimisticOracle.connect(proposer).claimDisputedReward(assertionId);
|
|
const receipt = await tx.wait();
|
|
const finalBalance = await ethers.provider.getBalance(proposer.address);
|
|
|
|
// Check that proposer received the reward (reward + bond - gas costs)
|
|
const expectedReward = reward * 3n;
|
|
const gasCost = receipt!.gasUsed * receipt!.gasPrice!;
|
|
expect(finalBalance - initialBalance + gasCost).to.equal(expectedReward);
|
|
});
|
|
|
|
it("Should allow disputer to claim when they win", async function () {
|
|
// Settle with disputer winning
|
|
await deciderContract.connect(owner).settleDispute(assertionId, false);
|
|
|
|
const initialBalance = await ethers.provider.getBalance(disputer.address);
|
|
const tx = await optimisticOracle.connect(disputer).claimDisputedReward(assertionId);
|
|
const receipt = await tx.wait();
|
|
const finalBalance = await ethers.provider.getBalance(disputer.address);
|
|
|
|
// Check that disputer received the reward
|
|
const expectedReward = reward * 3n;
|
|
const gasCost = receipt!.gasUsed * receipt!.gasPrice!;
|
|
expect(finalBalance - initialBalance + gasCost).to.equal(expectedReward);
|
|
});
|
|
|
|
it("Should reject claiming before settlement", async function () {
|
|
await expect(optimisticOracle.connect(proposer).claimDisputedReward(assertionId)).to.be.revertedWithCustomError(
|
|
optimisticOracle,
|
|
"AwaitingDecider",
|
|
);
|
|
});
|
|
|
|
it("Should reject claiming already claimed rewards", async function () {
|
|
await deciderContract.connect(owner).settleDispute(assertionId, true);
|
|
await optimisticOracle.connect(proposer).claimDisputedReward(assertionId);
|
|
|
|
await expect(optimisticOracle.connect(proposer).claimDisputedReward(assertionId)).to.be.revertedWithCustomError(
|
|
optimisticOracle,
|
|
"AlreadyClaimed",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Refund Claiming", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
});
|
|
|
|
it("Should allow asserter to claim refund for assertions without proposals", async function () {
|
|
// Fast forward time past dispute window
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const initialBalance = await ethers.provider.getBalance(asserter.address);
|
|
const tx = await optimisticOracle.connect(asserter).claimRefund(assertionId);
|
|
const receipt = await tx.wait();
|
|
const finalBalance = await ethers.provider.getBalance(asserter.address);
|
|
|
|
// Check that asserter received the refund (reward - gas costs)
|
|
const gasCost = receipt!.gasUsed * receipt!.gasPrice!;
|
|
expect(finalBalance - initialBalance + gasCost).to.equal(reward);
|
|
});
|
|
|
|
it("Should reject refund claiming for assertions with proposals", async function () {
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
|
|
await expect(optimisticOracle.connect(asserter).claimRefund(assertionId)).to.be.revertedWithCustomError(
|
|
optimisticOracle,
|
|
"AssertionProposed",
|
|
);
|
|
});
|
|
|
|
it("Should reject claiming already claimed refunds", async function () {
|
|
// Fast forward time and claim
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
await optimisticOracle.connect(asserter).claimRefund(assertionId);
|
|
|
|
await expect(optimisticOracle.connect(asserter).claimRefund(assertionId)).to.be.revertedWithCustomError(
|
|
optimisticOracle,
|
|
"AlreadyClaimed",
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Checkpoint6", function () {
|
|
describe("Dispute Settlement", function () {
|
|
let assertionId: bigint;
|
|
let description: string;
|
|
let reward: bigint;
|
|
|
|
beforeEach(async function () {
|
|
description = "Will Bitcoin reach $1m by end of 2026?";
|
|
reward = ethers.parseEther("1");
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
assertionId = parsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
});
|
|
|
|
it("Should allow decider to settle disputed assertions", async function () {
|
|
const resolvedOutcome = true;
|
|
const tx = await deciderContract.connect(owner).settleDispute(assertionId, resolvedOutcome);
|
|
|
|
expect(tx)
|
|
.to.emit(optimisticOracle, "AssertionSettled")
|
|
.withArgs(assertionId, resolvedOutcome, proposer.address);
|
|
|
|
// Check that the assertion was settled correctly by checking the state
|
|
const state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Settled); // Settled state
|
|
});
|
|
|
|
it("Should reject settlement by non-decider", async function () {
|
|
const resolvedOutcome = true;
|
|
|
|
await expect(
|
|
optimisticOracle.connect(otherUser).settleAssertion(assertionId, resolvedOutcome),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "OnlyDecider");
|
|
});
|
|
|
|
it("Should reject settling undisputed assertions", async function () {
|
|
// Create a new undisputed assertion
|
|
const newDescription = "Will Ethereum reach $10k by end of 2024?";
|
|
const newTx = await optimisticOracle.connect(asserter).assertEvent(newDescription, 0, 0, { value: reward });
|
|
const newReceipt = await newTx.wait();
|
|
const newEvent = newReceipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const newParsedEvent = optimisticOracle.interface.parseLog(newEvent as any);
|
|
const newAssertionId = newParsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(newAssertionId, true, { value: bond });
|
|
|
|
const resolvedOutcome = true;
|
|
await expect(
|
|
deciderContract.connect(owner).settleDispute(newAssertionId, resolvedOutcome),
|
|
).to.be.revertedWithCustomError(optimisticOracle, "NotDisputedAssertion");
|
|
});
|
|
});
|
|
|
|
describe("State Management", function () {
|
|
it("Should return correct states for different scenarios", async function () {
|
|
const description = "Will Bitcoin reach $1m by end of 2026?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
// Invalid state for non-existent assertion
|
|
let state = await optimisticOracle.getState(999n);
|
|
expect(state).to.equal(State.Invalid); // Invalid
|
|
|
|
// Asserted state
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Asserted); // Asserted
|
|
|
|
// Proposed state
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Proposed); // Proposed
|
|
|
|
// Disputed state
|
|
await optimisticOracle.connect(disputer).disputeOutcome(assertionId, { value: bond });
|
|
state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Disputed); // Disputed
|
|
|
|
// Settled state (after decider resolution)
|
|
await deciderContract.connect(owner).settleDispute(assertionId, true);
|
|
state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Settled); // Settled
|
|
});
|
|
|
|
it("Should show settled state for claimable uncontested assertions", async function () {
|
|
const description = "Will Ethereum reach $10k by end of 2024?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
const bond = reward * 2n;
|
|
await optimisticOracle.connect(proposer).proposeOutcome(assertionId, true, { value: bond });
|
|
|
|
// Fast forward time past dispute window
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Settled); // Settled (can be claimed)
|
|
});
|
|
|
|
it("Should show expired state for assertions without proposals after deadline", async function () {
|
|
const description = "Will Ethereum reach $10k by end of 2024?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
// Fast forward time past dispute window without any proposal
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
const state = await optimisticOracle.getState(assertionId);
|
|
expect(state).to.equal(State.Expired); // Expired
|
|
});
|
|
|
|
it("Should revert getResolution for expired assertions without proposals", async function () {
|
|
const description = "Will Ethereum reach $10k by end of 2024?";
|
|
const reward = ethers.parseEther("1");
|
|
|
|
const tx = await optimisticOracle.connect(asserter).assertEvent(description, 0, 0, { value: reward });
|
|
const receipt = await tx.wait();
|
|
const event = receipt!.logs.find(
|
|
log => optimisticOracle.interface.parseLog(log as any)?.name === "EventAsserted",
|
|
);
|
|
const parsedEvent = optimisticOracle.interface.parseLog(event as any);
|
|
const assertionId = parsedEvent!.args[0];
|
|
|
|
// Fast forward time past assertion window without any proposal
|
|
await ethers.provider.send("evm_increaseTime", [181]);
|
|
await ethers.provider.send("evm_mine");
|
|
|
|
// getResolution should revert because no proposal was ever made
|
|
// (expired assertions without proposals have no valid resolution)
|
|
await expect(optimisticOracle.getResolution(assertionId)).to.be.revertedWithCustomError(
|
|
optimisticOracle,
|
|
"NotProposedAssertion",
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|