Skip to content

Commit

Permalink
feat(contract): Use DrandOracle for on-chain randomness
Browse files Browse the repository at this point in the history
  • Loading branch information
oyyblin committed Nov 4, 2024
1 parent 84230fd commit 0131198
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 102 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: foundry

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

name: Foundry tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Show Forge version
run: |
forge --version
- name: Run Forge fmt
working-directory: contracts
run: |
forge fmt --check
id: fmt

- name: Run Forge build
working-directory: contracts
run: |
forge build --sizes
id: build

- name: Run Forge tests
working-directory: contracts
run: |
forge test -vvv
id: test
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "contracts/lib/sp1-contracts"]
path = contracts/lib/sp1-contracts
url = https://github.com/succinctlabs/sp1-contracts
[submodule "contracts/lib/drand-oracle"]
path = contracts/lib/drand-oracle
url = https://github.com/Galxe/drand-oracle
1 change: 1 addition & 0 deletions contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ fs_permissions = [{ access = "read-write", path = "./" }]
remappings = [
"@openzeppelin/=lib/openzeppelin-contracts/",
"@sp1-contracts/=lib/sp1-contracts/contracts/src/",
"@drand-oracle/=lib/drand-oracle/contracts/src/",
]

[etherscan]
Expand Down
1 change: 1 addition & 0 deletions contracts/lib/drand-oracle
Submodule drand-oracle added at adab72
2 changes: 2 additions & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@openzeppelin/=lib/openzeppelin-contracts/
@sp1-contracts/=lib/sp1-contracts/contracts/src/
@drand-oracle/=lib/drand-oracle/contracts/src/
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
drand-oracle/=lib/drand-oracle/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
sp1-contracts/=lib/sp1-contracts/
38 changes: 27 additions & 11 deletions contracts/interface/IRaffle.sol → contracts/src/IRaffle.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;

/// @title Minimal interface for drand oracle
interface IDrandOracle {
/// @notice Structure containing randomness data and associated signatures
/// @param round The drand round number
/// @param randomness The drand round randomness value
/// @param signature The drand round signature
struct Random {
uint64 round;
uint64 timestamp;
bytes32 randomness;
bytes signature;
}

/// @notice Retrieves the complete randomness data for a specific timestamp
/// @param _timestamp The timestamp to query
/// @return The Random struct containing the timestamp's data
function getRandomnessFromTimestamp(uint64 _timestamp) external view returns (Random memory);
}

/// @title Interface for Galxe Raffle Contract
/// @notice Interface for participating in raffle quests
interface IRaffle {
// Custom Errors
error InvalidAddress();
error InvalidQuestID();
error InvalidBeacon();
error InvalidRandomness();
error InvalidSignature();
error VerifyIdAlreadyUsed(uint256 verifyId);
error QuestNotExists();
Expand All @@ -18,38 +37,35 @@ interface IRaffle {
error IncorrectProof();

// Events
event SignerUpdated(address signer);
event VerifierUpdated(address verifier);
event VkeyUpdated(bytes32 vkey);
event DrandOracleUpdated(address drandOracle);
event Participate(uint64 questID, uint256 user, uint64 verifyID);
event CommitRandomness(uint64 questID, uint64 roundID, bytes32 randomness);
event Reveal(uint64 questID, uint32 participantCount, uint32 winnerCount, bytes32 randomness, bytes32 merkleRoot);

// Structs
struct DrandBeacon {
uint64 round;
bytes32 randomness;
bytes signature;
bytes previousSignature;
}

// Functions
function pause() external;
function unpause() external;
function setSigner(address _signer) external;

function participate(uint64 _questID, uint256 _user, uint64 _verifyID, bytes calldata _signature) external;

function commitRandomness(uint64 _questID, DrandBeacon calldata _beacon, bytes calldata _signature) external;
function commitRandomness(uint64 _questID, uint64 _timestamp, bytes calldata _signature) external;

function reveal(uint64 _questID, bytes calldata _publicValues, bytes calldata _proofBytes) external;

function getQuest(uint256 _questID)
external
view
returns (bool _active, DrandBeacon memory _beacon, bytes32 _merkleRoot);
returns (bool _active, IDrandOracle.Random memory random, bytes32 _merkleRoot);

function hasParticipated(uint256 _verifyID) external view returns (bool);

// State Variables
function signer() external view returns (address);
function verifier() external view returns (address);
function vkey() external view returns (bytes32);
function drandOracle() external view returns (address);
}
86 changes: 53 additions & 33 deletions contracts/src/Raffle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {ISP1Verifier} from "@sp1-contracts/ISP1Verifier.sol";
import {IRaffle} from "../interface/IRaffle.sol";
import {IDrandOracle, IRaffle} from "./IRaffle.sol";

/// @title Galxe Raffle Contract
/// @author Galxe Team
Expand All @@ -34,7 +34,7 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
struct RaffleQuest {
uint64 questID;
bool active;
DrandBeacon beacon;
IDrandOracle.Random random;
mapping(uint256 => uint256) participantIds;
uint256 participantCount;
uint256 winnerCount;
Expand All @@ -53,6 +53,9 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
/// @dev The address of the SP1 verifier contract.
address public override verifier;

/// @dev The address of the DrandOracle contract.
address public override drandOracle;

/// @notice The verification key for the SP1 RISC-V program.
bytes32 public override vkey;

Expand All @@ -61,7 +64,7 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
/// @param _signer The signer address.
/// @param _verifier The SP1 verifier address.
/// @param _vkey The verification key for the SP1 RISC-V program.
constructor(address _initialOwner, address _signer, address _verifier, bytes32 _vkey)
constructor(address _initialOwner, address _signer, address _verifier, bytes32 _vkey, address _drandOracle)
Ownable(_initialOwner)
EIP712("Galxe Raffle", "1.0.0")
{
Expand All @@ -77,6 +80,7 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
signer = _signer;
verifier = _verifier;
vkey = _vkey;
drandOracle = _drandOracle;
}

/// @notice Pauses the contract.
Expand All @@ -96,6 +100,31 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
revert IRaffle.InvalidAddress();
}
signer = _signer;
emit IRaffle.SignerUpdated(_signer);
}

/// @notice Sets the verifier address.
/// @param _verifier The new verifier address.
function setVerifier(address _verifier) public onlyOwner {
if (_verifier == address(0)) {
revert IRaffle.InvalidAddress();
}
verifier = _verifier;
emit IRaffle.VerifierUpdated(_verifier);
}

/// @notice Sets the verification key.
/// @param _vkey The new verification key.
function setVkey(bytes32 _vkey) public onlyOwner {
vkey = _vkey;
emit IRaffle.VkeyUpdated(_vkey);
}

/// @notice Sets the DrandOracle address.
/// @param _drandOracle The new DrandOracle address.
function setDrandOracle(address _drandOracle) public onlyOwner {
drandOracle = _drandOracle;
emit IRaffle.DrandOracleUpdated(_drandOracle);
}

/// @notice Participates in the raffle reward quest.
Expand All @@ -111,6 +140,10 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
revert IRaffle.InvalidQuestID();
}

if (_user == 0) {
revert IRaffle.InvalidAddress();
}

if (hasParticipated(_verifyID)) {
revert IRaffle.VerifyIdAlreadyUsed(_verifyID);
}
Expand Down Expand Up @@ -141,9 +174,8 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {

/// @notice Commits the randomness for the quest.
/// @param _questID The quest ID.
/// @param _beacon The drand beacon.
/// @param _signature The signature.
function commitRandomness(uint64 _questID, DrandBeacon calldata _beacon, bytes calldata _signature) public {
function commitRandomness(uint64 _questID, uint64 _timestamp, bytes calldata _signature) public {
RaffleQuest storage quest = quests[_questID];

if (quest.questID == 0) {
Expand All @@ -154,26 +186,25 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
revert IRaffle.QuestNotActive();
}

if (quest.beacon.randomness != bytes32(0)) {
if (quest.random.randomness != bytes32(0)) {
revert IRaffle.QuestRandomnessAlreadyCommitted();
}

if (
_beacon.round == 0 || _beacon.randomness == bytes32(0) || _beacon.signature.length == 0
|| _beacon.previousSignature.length == 0
) {
revert IRaffle.InvalidBeacon();
}

bool isVerified = _verify(_hashCommitRandomness(_questID, _beacon), _signature);
bool isVerified = _verify(_hashCommitRandomness(_questID, _timestamp), _signature);
if (!isVerified) {
revert IRaffle.InvalidSignature();
}

quest.beacon = _beacon;
IDrandOracle.Random memory random = IDrandOracle(drandOracle).getRandomnessFromTimestamp(_timestamp);

if (random.round == 0 || random.randomness == bytes32(0) || random.signature.length == 0) {
revert IRaffle.InvalidRandomness();
}

quest.random = random;
quest.active = false;

emit IRaffle.CommitRandomness(_questID, _beacon.round, _beacon.randomness);
emit IRaffle.CommitRandomness(_questID, random.round, random.randomness);
}

/// @notice Verifies the proof and reveals the raffle result.
Expand All @@ -198,7 +229,7 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
(uint32 participantCount, uint32 winnerCount, bytes32 randomness, bytes32 merkleRoot) =
abi.decode(_publicValues, (uint32, uint32, bytes32, bytes32));

if (participantCount != quest.participantCount || randomness != quest.beacon.randomness) {
if (participantCount != quest.participantCount || randomness != quest.random.randomness) {
revert IRaffle.IncorrectProof();
}

Expand All @@ -210,13 +241,13 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
}

function getQuest(uint256 _questID)
public
external
view
returns (bool _active, DrandBeacon memory _beacon, bytes32 _merkleRoot)
returns (bool _active, IDrandOracle.Random memory random, bytes32 _merkleRoot)
{
RaffleQuest storage quest = quests[_questID];
_active = quest.active;
_beacon = quest.beacon;
random = quest.random;
_merkleRoot = quest.merkleRoot;
}

Expand All @@ -238,20 +269,9 @@ contract Raffle is IRaffle, Ownable2Step, Pausable, EIP712 {
);
}

function _hashCommitRandomness(uint64 _questID, DrandBeacon calldata _beacon) private view returns (bytes32) {
function _hashCommitRandomness(uint64 _questID, uint64 _timestamp) private view returns (bytes32) {
return _hashTypedDataV4(
keccak256(
abi.encode(
keccak256(
"CommitRandomness(uint64 questID,uint64 round,bytes32 randomness,bytes signature,bytes previousSignature)"
),
_questID,
_beacon.round,
_beacon.randomness,
keccak256(_beacon.signature),
keccak256(_beacon.previousSignature)
)
)
keccak256(abi.encode(keccak256("CommitRandomness(uint64 questID,uint64 timestamp)"), _questID, _timestamp))
);
}

Expand Down
Loading

0 comments on commit 0131198

Please sign in to comment.