Confidential Smart Contracts & Building w/Oasis Sapphire
tosynthegeek
Posted on May 6, 2024
- Introduction: Confidential Smart Contracts & Oasis Network
- Building a Sapphire Native dApp
- Conclusion
Introduction: Confidential Smart Contract & Oasis Network
As blockchain adoption expands across diverse industries, maintaining transaction inputs and contract states private from unauthorized access becomes crucial for safeguarding on-chain privacy. This enables the potential to strike a balance between the inherent transparency of public blockchains and the privacy control offered by confidential smart contracts, ultimately paving the way for widespread adoption in real-world applications.
In this article, we will dive into confidential smart contracts and how we can build with Oasis Sapphire.
Confidential Smart Contract: A Quick Overview
Confidential smart contracts are self-executing programs on a blockchain that can keep data involved in the agreement private, even from unauthorized parties. This is achieved through user-defined permissions that control which data is encrypted and who can access it.
While confidential smart contracts represent a paradigm shift, not every contract or application requires the same level of confidentiality. To address this, Oasis introduces the key design element of customizability. This allows developers to tailor contracts to meet the specific privacy requirements of each use case, ensuring that only the necessary data is kept confidential.
Oasis Network: Smart Privacy for Web3
Oasis Network is a privacy-first, layer-1 blockchain that provides a unique architecture and infrastructure for building confidential and secure applications.
Through its layered architecture, separating consensus and execution into two layers, the Consensus and the Paratime layer, Oasis is able to offer a high degree of scalability, through parallel processing and versatility through the ability to develop specialized ParaTimes.
The Consensus layer is a scalable, high-throughput, secure, proof-of-stake consensus run by a decentralized set of validator nodes.
The Paratimes layer hosts many parallel runtimes (ParaTimes), each representing a replicated compute environment with a shared state.
Building a Sapphire Native dApp
We would be building an auction to demonstrate the difference between confidential and non-confidential computation by analyzing the working of a Solidity smart contract on the Ethereum Sepolia Testnet and the Oasis Sapphire Testnet.
Prerequisites
Before we dive in, ensure you have the following tools and prerequisites in place:
- Node.js
- Hardhat
- Metamask wallet: Configure metamask to connect to Sepolia network and Sapphire testnet to your metamask
- Alchemy or Infura: Get your HTTP endpoint for the Sepolia testnet. Here is a guide on how to set it up.
- Test tokens: Request for Sepolia test and Sapphire test tokens
Set up environment
Now that we've gathered our tools, it's time to set up our development environment. Here's a step-by-step guide:
- Start by running the following commands:
mkdir auction
cd auction
npm init -y
npm install --save-dev hardhat (We will be using typescript)
npx hardhat init
npm install --save-dev @nomicfoundation/hardhat-toolbox @oasisprotocol/sapphire-hardhat
npm i dotenv
code .
- To keep sensitive information like your Metamask private key and RPC URL secure, create a .env file in your project directory and store your keys there in the format below:
- Modify your Hardhat configuration file (hardhat.config.ts) to recognize the keys from your .env file. Also, add sepolia and Sapphire testnet as networks
import { HardhatUserConfig } from "hardhat/config";
import "@oasisprotocol/sapphire-hardhat";
import "@nomicfoundation/hardhat-toolbox";
require("dotenv").config();
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
"sapphire-testnet": {
url: "https://testnet.sapphire.oasis.io",
accounts: [process.env.PRIVATE_KEY],
chainId: 0x5aff,
},
sepolia: {
url: process.env.ALCHEMY_API_KEY_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
};
export default config;
Build and Compile your Contract
In the /contracts
directory of your Hardhat project, create a new file named Auction.sol and add the following code to it:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@oasisprotocol/sapphire-contracts/contracts/Sapphire.sol";
contract Auction {
// Auction details
address payable public seller; // Address of the seller
uint256 public startingPrice; // Starting price of the auction
uint256 public highestBid; // Highest bid amount
address public highestBidder; // Address of the highest bidder
uint256 public auctionEndTime; // Timestamp when the auction ends
bool public auctionEnded; // Flag to indicate if the auction has ended
uint256 public bidCount;
// Struct to store bid details
struct Bid {
address bidder;
uint256 bid;
}
Bid[] public bids; // Array to store all bids
// Events for auction lifecycle
event BidCreated(address bidder, uint256 amount);
event AuctionEnded(address winner, uint256 winningBid);
// Constructor to initialize auction parameters
constructor(
address payable _seller, // Address of the seller
uint256 _startingPrice, // Starting price of the auction
uint256 _auctionEndTime // Duration of the auction
) {
seller = _seller;
startingPrice = _startingPrice;
highestBid = startingPrice;
auctionEndTime = block.timestamp + _auctionEndTime; // Set the end time of the auction
}
/**
* @notice Allows a user to place a bid in the auction.
* @dev The bid must be higher than the current highest bid, and the auction must not have ended.
*/
function bid() external payable {
require(block.timestamp < auctionEndTime, "Auction has ended");
require(
msg.value > highestBid,
"Bid must be higher than the current highest bid"
);
if (highestBidder != address(0)) {
payable(highestBidder).transfer(highestBid);
}
highestBid = msg.value;
highestBidder = msg.sender;
Bid memory newBid = Bid(msg.sender, msg.value);
bids.push(newBid);
bidCount++;
emit BidCreated(msg.sender, msg.value);
}
/**
* @notice Allows the highest bidder or seller to check the bid at a given index.
* @dev The auction must have ended, and only the highest bidder or seller can check bids.
* @param index The index of the bid to check.
* @return The bidder's address and bid amount.
*/
function checkBid(uint256 index) external view returns (address, uint256) {
require(index <= bidCount, "Wrong Index");
Bid memory getBid = bids[index];
require(block.timestamp > auctionEndTime, "Auction is still ongoing");
require(
msg.sender == highestBidder || msg.sender == seller,
"Only Highest Bidder and seller can check bids"
);
return (getBid.bidder, getBid.bid);
}
/**
* @notice Returns the timestamp when the auction ends.
* @return The timestamp when the auction ends.
*/
function checkAuctionEndTime() external view returns (uint256) {
return auctionEndTime;
}
/**
* @notice Ends the auction and transfers the highest bid amount to the seller.
* @dev The auction must have ended.
*/
function endAuction() external {
require(block.timestamp >= auctionEndTime, "Auction is still ongoing");
auctionEnded = true; // Update auction status
seller.transfer(highestBid); // Transfer the highest bid to the seller
emit AuctionEnded(highestBidder, highestBid); // Emit AuctionEnded event
}
/**
* @notice Returns the auction details.
* @return The seller's address, starting price, highest bid amount, highest bidder's address,
* auction end timestamp, auction end status, and total bid count.
*/
function getAuctionDetails()
external
view
returns (address, uint256, uint256, address, uint256, bool, uint256)
{
return (
seller,
startingPrice,
highestBid,
highestBidder,
auctionEndTime,
auctionEnded,
bidCount
);
}
}
The Auction
contract implemets a simple auction system that allows users to bid on an item with the highest bid winning the auction. An auction is initialized with essential parameters like sellers address, starting price and duration. Bidders can participate by placing bids higher than the current highest bid until the auction's predetermined end time. The contract has a checkBid
function which offers a limited form of access control, allowing only the highest bidder or the seller to access the details of a specific bid after the auction ends. This restriction prevents other users from viewing bid amounts or bidder information for bids they weren't directly involved in.
Overall, this is a basic auction contract, you can find the contract code here.
Compile your contract by running this command: npx hardhat compile
npx hardhat compile
Compiled 1 Solidity file successfully
Deploying and Interacting with the Auction Contract
Now that you have your contract compiled, the next step would be to write a script to deploy and interact with your contract. In the /scripts
folder, you'll find a default script, deploy.ts
, you can delete that. Go ahead and create a new file called run-auction.ts
and add the following code into it:
import { ethers } from "hardhat";
/**
* @notice Retrieves the storage data at a specific slot for a given address.
* @param address The address of the contract.
* @param slotNumber The slot number in hexadecimal format.
* @returns The storage data at the specified slot as a string.
*/
async function getStorageAt(address: string, slotNumber: string) {
const provider = ethers.provider;
const result = await provider.getStorage(address, slotNumber);
return result.toString();
}
/**
* @notice Decode the transaction input data using the ABI.
* @param abi The ABI of the contract.
* @param inputData The input data of the transaction.
* @returns The decoded transaction data, or an empty object if decoding fails.
*/
function decodeTransactionInput(abi: any[], inputData: string) {
try {
const iauction = new ethers.Interface(abi);
const decodedData = iauction.parseTransaction({ data: inputData });
return decodedData;
} catch (error) {
console.error("Error decoding transaction input:");
return { args: [] };
}
}
/**
* @notice Main function to interact with the Auction contract.
* @param value The bid amount to place in the auction.
*/
async function main(value: number) {
const index = 0;
const address = "0xDA01D79Ca36b493C7906F3C032D2365Fb3470aEC";
// Deploying the Auction contract
const Auction = await ethers.getContractFactory("Auction");
const auction = await Auction.deploy(
"0xd109e8c395741b4b3130e3d84041f8f62af765ef",
100,
60 // 10 minutes for the auction duration
);
console.log("Auction contract deployed to: ", await auction.getAddress());
// Placing a bid in the auction
console.log("Bidding....");
const tx = await auction.bid({
value: value.toString(),
});
await tx.wait();
console.log("Bid successful!");
// Checking a bid at a specific index
try {
console.log("Checking bid at Index: ", index);
const bid = await auction.checkBid(index);
console.log("Bid at Index 0 is:", bid);
} catch (error) {
console.error("Failed to check bid: Auction is still ongoing");
}
// Waiting for the auction to end
console.log("Waiting....");
const endTime = await auction.checkAuctionEndTime();
console.log("Auction endtime is: ", endTime);
console.log("Still waiting....");
try {
await new Promise((resolve) => setTimeout(resolve, 100_000));
console.log("Checking bid again");
const bid = await auction.checkBid(index);
console.log("Bid:", bid);
} catch (error) {
console.log("Failed to check bid: Auction is still ongoing");
}
// Decoding the transaction input data
const decodedInput = decodeTransactionInput(
auction.interface.format(),
tx.data
);
console.log("Decoded data input: ", decodedInput?.args);
// Retrieving storage data at a specific slot
const StateData = await getStorageAt(await auction.getAddress(), "0x0");
console.log("State data at slot 0x0 is: ", StateData);
}
main(120).catch((error) => {
console.error(error);
process.exitCode = 1;
});
This script demonstrates how to interact with the auction contract on by performing several actions:
- Deployment: Deploys the contract, initializing it with the starting price and auction duration.
- Bidding: Places a bid in the auction, confirming the transaction hash.
- Bid Access Control: Attempts to check a specific bid while the auction is ongoing, showcasing the access restriction that only allows the highest bidder or seller to view bid details during the auction. It then successfully checks the bid details after the auction ends.
- Transaction Input Decoding: Decodes the transaction input data used for checking the bid, extracting relevant information like the bid amount.
- State Retrieval Simulation: Simulates retrieving state data at a specific slot.
Deploying to Sepolia
To deploy to sepolia, run the following command:
npx hardhat run scripts/run-auction.ts --network sepolia
The script will produce the following output:
Auction contract deployed to: 0xf1D32C3e2a084aE8694151D66DA647416ed54871
Bidding....
Bid successful!
Checking bid at Index: 0
Failed to check bid: Auction is still ongoing
Waiting....
Auction endtime is: 1714989132n
Still waiting....
Checking bid again
Bid: Result(2) [ '0xDA01D79Ca36b493C7906F3C032D2365Fb3470aEC', 120n ]
Decoded data input: Result(0) []
State data at slot 0x0 is: 0x000000000000000000000000d109e8c395741b4b3130e3d84041f8f62af765ef
Deploying to Sapphire
To deploy to sepolia, run the following command:
npx hardhat run scripts/run-auction.ts --network sapphire-testnet
The script will produce the following output:
Auction contract deployed to: 0x17a8FdB2526bd5d2049EF5D7A57ff9b54628b67f
Bidding....
Bid successful!
Checking bid at Index: 0
Failed to check bid: Auction is still ongoing
Waiting....
Auction endtime is: 1714987471n
Still waiting....
Checking bid again
Bid: Result(2) [ '0xDA01D79Ca36b493C7906F3C032D2365Fb3470aEC', 120n ]
Decoded data input: undefined
State data at slot 0x0 is: 0x0
Comparing the results on Sepolia and Sapphire testnets reveals key differences that highlight the presence of confidential data on Sapphire:
- Decoding: The script successfully decoded the transaction input data on Sepolia, indicating a standard transaction format. However, on Sapphire, the decoding failed, returning "undefined," suggesting the presence of confidential data inaccessible to standard decoding tools.
- State Data: While both networks retrieved data at slot 0x0, the values differed. On Sepolia, the retrieved data contained a specific value, while on Sapphire, it was "0x0." This potentially indicates that the actual auction data on Sapphire is stored in a confidential slot.
These contrasting results demonstrate the fundamental difference between public and confidential networks. Sepolia operates with standard data formats, while Sapphire utilizes confidential storage for specific data, requiring specialized tools like the Oasis SDK for interaction. This comparison effectively showcases the presence and impact of confidentiality features on the Sapphire testnet.
Conclusion
You have successfully built and deployed your first dApp on Sapphire experiencing the power of confidential computation. This journey likely provided valuable insights into the core principles of confidential smart contracts and how they can be implemented to safeguard sensitive data on the blockchain.
As you delve deeper into confidential computation, consider exploring the following resources to broaden your understanding and explore its potential:
- Oasis Network Documentation: Dive into the technical specifications and functionalities of the Oasis Network and its confidential computing capabilities. https://docs.oasis.io/
- Oasis Community: Join the Oasis developer community to share knowledge, collaborate on projects, and stay updated on the latest developments in the community. https://oasisprotocol.org/
- Read on What You Can Build with Sapphire
- Check out the Oasis Blog
Posted on May 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.