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:
After successfully installing the dependencies, proceed to create a basic Hardhat project. In the previously created coin_pusher folder, run the following command:
npxhardhatinit
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: MITpragmasolidity ^0.8.20;import"@sight-oracle/contracts/Oracle/Types.sol";import"@sight-oracle/contracts/Oracle/Oracle.sol";import"@sight-oracle/contracts/Oracle/ResponseResolver.sol";enumState { Initial, Launching,// is setting target Launched,// target set done, user can play Completed,// Revealing, Revealed}// The FHE Coin Pusher ContractcontractCoinPusherisOwnable2Step {// Use Sight Oracle's RequestBuilder and ResponseResolver to interact with Sight OracleusingRequestBuilderforRequest;usingResponseResolverforCapsulatedValue;eventTargetSet(uint64 min, uint64 max);eventDeposit(addressindexed requester, uint64indexed amount, uint64 sum);eventDepositConfirmed(addressindexed requester,uint64indexed amount,uint64 sum );eventTargetRevealed(uint64 target);eventGameComplete(addressindexed winner, uint64 sum); Oracle public oracle; State private _state;uint64 _min;uint64 _max; CapsulatedValue private _encrypted_target;uint64private _plaintext_target; // not revealed until game ends, in micro-ether unituint64private _sum; // in micro-ether unitaddressprivate _winner;mapping(address=>uint64) internal deposits; // in micro-ether unitmapping(address=>bool) internal users; // can only deposit oncemapping(bytes32=>bytes) requestExtraData;structGameStatus {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; }functionsetTarget(uint64 min,uint64 max) privateonlyOwner {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 thisfunctionsetTarget_cb(bytes32/* requestId */,CapsulatedValue[] memoryEVs ) publiconlyOracle {// Decode value from Oracle callback _encrypted_target = EVs[EVs.length -1]; _state = State.Launched;emitTargetSet(_min, _max); }// user make a depositfunctiondeposit(uint8 flag) publicpayable {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.001ether; } elseif (flag ==1) { amount =5000; // This could represent 5000 microether (0.005 ETH) value =0.005ether; } elseif (flag ==2) { amount =10000; // This could represent 10000 microether (0.01 ETH) value =0.01ether; } else {revert("only flag 1 - small, 2 - medium, 3 - large allowed"); }// Check if the sent value matches the required amountrequire(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);emitDeposit(msg.sender, amount, _sum); }// only Oracle can call thisfunctiondeposit_cb(bytes32 requestId,CapsulatedValue[] memoryEVs ) publiconlyOracle {if (_state != State.Launched) {return; }bytesmemory extraData = requestExtraData[requestId]; (address requester,uint64 amount,uint64 sum) = abi.decode( extraData, (address,uint64,uint64) );emitDepositConfirmed(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 herebool isWinner = EVs[EVs.length -1].asBool();if (isWinner) { _winner = requester; _state = State.Completed;emitGameComplete(_winner, _sum);revealTarget(); } }// Reveal the targetfunctionrevealTarget() 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 thisfunctionrevealTarget_cb(bytes32/* requestId */,CapsulatedValue[] memoryEVs ) publiconlyOracle { CapsulatedValue memory wrapped_plaintext_target = EVs[EVs.length -1];// unwrap the plaintext value _plaintext_target = wrapped_plaintext_target.asUint64(); _state = State.Revealed;emitTargetRevealed(_plaintext_target); }modifieronlyOracle() {require(msg.sender ==address(oracle),"Only Oracle Can Do This"); _; }functiondepositOf(address addr) publicviewreturns (uint64) {return deposits[addr]; }functiongetGameStatus() publicviewreturns (GameStatusmemory) {returnGameStatus({ isComplete: _state == State.Completed, winner: _winner, target: _plaintext_target, state: _state, sum: _sum }); }functionwithdraw() externalonlyOwner {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() externalpayable {}receive() externalpayable {}}
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.
enumState { 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.
contractCoinPusherisOwnable2Step {// Use Sight Oracle's RequestBuilder and ResponseResolver to interact with Sight OracleusingRequestBuilderforRequest;usingResponseResolverforCapsulatedValue;
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.
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;uint64private _plaintext_target; // not revealed until game ends, in micro-ether unituint64private _sum; // in micro-ether unitaddressprivate _winner;mapping(address=>uint64) internal deposits; // in micro-ether unitmapping(address=>bool) internal users; // can only deposit oncemapping(bytes32=>bytes) requestExtraData;structGameStatus {bool isComplete;address winner;uint64 target; State state;uint64 sum;}
This code defines several state variables:
oracle is an instance of Oracle 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.
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
functionsetTarget(uint64 min,uint64 max) privateonlyOwner {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 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
functiondeposit(uint8 flag) publicpayable {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.001ether; } elseif (flag ==1) { amount =5000; // This could represent 5000 microether (0.005 ETH) value =0.005ether; } elseif (flag ==2) { amount =10000; // This could represent 10000 microether (0.01 ETH) value =0.01ether; } else {revert("only flag 1 - small, 2 - medium, 3 - large allowed"); }// Check if the sent value matches the required amountrequire(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 Oraclebytes32 lastReqId = oracle.send(r); requestExtraData[lastReqId] = abi.encode(msg.sender, amount, _sum);emitDeposit(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.
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
functionrevealTarget() 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.
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!).
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 a ModuleBuilder, 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.
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.