Contract
Prerequisites
Create Hardhat Project
Create a new folder named coin_pusher
in the root directory to serve as the contract project folder. Then, navigate into this folder and install the required dependencies, Hardhat
and Sight Oracle
, by running the following commands:
cd coin_pusher
npm install --save-dev hardhat @sight-oracle/contracts dotenv
After successfully installing the dependencies, proceed to create a basic Hardhat project. In the previously created coin_pusher
folder, run the following command:
npx hardhat init
Letâs choose create a JavaScript
or TypeScript
project. We recommend choosing TypeScript, but if youâre not familiar with it, simply select JavaScript.

Writing Contract
Copy the following code into /contracts/CoinPusher.sol
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@sight-oracle/contracts/Oracle/Types.sol";
import "@sight-oracle/contracts/Oracle/Oracle.sol";
import "@sight-oracle/contracts/Oracle/ResponseResolver.sol";
enum State {
Initial,
Launching, // is setting target
Launched, // target set done, user can play
Completed, //
Revealing,
Revealed
}
// The FHE Coin Pusher Contract
contract CoinPusher is Ownable2Step {
// Use Sight Oracle's RequestBuilder and ResponseResolver to interact with Sight Oracle
using RequestBuilder for Request;
using ResponseResolver for CapsulatedValue;
event TargetSet(uint64 min, uint64 max);
event Deposit(address indexed requester, uint64 indexed amount, uint64 sum);
event DepositConfirmed(
address indexed requester,
uint64 indexed amount,
uint64 sum
);
event TargetRevealed(uint64 target);
event GameComplete(address indexed winner, uint64 sum);
Oracle public oracle;
State private _state;
uint64 _min;
uint64 _max;
CapsulatedValue private _encrypted_target;
uint64 private _plaintext_target; // not revealed until game ends, in micro-ether unit
uint64 private _sum; // in micro-ether unit
address private _winner;
mapping(address => uint64) internal deposits; // in micro-ether unit
mapping(address => bool) internal users; // can only deposit once
mapping(bytes32 => bytes) requestExtraData;
struct GameStatus {
bool isComplete;
address winner;
uint64 target;
State state;
uint64 sum;
}
constructor(address oracle_, uint64 min, uint64 max) payable {
oracle = Oracle(payable(oracle_));
setTarget(min, max);
_state = State.Launching;
// initialize status
_winner = address(0);
_sum = 0;
}
function setTarget(uint64 min, uint64 max) private onlyOwner {
require(_state != State.Launching, "Game is not complete!");
require(max > min, "require max > min");
// clear up
_plaintext_target = 0;
_min = min;
_max = max;
// Initialize new FHE computation request of 3 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
3,
address(this),
this.setTarget_cb.selector,
""
);
uint64 range = max - min + 1;
uint64 shards = (type(uint64).max / range + 1);
// Step 1: generate random value
op encryptedValueA = r.rand();
// Step 2 - 3: limit the random value into range min - max
op scaled_random_value = r.div(encryptedValueA, shards);
r.add(scaled_random_value, min);
// Send the request via Sight FHE Oracle
oracle.send(r);
}
// only Oracle can call this
function setTarget_cb(
bytes32 /* requestId */,
CapsulatedValue[] memory EVs
) public onlyOracle {
// Decode value from Oracle callback
_encrypted_target = EVs[EVs.length - 1];
_state = State.Launched;
emit TargetSet(_min, _max);
}
// user make a deposit
function deposit(uint8 flag) public payable {
require(
_state == State.Launched,
"Game not launched or Game already Completed."
);
require(users[msg.sender] == false, "You have already deposited");
uint64 amount = 0;
uint256 value;
if (flag == 0) {
amount = 1000; // This could represent 1000 microether (0.001 ETH)
value = 0.001 ether;
} else if (flag == 1) {
amount = 5000; // This could represent 5000 microether (0.005 ETH)
value = 0.005 ether;
} else if (flag == 2) {
amount = 10000; // This could represent 10000 microether (0.01 ETH)
value = 0.01 ether;
} else {
revert("only flag 1 - small, 2 - medium, 3 - large allowed");
}
// Check if the sent value matches the required amount
require(msg.value == value, "Incorrect payment amount.");
users[msg.sender] = true;
deposits[msg.sender] = amount;
_sum = _sum + amount;
// Initialize new FHE computation request of 3 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
3,
address(this),
this.deposit_cb.selector,
""
);
// Step 1: load local stored encrypted target into request processing context
op e_target = r.getEuint64(_encrypted_target.asEuint64());
// Step 2: compare balance and encrypted_target
op e_greater = r.ge(_sum, e_target);
// Step 3: decrypt the comparison result, it is safe to reveal
r.decryptEbool(e_greater);
// requestExtraData[r.id] = abi.encode(msg.sender, amount, _sum);
// send request to Sight FHE Oracle
oracle.send(r);
emit Deposit(msg.sender, amount, _sum);
}
// only Oracle can call this
function deposit_cb(
bytes32 requestId,
CapsulatedValue[] memory EVs
) public onlyOracle {
if (_state != State.Launched) {
return;
}
bytes memory extraData = requestExtraData[requestId];
(address requester, uint64 amount, uint64 sum) = abi.decode(
extraData,
(address, uint64, uint64)
);
emit DepositConfirmed(requester, amount, sum);
// Check winning condition
// CapsulatedValue 0: the encrypted target
// CapsulatedValue 1: the encrypted compare result
// CapsulatedValue 2: the decrypted compare result, as used here
bool isWinner = EVs[EVs.length - 1].asBool();
if (isWinner) {
_winner = requester;
_state = State.Completed;
emit GameComplete(_winner, _sum);
revealTarget();
}
}
// Reveal the target
function revealTarget() private {
require(_state == State.Completed, "Game is not complete!");
_state = State.Revealing;
// Initialize new FHE computation request of 2 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
2,
address(this),
this.revealTarget_cb.selector,
""
);
// Step 1: load encrypted target into processing context
op e_target = r.getEuint64(_encrypted_target.asEuint64());
// Step 2: decrypt the target
r.decryptEuint64(e_target);
oracle.send(r);
}
// only Oracle can call this
function revealTarget_cb(
bytes32 /* requestId */,
CapsulatedValue[] memory EVs
) public onlyOracle {
CapsulatedValue memory wrapped_plaintext_target = EVs[EVs.length - 1];
// unwrap the plaintext value
_plaintext_target = wrapped_plaintext_target.asUint64();
_state = State.Revealed;
emit TargetRevealed(_plaintext_target);
}
modifier onlyOracle() {
require(msg.sender == address(oracle), "Only Oracle Can Do This");
_;
}
function depositOf(address addr) public view returns (uint64) {
return deposits[addr];
}
function getGameStatus() public view returns (GameStatus memory) {
return
GameStatus({
isComplete: _state == State.Completed,
winner: _winner,
target: _plaintext_target,
state: _state,
sum: _sum
});
}
function withdraw() external onlyOwner {
require(
_state == State.Completed ||
_state == State.Revealing ||
_state == State.Revealed,
"Invalid state"
);
require(address(this).balance > 0, "No funds to withdraw");
payable(owner()).transfer(address(this).balance);
}
fallback() external payable {}
receive() external payable {}
}
This is the core contract code for the entire game, ensuring that the game is fair, secure, and transparent. It may seem complex, but donât worryâweâll break it down and provide a clear, detailed explanation of each section.
enum State {
Initial,
Launching, // is setting target
Launched, // target set done, user can play
Completed, //
Revealing,
Revealed
}
First, we defined an enum
type called State
, which will be used to track the gameâs status.
contract CoinPusher is Ownable2Step {
// Use Sight Oracle's RequestBuilder and ResponseResolver to interact with Sight Oracle
using RequestBuilder for Request;
using ResponseResolver for CapsulatedValue;
Ownable2Step
is a contract for ownership control that inherits from Ownable
and offers enhanced security compared to Ownable
.
The contract uses the RequestBuilder
library to construct requests and the ResponseResolver
library to interpret the responses.
event TargetSet(uint64 min, uint64 max);
event Deposit(address indexed requester, uint64 indexed amount, uint64 sum);
event DepositConfirmed(
address indexed requester,
uint64 indexed amount,
uint64 sum
);
event TargetRevealed(uint64 target);
event GameComplete(address indexed winner, uint64 sum);
Here are some Event
definitions. An Event
is a mechanism for recording changes in the contractâs state, allowing external applications to listen to and query these changes. You can use emit
to trigger an Event
.
Oracle public oracle;
State private _state;
uint64 _min;
uint64 _max;
CapsulatedValue private _encrypted_target;
uint64 private _plaintext_target; // not revealed until game ends, in micro-ether unit
uint64 private _sum; // in micro-ether unit
address private _winner;
mapping(address => uint64) internal deposits; // in micro-ether unit
mapping(address => bool) internal users; // can only deposit once
mapping(bytes32 => bytes) requestExtraData;
struct GameStatus {
bool isComplete;
address winner;
uint64 target;
State state;
uint64 sum;
}
This code defines several state variables:
oracle
is an instance ofOracle
used to handle cryptographic operations and send requests._state
tracks the current state of the game._min
and_max
represent the minimum and maximum range of the gameâs target value._encrypted_target
and_plaintext_target
store the encrypted and decrypted target values._sum
records the current total deposit amount._winner
holds the winnerâs address.deposits
keeps track of each playerâs deposit amount.users
indicates whether a user has participated in the game.requestExtraData
contains additional data for requests sent to the Oracle.GameStatus
is a data structure that simplifies checking the status of various game variables.
Constructor Function
constructor(address oracle_, uint64 min, uint64 max) payable {
oracle = Oracle(payable(oracle_));
setTarget(min, max);
_state = State.Launching;
// initialize status
_winner = address(0);
_sum = 0;
}
The constructor
function takes three parameters, which are used to initialize the basic settings and state of the game. These parameters must be provided when deploying the contract.
SetTarget Function
function setTarget(uint64 min, uint64 max) private onlyOwner {
require(_state != State.Launching, "Game is not complete!");
require(max > min, "require max > min");
// clear up
_plaintext_target = 0;
_min = min;
_max = max;
// Initialize new FHE computation request of 3 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
3,
address(this),
this.setTarget_cb.selector,
""
);
uint64 range = max - min + 1;
uint64 shards = (type(uint64).max / range + 1);
// Step 1: generate random value
op encryptedValueA = r.rand();
// Step 2 - 3: limit the random value into range min - max
op scaled_random_value = r.div(encryptedValueA, shards);
r.add(scaled_random_value, min);
// Send the request via Sight FHE Oracle
oracle.send(r);
}
The setTarget
function is called once within the constructor to initialize the target range.
The onlyOwner
modifier from the Ownable2Step
contract, restricts access to the setTarget
function, allowing only the contract owner to call it. A new request is created using RequestBuilder.newRequest
, which specifies the sender, the number of computation steps, the callback address, and the callback function. The rand
function is then called to generate a random encrypted value, which is stored in the Sight Network. To ensure the generated random value falls within the specified min and max range, the encrypted div
and add
functions are used for secure division and addition operations. Finally, the send
function submits the request to the Sight Oracle.
setTarget_cb function
function setTarget_cb(
bytes32 /* requestId */,
CapsulatedValue[] memory EVs
) public onlyOracle {
// Decode value from Oracle callback
_encrypted_target = EVs[EVs.length - 1];
_state = State.Launched;
emit TargetSet(_min, _max);
}
setTarget_cb
is a callback function (with âcbâ standing for callback) that has an onlyOracle
modifier, meaning it can only be called by the Oracle
contract. EVs[EVs.length - 1]
will retrieve the last element from the EVs
array and assign it to _encrypted_target
, which means storing the encrypted value returned from the Oracle in _encrypted_target
. Then, the _state
is updated to Launched
, indicating that the game has started. Finally, the TargetSet
event is triggered to notify listeners that the target range has been set.
deposit function
function deposit(uint8 flag) public payable {
require(
_state == State.Launched,
"Game not launched or Game already Completed."
);
require(users[msg.sender] == false, "You have already deposited");
uint64 amount = 0;
uint256 value;
if (flag == 0) {
amount = 1000; // This could represent 1000 microether (0.001 ETH)
value = 0.001 ether;
} else if (flag == 1) {
amount = 5000; // This could represent 5000 microether (0.005 ETH)
value = 0.005 ether;
} else if (flag == 2) {
amount = 10000; // This could represent 10000 microether (0.01 ETH)
value = 0.01 ether;
} else {
revert("only flag 1 - small, 2 - medium, 3 - large allowed");
}
// Check if the sent value matches the required amount
require(msg.value == value, "Incorrect payment amount.");
users[msg.sender] = true;
deposits[msg.sender] = amount;
_sum = _sum + amount;
// Initialize new FHE computation request of 3 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
3,
address(this),
this.deposit_cb.selector,
""
);
// Step 1: load local stored encrypted target into request processing context
op e_target = r.getEuint64(_encrypted_target.asEuint64());
// Step 2: compare balance and encrypted_target
op e_greater = r.ge(_sum, e_target);
// Step 3: decrypt the comparison result, it is safe to reveal
r.decryptEbool(e_greater);
// send request to Sight FHE Oracle
bytes32 lastReqId = oracle.send(r);
requestExtraData[lastReqId] = abi.encode(msg.sender, amount, _sum);
emit Deposit(msg.sender, amount, _sum);
}
The deposit
function is used to handle user deposits behavior. When calling deposit
, you need to pass in a parameter of 0, 1, or 2, and send the corresponding amount of Ether. These parameters correspond to three different deposit amounts. Once the player's deposit is confirmed successful, the user is marked as having deposited, and the user's deposit amount and the total deposit amount _sum
are updated. Then, a request is initialized through RequestBuilder.newRequest
. The getEuint64
function will load the locally stored encrypted target value into the request's processing context, and the ge
function will compare _sum
with e_target
. ge
stands for greater than or equal to. If _sum
is greater than or equal to e_target
, the result will be true; otherwise, it will be false. decryptEbool
is used to decrypt the encrypted boolean value. Finally, record some information about the request in requestExtraData
.
deposit_cb function
function deposit_cb(
bytes32 requestId,
CapsulatedValue[] memory EVs
) public onlyOracle {
if (_state != State.Launched) {
return;
}
bytes memory extraData = requestExtraData[requestId];
(address requester, uint64 amount, uint64 sum) = abi.decode(
extraData,
(address, uint64, uint64)
);
emit DepositConfirmed(requester, amount, sum);
// Check winning condition
// CapsulatedValue 0: the encrypted target
// CapsulatedValue 1: the encrypted compare result
// CapsulatedValue 2: the decrypted compare result, as used here
bool isWinner = EVs[EVs.length - 1].asBool();
if (isWinner) {
_winner = requester;
_state = State.Completed;
emit GameComplete(_winner, _sum);
revealTarget();
}
}
First, retrieve the extraData
corresponding to the requestId from requestExtraData
. Then, decode the extraData
to obtain information about the deposit request, and trigger the DepositConfirmed
event to indicate that the deposit transaction has been successfully completed. The line of code EVs[EVs.length - 1].asBool()
retrieves the last CapsulatedValue from the EVs
array and assigns its boolean value to isWinner
. If isWinner
is true, it means the sender of this request has won the competition. Therefore, update _winner
to requester
and change the game's status to Completed
. Finally, call revealTarget
to convert the encrypted target value into plaintext.
revealTarget function
function revealTarget() private {
require(_state == State.Completed, "Game is not complete!");
_state = State.Revealing;
// Initialize new FHE computation request of 2 steps.
Request memory r = RequestBuilder.newRequest(
msg.sender,
2,
address(this),
this.revealTarget_cb.selector,
""
);
// Step 1: load encrypted target into processing context
op e_target = r.getEuint64(_encrypted_target.asEuint64());
// Step 2: decrypt the target
r.decryptEuint64(e_target);
oracle.send(r);
}
The revealTarget
function loads the locally stored encrypted target value _encrypted_target
into the request's processing context by using getEuint64
and assigns it to e_target
. It then decrypts e_target
by performing a decryption operation with decryptEuint64
.
revealTarget_cb function
function revealTarget_cb(
bytes32 /* requestId */,
CapsulatedValue[] memory EVs
) public onlyOracle {
CapsulatedValue memory wrapped_plaintext_target = EVs[EVs.length - 1];
// unwrap the plaintext value
_plaintext_target = wrapped_plaintext_target.asUint64();
_state = State.Revealed;
emit TargetRevealed(_plaintext_target);
}
In the revealTarget_cb
function, the plaintext of the target
returned by the Oracle will be stored in _plaintext_target
, then the game status will be updated to Revealed
and the TargetRevealed
event will be emitted.
The above describes the core functionalities of the Coin Pusher game. There are also some functions for reading contract states, but I won't go into detail about those. Next, let's proceed to deploy the contract to the Sepolia test network.
Deploy Contract
To deploy the contract on the Sepolia test network, you first need to obtain some Sepolia test tokens and the Sepolia RPC URL.
If you donât have a Sepolia RPC yet, you can click here to require one.
Note: You can get the
private key
through MetaMask by going to Account Details => Export Private Key (Please use a test wallet, as leaking the private key poses a risk of account theft!).
PRIVATE_KEY=${YOUR_PRIVATE_KEY}
SEPOLIA_RPC_URL=${YOUR_SEPOLIA_RPC_URL}
SEPOLIA_ORACLE_CONTRACT_ADDRESS=0xC5ac65f17Ce781E9F325634b6218Dc75a5CF9abF
Add these environment variables to the .env
file. SEPOLIA_ORACLE_CONTRACT_ADDRESS
is the Oracle address deployed on the Sepolia test network.
Copy the following deployment code into /ignition/modules/CoinPusher.ts
.
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const oracleAddr = process.env.SEPOLIA_ORACLE_ADDRESS!;
const Coin_PusherModule = buildModule("Coin_PusherModule", (m) => {
const Coin_Pusher = m.contract("CoinPusher", [oracleAddr, 10000, 50000]);
return { Coin_Pusher };
});
export default Coin_PusherModule;
Explanation:
buildModule: The first aspect to note is that modules are created by calling the
buildModule
function, which requires a module ID and a callback function. Our module will be identified as"Coin_PusherModule"
.m: The
m
parameter being passed into the callback is an instance of aModuleBuilder
, which is an object with methods to define and configure your smart contract instances.oracleAddr:
oracleAddr
is the address of a Sight Oracle, used for requesting and responding to data. The address is chosen based on the network.m.contract("CoinPusher", [oracleAddr, 10000, 50000]): Deploys the CoinPusher contract using Ignition and passes the Oracle address as a constructor parameter. 10000 and 50000 are the minimum and maximum values for initializing the target.
Running the Deployment Script
npx hardhat ignition deploy ignition/modules/Example.ts --network sepolia
After a successful deployment, you will see some deployment information printed to the console, including the Example contract address.

After deploying the contract, you will find a new file named deployments/chain-11155111
under the ignition
directory. This file contains the contract's ABI and contract address, which we will need to use when building the frontend to interact with the contract.

Congratulations on completing the smart contract part of the dapp! Take a break, and then click to join us in building the frontend pages of the dapp.
Last updated