Unleashing Privacy in Smart Contracts: Building and Deploying Your First Privacy-Supported Smart Contract on Oasis Sapphire Parachain Using Foundry.

Unleashing Privacy in Smart Contracts: Building and Deploying Your First Privacy-Supported Smart Contract on Oasis Sapphire Parachain Using Foundry.

Introduction

Building a smart contract backend for DApp applications that consist of sensitive or confidential information has never been easy. Many times, we usually make use of a Web2 backend service to handle these sensitive parts, which is actually against the philosophy of Web3.

Oasis Sapphire is an EVM-compatible blockchain that allows storing of sensitive information on-chain. Unlike its other EVM-compatible counterparts, which do not support this feature, this article will guide you through the process of building and deploying your first privacy-supported smart contract on the Oasis Sapphire Blockchain using Foundry.

Before we delve deeper into what the article entails, I would like us to take a sneak peek into what the Oasis Sapphire blockchain is all about and why we should consider building on it.

Sneak Peek Into Oasis Sapphire Chain

As describe in the official oasis documentation :

"The Sapphire ParaTime is our official confidential EVM Compatible ParaTime providing a smart contract development environment with EVM compatibility."

The Sapphire blockchain shares many similarities with other EVM-compatible blockchains. So, you shouldn't be concerned when building your Solidity smart contract on Oasis Sapphire or porting your application. Instead, Oasis Sapphire is designed to enhance the features already present on other EVM-compatible chains.

Building on the Sapphire blockchain provides you with the following benefits, as detailed in the official documentation:

i. Confidential state, end-to-end encryption, confidential randomness

ii. EVM compatibility

iii. Easy integration with EVM-based dApps, such as DeFi, NFT, Metaverse, and crypto gaming

iv. Scalability — increased transaction throughput

v. Lower costs — 99%+ lower fees than Ethereum

vi. 6-second finality (1 block)

vii. Upcoming cross-chain bridge for enabling cross-chain interoperability

Sapphire vs Ethereum

The Sapphire chain and the Ethereum chain have a few significant differences. However, these changes do not prevent you from building your Solidity smart contract on the Oasis sapphire chain.

The following are the various breaking changes, as written in the official Oasis documentation:

i. Contract state is only visible to the contract that wrote it. With respect to the contract API, it's as if all state variables are declared as private, but with the further restriction that not even full nodes can read the values. Public or access-controlled values are provided instead through explicit getters.

ii. Transactions and calls are end-to-end encrypted into the contract. Only the caller and the contract can see the data sent to/received from the ParaTime. This ends up defeating most of the utility of block explorers, however.

iii. The from address using of calls is derived from a signature attached to the call. Unsigned calls have their sender set to the zero address. This allows contract authors to write getters that release secrets to authenticated callers, but without requiring a transaction to be posted on-chain.

Without wasting too much time looking at what Oasis Sapphire has to offer, let's dive into the main discussion of this article. Also, if you want to learn more about the Oasis chain, visit this link.

Building And Deploying On the Oasis Sapphire Chain Using Foundry

Before frameworks like Foundry came into existence, we had many excellent libraries like Hardhat, Vyper, etc., used for writing Solidity smart contracts. However, over time, each of these frameworks had its drawbacks. This led to the creation of various Solidity frameworks to address the various problems encountered along the way.

Also, using Foundry as our development framework comes with numerous advantages, such as enabling us to write tests in Solidity, fuzz testing, etc. We won't go deeper into Foundry here, as it would distract from the purpose of this article. If you want to learn more about Foundry, please check out this link

Set Up Your Development Environment

Now it's time for us to set up our Foundry development environment. Before we proceed, we will need to install Foundry.

Note, that setting up Foundry on Windows is slightly different from the way Linux and Mac users would set up theirs. check out this foundry installation guide to assist you on how to install on windows.

Install Foundry

curl -Lhttps://foundry.paradigm.xyz| bash

Confirming Successful Installation

forge --version

After running the command above, the corresponding version number will appear, as shown in the image below.

Creating Our First Project

forge init <NAME OF PROJECT>

After successfully executing the command above, you should see a folder structure that resembles the image below.

Let's take a look at the following folders:

Src folder: This is simply where our smart contract resides.

The Script folder: This is simply where our smart contract deployment script resides

Test folder: This is simply where our smart contract test cases resides.

Writing Our Smart Contract

It's now time for us to start writing our smart contract. The aim of this article is to walk you through how to deploy our Solidity smart contract to the Sapphire chain using Foundry. We will be utilizing the Solidity code already written by the Oasis team on how to deploy to the Sapphire chain using Hardhat. The code can be found here. Additionally, the Oasis team has done an excellent job of explaining every aspect of the code.

Here is a screenshot of the explanation from the Oasis team.

Go to your src folder and create a new file called privacy.sol. You need to paste this code into your privacy.sol file.

Privacy.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

contract Vigil {
    struct SecretMetadata {
        address creator;
        string name;
        /// @notice How long (in seconds) the secret should remain so past the creator's last update.
        uint256 longevity;
    }

    event SecretCreated(
        address indexed creator,
        string indexed name,
        uint256 index
    );
    event SecretRevealed(
        address indexed creator,
        string indexed name,
        uint256 index
    );

    SecretMetadata[] public _metas;
    bytes[] private _secrets;
    /// @dev The unix timestamp at which the address was last seen.
    mapping(address => uint256) public _lastSeen;

    function createSecret(
        string calldata name,
        uint256 longevity,
        bytes calldata secret
    ) external {
        _updateLastSeen();
        _metas.push(
            SecretMetadata({
                creator: msg.sender,
                name: name,
                longevity: longevity
            })
        );
        _secrets.push(secret);
        emit SecretCreated(msg.sender, name, _metas.length - 1);
    }

    /// @notice Reveals the secret at the specified index.
    function revealSecret(uint256 index) external returns (bytes memory) {
        require(index < _metas.length, "no such secret");
        address creator = _metas[index].creator;
        uint256 expiry = _lastSeen[creator] + _metas[index].longevity;
        require(block.timestamp >= expiry, "not expired");
        emit SecretRevealed(creator, _metas[index].name, index);
        return _secrets[index];
    }

    /// @notice Returns the time (in seconds since the epoch) at which the owner was last seen, or zero if never seen.
    function getLastSeen(address owner) external view returns (uint256) {
        return _lastSeen[owner];
    }

    function getMetas(uint256 offset, uint256 count)
        external
        view
        returns (SecretMetadata[] memory)
    {
        if (offset >= _metas.length) return new SecretMetadata[](0);
        uint256 c = offset + count <= _metas.length
            ? count
            : _metas.length - offset;
        SecretMetadata[] memory metas = new SecretMetadata[](c);
        for (uint256 i = 0; i < c; ++i) {
            metas[i] = _metas[offset + i];
        }
        return metas;
    }

    function refreshSecrets() external {
        _updateLastSeen();
    }

    function _updateLastSeen() internal {
        _lastSeen[msg.sender] = block.timestamp;
    }
}

Compile Your Code

forge compile

An 'out' folder will be generated, which consists of the smart contract ABI and the smart contract bytes code.

Writing Tests For Our Smart Contract

It is actually a good practice for us to usually write tests for smart contracts, so that we will have enough trust in our code in production.

Navigate to the test folder and create a file called privacy.t.sol.

privacy.t.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console2} from "forge-std/Test.sol";
import {Vigil} from "../src/createSecret.sol";

contract VigilTest is Test {
    Vigil _vigilContractInstance;
    address _callersAddr = address(0x124);

    function testCreateSecret() public {
        vm.startPrank(_callersAddr);
        _vigilContractInstance = new Vigil();
        _vigilContractInstance.createSecret(
            "sent forth msg",
            block.timestamp + 3 days,
            "the money is in the drawer"
        );
        // check for timers updated
        assertEq(
            _vigilContractInstance.getLastSeen(_callersAddr),
            block.timestamp
        );

        // check for the metadata
        assertEq(
            _vigilContractInstance.getMetas(0, 1)[0].creator,
            _callersAddr
        );
    }

    function testrevealSecret() public {
        vm.startPrank(_callersAddr);
        _vigilContractInstance = new Vigil();
        _vigilContractInstance.createSecret(
            "sent forth msg",
            block.timestamp + 3 days,
            "the money is in the drawer"
        );
        vm.expectRevert("not expired");
        _vigilContractInstance.revealSecret(0);
    }

    function testRefreshSecret() public {
        _vigilContractInstance = new Vigil();
        vm.startPrank(_callersAddr);
        _vigilContractInstance.refreshSecrets();
        assertEq(
            _vigilContractInstance.getLastSeen(_callersAddr),
            block.timestamp
        );
    }

    function testLastSeen() public {
        _vigilContractInstance = new Vigil();
        vm.startPrank(_callersAddr);
        _vigilContractInstance.refreshSecrets();
        assertEq(
            _vigilContractInstance.getLastSeen(_callersAddr),
            block.timestamp
        );
    }
}

Then, run the command forge test to execute all your tests. All my tests are passing

Setting up our deployment Script

One beautiful thing about Foundry is that it enables us to use Solidity to write our deployment script.

To create our deployment script, navigate to the script folder and create a file called privacy.s.sol. Then, copy and paste the code below into the file.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console2} from "forge-std/Script.sol";
import {Vigil} from "../src/createSecret.sol";

contract PrivacyScript is Script {


    function run() public {
        vm.startBroadcast();
        new Vigil();
        vm.stopBroadcast();
    }
}

Deploy The Smart Contract To Your preferred blockchain

forge script script/<DEPLOYMENT-SCRIPT-FILE-NAME>:<Deployment-script-contract-name> --rpc-url <RPC-URL> --private-key <PRIVATE-KEY> --legacy --broadcast

Note: Running the command without the--broadcastflag enables us to simulate our deployment.

You can get Oasis sapphire testnet and mainnet configuration settings here

Reading The State Of The Smart Contract

The state of the smart contract deployed on Oasis Sapphire can only be accessed through the smart contract itself. To prove this, we will read the state of the smart contract that was deployed on both the Sepolia testnet and the Sapphire testnet.

Reading The State Of The Smart Contract on the Seploia Testnet

Foundry's cast gives us the ability to perform various operations on smart contracts, like calling a smart contract function, and reading the smart contract state variable values, regardless of the visibility of such state variables. Also, even though a state variable is marked as private, it doesn't mean we can't access its value.

cast storage --rpc-url <RPC-URL> < Contract-address> .

Paste the command above into your terminal. After successful execution, you will receive a response similar to the image below. Congratulations, you have successfully read the value of the state variable of our contract deployed on Sepolia testnet.

Reading From A Specific Slot In Our Smart Contract On The Sepolia Testnet

In Solidity, state variables are used to store data that can change over time. These variables are permanently stored in a contract's storage and can be thought of as a single slot in a database that you can query and alter by calling functions of the code that manages the database .

So, each state variable is assigned a slot number starting from 0. If we have 5 state variables in a smart contract, the first state variable will be assigned a slot number of 0, the second state variable a slot number of 1, and so on, until all the numbering is complete.

So, in our case, we will be reading data in slot 1. To do that, enter the command as shown below.

cast storage --rpc-url <SEPOLIA-RPC-URL> <CONTRACT-ADDRESS> <SLOT NUMBER>

Reading Smart Contract State On Oasis Sapphire Testnet

cast storage --rpc-url <Sapphire-testnet-RPC-URL> <CONTRACT-ADDRESS>

After entering this command in the terminal, this was the response I got as seen below. As stated by the Oasis official documentation, 'only the smart contract itself can have access to the state of the smart contract'. This means that any attempt to access the smart contract state other than through the smart contract itself will fail.

Reading From A Specific Slot In Our Smart Contract On The Oasis Sapphire Testnet

cast storage --rpc-url <Sapphire-testnet-RPC-URL> <CONTRACT-ADDRESS> <SLOT NUMBER>

All attempts to read the entire state variable value on Oasis Sapphire testnet and a single state variable value from slot 1 failed. This is a result of the confidential feature of Oasis Sapphire, which allows only the smart contract to have access to its own state, and no third party is allowed to have access to it

Congratulations! you have reached the end of the tutorial. If you learned a lot from this article, kindly give us a follow, like, or comment. Also, you can check out the official Oasis documentation here. https://docs.oasis.io/