Build a Confidential DApp in Solidity on Sapphire

This is a sponsored article by Oasis Network

The launch of the Sapphire ParaTime on the Oasis Network is a huge step forward in scaling blockchain toward Web3. Sapphire offers the first and only confidential Ethereum Virtual Machine (EVM) compatible runtime allowing EVM builders and Solidity developers to build privacy-enabled DApps quickly.

Sapphire is a leap forward for blockchain and Web3 development. It is a runtime that takes EVM's benefits while allowing creators to build confidential DApps with confidential smart contracts. Tools like confidential smart contracts will scale blockchain into Web3.

Confidentiality is a strong prospect for protecting and empowering user privacy; governmental and legislative action already shield core pillars of Web3 (such as GDPR).

Thanks to Oasis’s unique architecture, Sapphire can run as an EVM, but with 99% fewer fees than Ethereum and much better scalability, while adding in the confidential state, end-to-end encryption, and confidential randomness offers far more use cases for a Sapphire build DApp.

D_D Newsletter CTA

The Oasis Architecture

The Oasis Network utilizes a modular architecture that separates consensus and smart contract execution into the Consensus Layer and the ParaTime Layer. We integrated both layers to provide the same functionality of a single network.

However, separating these layers allows ParaTimes to process transactions of different complexity in parallel with shared consensus. Additionally, the ParaTimes are highly flexible, customizable, and easy to deploy.

With this flexibility, workloads and upgrades processed on one ParaTime will work symphonically with consensus to ensure network security and finality without impacting other ParaTimes.

Sapphire, Emerald, and Cipher are three of the current ParaTimes in operation. Each offers a unique runtime that is suited for different developers. Sapphire sits as a jewel in the crown as it provides the EVM compatibility of Emerald with the privacy-preserving technology of Cipher.

Why Confidentiality?

Currently, a critical security component of blockchain means data executed on-chain is publicly available and transparent. Yet, we must acknowledge that sensitive data, such as financial and health information, must remain confidential if we scale to Web3.

Sapphire allows developers to build DApps that utilize confidential smart contracts where certain aspects of the smart contract can remain hidden and private.

Hiding certain aspects of smart contract execution is a powerful tool for Web3. Sapphire empowers EVM developers to add confidential elements to their dApps through confidential smart contracts, scaling their creations to Web3.

As the industry’s first confidential EVM compatible ParaTime, Sapphire is the only place for Solidity developers to build dApps to protect user privacy.

Use cases

The ability to create confidential smart contracts adds new levels of use cases to blockchain technology. It also aligns the use cases with the burgeoning Web3 movement, where data privacy is a core pillar.

Confidentiality in DeFi, for example, allows for concealing the slippage parameters and creating an MEV-resistant DEX since there would be no way for sandwich attacks to occur. Confidential DeFi can also mix well with Decentralized ID (DID). By enabling financial DID information, such as credit scores, to the blockchain, decentralized lending becomes much more viable and appealing to the mass market.

Gaming can benefit hugely from confidentiality too. Card games can remain on-chain to inherit the blockchain’s integrity while keeping each player’s hand hidden.

NFTs can also benefit from confidentiality as issues like gas wars still plague the space. By enabling confidentiality, more sophisticated auction mechanisms could be deployed, such as second price auctions or candlestick auctions, allowing for a much less chaotic and more fair minting and auction mechanism for NFT sales.

Porting an Ethereum Project

In this tutorial, you'll port an Ethereum project to deploy a unique DApp that requires confidentiality to work. By the end of the tutorial, you should feel comfortable setting up your Ethereum development environment to target Sapphire and know how and when to use confidentiality.

This tutorial is also available as a video on Youtube:

Project Setup

Start by installing Truffle.

Then, run these commands in your terminal:

mkdir MetaCoin && cd MetaCoin truffle unbox MetaCoin git init git add :/ && git commit -m "Initial commit"

Deploying to a Non-confidential EVM

To deploy to the Emerald Testnet, we'll need some tokens. Get those by heading to Oasis Testnet faucet and selecting "Emerald" as the first dropdown. Set the second box to the address of a burner wallet. After submitting the form, it'll take a few moments to receive your tokens.

Add the Emerald Testnet to Truffle

Apply this patch to truffle-config.js:

diff --git a/truffle-config.js b/truffle-config.js
index 68d534c..15c671d 100644
--- a/truffle-config.js
+++ b/truffle-config.js
@@ -22,7 +22,7 @@
 // const mnemonic = process.env["MNEMONIC"];
 // const infuraProjectId = process.env["INFURA_PROJECT_ID"];

-// const HDWalletProvider = require('@truffle/hdwallet-provider');
+const HDWalletProvider = require('@truffle/hdwallet-provider');

 module.exports = {
   /**
@@ -53,6 +53,14 @@ module.exports = {
     //   network_id: 5,       // Goerli's id
     //   chain_id: 5
     // }
+    // This is Testnet! If you want Mainnet, add a new network config item.
+    emerald: {
+      provider: () =>
+        new HDWalletProvider([process.env.PRIVATE_KEY], "https://testnet.emerald.oasis.dev"),
+      network_id: 0xa515,
+    },
   },

   // Set default mocha options here, use special reporters, etc.

Do the Truffle Thing.

You'll want a script to run some methods on the contract. Pop open your favorite editor and paste in this code:

const keccak256 = require("web3").utils.keccak256;

const MetaCoin = artifacts.require("MetaCoin");

async function exerciseContract() {
  const mc = await MetaCoin.deployed();

  const tx = await mc.sendCoin(mc.address, 42);
  console.log(`\nSent some coins in ${tx.tx}.`);
  const t = tx.logs[0].args;
  console.log(`A Transfer(${t[0]}, ${t[0]}, ${t[2].toNumber()}) was emitted.`);

  const storageSlot = await new Promise((resolve, reject) => {
    const getStoragePayload = {
      method: "eth_getStorageAt",
      params: [
        mc.address,
        keccak256(
          "0x" + "00".repeat(12) + mc.address.slice(2) + "00".repeat(32)
        ),
        "latest",
      ],
      jsonrpc: "2.0",
      id: "test",
    };
    mc.contract.currentProvider.send(getStoragePayload, (err, res) => {
      if (err) reject(err);
      else resolve(res.result);
    });
  });
  console.log(`The balance storage slot contains ${storageSlot}.`);

  const balance = await mc.getBalance(mc.address);
  console.log(`The contract now has balance: ${balance.toNumber()}.`);
}

module.exports = async function (callback) {
  try {
    await exerciseContract();
  } catch (e) {
    console.error(e);
  }
  callback();
};

Save it to scripts/exercise.contract.js. We'll use it in just a bit.

Next, you can run the following and see if the contract is deployed.

$ PRIVATE_KEY="0x..." truffle migrate --network emerald

Everything should be succeeding so far.

Finally, run this line and observe some output.

$ PRIVATE_KEY="0x..." truffle exec --network emerald scripts/exercise.contract.js`

Sent some coins in 0xf415ab586ef1c6c61b84b3bd803ae322f375d1d3164aa8ac13c9ae83c698a002
A Transfer(0x56e5F834F88F9f7631E9d6a43254e173478cE06a, 0x56e5F834F88F9f7631E9d6a43254e173478cE06a, 42) was emitted.
The balance storage slot contains 0x2a.
The contract now has balance: 42

Great! That'll be the baseline for our confidential deployment.

Deploying to a Confidential EVM

As for Emerald, we need to configure the Sapphire network and get tokens. Hit the Oasis Testnet faucet, and select "Sapphire." Submit the form.

Add the Sapphire Testnet to Truffle

And another diff for your applying pleasure:

diff --git a/truffle-config.js b/truffle-config.js
index 7af2f42..0cd9d36 100644
--- a/truffle-config.js
+++ b/truffle-config.js
@@ -58,6 +58,11 @@ module.exports = {
         new HDWalletProvider([process.env.PRIVATE_KEY], "https://testnet.emerald.oasis.dev"),
       network_id: 0xa515,
     },
+    // This is Testnet! If you want Mainnet, add a new network config item.
+    sapphire: {
+      provider: () =>
+        new HDWalletProvider([process.env.PRIVATE_KEY], "https://testnet.sapphire.oasis.dev"),
+      network_id: 0x5aff,
+    },
   },

   // Set default mocha options here, use special reporters, etc.

Porting to Sapphire

Here's where things start to get interesting. We will add confidentiality to this starter project in precisely two lines of code.

You'll need to grab the Sapphire compatibility library (@oasisprotocol/sapphire-paratime), so make that happen by issuing the following command:

$ npm add -D @oasisprotocol/sapphire-paratime

So far, so good. Next, import it by adding this line to the top of truffle-config.js:

const sapphire = require('@oasisprotocol/sapphire-paratime');

That's the first line of code. Here's the second:

diff --git a/truffle-config.js b/truffle-config.js
index 0cd9d36..7db7cf8 100644
--- a/truffle-config.js
+++ b/truffle-config.js
@@ -60,7 +60,7 @@ module.exports = {
     },
     sapphire: {
       provider: () =>
-        new HDWalletProvider([process.env.PRIVATE_KEY], "https://testnet.sapphire.oasis.dev"),
+        sapphire.wrap(new HDWalletProvider([process.env.PRIVATE_KEY], "https://testnet.sapphire.oasis.dev")),
       network_id: 0x5aff,
     },
   },

This wrap function takes any kind of provider or signer you've got and turns it into one that works with Sapphire and confidentiality. For the most part, wrapping your signer/provider is the most you'll need to do to get your DApp running on Sapphire, but that's not a complete story since an entire contract may leak state through a regular operation.

And now, for the moment we've all been waiting for:

$ PRIVATE_KEY="0x..." truffle migrate --network sapphire
$ PRIVATE_KEY="0x..." truffle exec --network sapphire scripts/exercise.contract.js

Sent some coins in 0x6dc6774addf4c5c68a9b2c6b5e5634263e734d321f84012ab1b4cbe237fbe7c2.
A Transfer(0x56e5F834F88F9f7631E9d6a43254e173478cE06a, 0x56e5F834F88F9f7631E9d6a43254e173478cE06a, 42) was emitted.
The balance storage slot contains 0x0.
The contract now has balance: 42.

So basically, nothing changed, which is what we're going for. But look at that second to the last line where it says what's in the storage slot. Before, it said 0x2a, but now it says 0x0.

The slot does contain data, or else we couldn't have returned the contract balance. What's happened here is that the Web3 gateway does not have the key used to decrypt the storage slot, so it returns a default value.

Indeed, the gateway does not even have the key needed to decrypt the key in the MKVS; it can tell that we wrote a storage slot, but not which one (although it can make an excellent guess by reading the contract code).

All in all, you can see that confidentiality is in effect, but it's not something end-users need to overthink.

Bonus: Create a Sapphire-Native DApp

Porting an existing Eth app is incredible and can already provide benefits like protecting against MEV. However, starting from scratch with confidentiality in mind can unlock some novel DApps and provide a higher level of security.

One simple-but-useful DApp that takes advantage of confidentiality is a dead person's switch that reveals a secret (let's say the encryption key to a data trove) if the operator fails to re-up before too long. Let's make it happen!

Initializing a New Hardhat Project

We'll use Hardhat this time because it's very convenient to use.

  1. Make & enter a new directory.
  2. Run npx hardhat, then create a TypeScript project.
  3. Install @nomicfoundation/hardhat-toolbox and its peer dependencies.

Add the Sapphire Testnet to Hardhat

Open up your hardhat.config.ts and drop in these lines. They should remind you a lot about what happened with Truffle.

diff --git a/hardhat.config.ts b/hardhat.config.ts
index 414e974..49c95f9 100644
--- a/hardhat.config.ts
+++ b/hardhat.config.ts
@@ -3,6 +3,15 @@ import "@nomicfoundation/hardhat-toolbox";

 const config: HardhatUserConfig = {
   solidity: "0.8.9",
+  networks: {
+    sapphire: {
+      // This is Testnet! If you want Mainnet, add a new network config item.
+      chainId: 0x5aff,
+      url: "https://testnet.sapphire.oasis.dev",
+      accounts: [
+        process.env.PRIVATE_KEY ?? Buffer.alloc(0, 32).toString("hex"),
+      ],
+    },
+  },
 };

 export default config;

Getting the Contract

This is a Sapphire tutorial, and you're already a Solidity expert, so let's not bore you with explaining the gritty details of the contract. Start by pasting Vigil.sol into contracts/Vigil.sol.

While you're there, also place run-vigil.ts into scripts/run-vigil.ts. We'll need that later.

Vigil.sol, the exciting parts

The critical state variables are:

SecretMetadata[] public _metas;
bytes[] private _secrets;
  • _metas has public visibility, so despite the state itself is encrypted and not readable directly, Solidity will generate a getter that will do the decryption for you.
  • _secrets is private and therefore truly secret; only the contract can access the data contained in this mapping.

And the methods we'll care most about are:

  • createSecret adds an entry to both _metas and _secrets.
  • revealSecret acts as an access-controlled getter for the data contained with _secrets. Due to trusted execution and confidentiality, the secret will be revealed only if execution proceeds to the end of the function and does not revert.

The rest of the methods are helpful if you intend to use the contract in production, but they demonstrate that developing for Sapphire is essentially the same as for Ethereum. You can even write tests against the Hardhat network and use Hardhat plugins.

Runing the Contract

And to wrap things up, we'll put Vigil through its paces. First, let's see what's going on.

At the top of the file, we have our favorite import, [@oasisprotocol/sapphire-paratime]. Unlike Truffle, we have to "manually" wrap the signer since the Hardhat config only takes a private key. We do that at the top of the main method.

After deploying the contract, we can create a secret, check that it's not readable, wait a bit, and then check that it has become readable. Pretty cool if you ask me!

Anyway, make it happen by running this command:

$ PRIVATE_KEY="0x..." \ npm hardhat run scripts/run-vigil.ts --network sapphire

And if you see something like the following, you'll know you're well on the road to deploying confidential DApps on Sapphire.

Vigil deployed to: 0x74dC4879B152FDD1DDe834E9ba187b3e14f462f1
Storing a secret in 0x13125d868f5fb3cbc501466df26055ea063a90014b5ccc8dfd5164dc1dd67543
Checking the secret
failed to fetch secret: reverted: not expired
Waiting...
Checking the secret again
The secret ingredient is brussels sprouts

All Done!

Congratulations, you made it through the Sapphire tutorial! If you have any questions, please check out the guide and join the discussion on our Discord server in the #sapphire-paratime channel.

Best of luck on your future forays into confidentiality!

Join the ‘Keeping it Confidential’ Hackathon and Win

From September 14 to October 14, Oasis invites Solidity developers and those wanting to build Web3, privacy-enabled EVM DApps, to participate in the Keeping it Confidential Hackathon.

Dates

September 14, 2022 – October 14, 2022 (midnight UTC)

D_D Newsletter CTA

Prizes

  • 1st place prize - $5000 (equivalent in ROSE)
  • 2nd place prize - $2500 (equivalent in ROSE)
  • 3rd place prize - $1500 (equivalent in ROSE)

Join the Hackathon here!