Migrate your dApp to TEN
Migrating to TEN enables your dApp to leverage "Programmable Encryption". Below are steps to help you transition smoothly.
Key Migration Steps
- Update your Hardhat deployment to support the
--network ten
option. - Add data protection logic to your view functions (if applicable).
- Configure visibility rules for event logs and internal storage.
- Add the TEN onboarding widget to your JavaScript UI.
1. Configuring Hardhat
First, set up a Hardhat project if you haven't already.
1.1 Installing the TEN Hardhat Plugin
To add TEN Network compatibility, install the ten-hardhat-plugin
:
npm install ten-hardhat-plugin
You can use npm
or yarn
to install plugins.
1.2 Configuring hardhat.config.js
Modify hardhat.config.js
in your project’s root directory as follows:
import { HardhatUserConfig } from "hardhat/config";
import "@nomiclabs/hardhat-waffle";
import "ten-hardhat-plugin";
module.exports = {
solidity: "0.8.10",
networks: {
hardhat: {
// Configuration for the Hardhat Network
},
ten: {
url: "https://testnet.ten.xyz/v1/",
chainId: 443,
accounts: ["your-private-key"],
},
},
};
export default config;
Once configured, you can start writing or migrating your smart contracts.
2. Writing Smart Contracts for TEN
TEN executes smart contracts within the EVM similarly to Ethereum, so you can reuse your existing code. However, the execution and the internal state are hidden from everyone, including node operators and the sequencer.
TEN encrypts both the execution and its internal database using Trusted Execution Environments (TEEs).
The getStorageAt method is disabled by default on TEN, so data access relies on view functions that you define. Public variables remain accessible as Solidity automatically creates getters for them.
Let's illustrate with a basic storage dApp example where users can store and retrieve a number.
At every step, we'll add a new feature and explain the difference between TEN
and Ethereum
.
Step 1: Basic contract with a Public Variable
Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageExample {
mapping(address => uint256) public storedValues;
function storeValue(uint256 value) public {
storedValues[tx.origin] = value;
}
}
Explanation
In this step, we created a public variable storedValues
that maps the provided value to the address of the user who called the storeValue
function.
Because the variable is public, Solidity will provide a default public getter for it.
Since there are no data access restrictions, on both Ethereum and TEN, everyone will be able to read the values of all users.
Step 2: Converting to a Private Variable with an explicit Getter Function
Code
contract StorageExample {
mapping(address => uint256) private _storedValues;
function storeValue(uint256 value) public {
_storedValues[tx.origin] = value;
}
function getValue(address account) public view returns (uint256) {
return _storedValues[account];
}
}
Explanation
The storedValues
variable is now private, and we added a basic getValue
function for users to retrieve their value.
On both Ethereum and TEN, anyone can call getValue
to retrieve any value.
On Ethereum, _storedValues
can also be accessed directly with getStorageAt
Step 3: Data Access Control
In this step, we'll add restrictions so users can only access their own data.
Code
contract StorageExample {
mapping(address => uint256) private _storedValues;
function storeValue(uint256 value) public {
_storedValues[tx.origin] = value;
}
function getValue(address account) public view returns (uint256) {
require(tx.origin == account, "Not authorized!");
return _storedValues[account];
}
}
Explanation
The key line is: require(tx.origin == account, "Not authorized!");
, which ensures that the caller of the view function is the owner of the data.
TEN uses "Viewing Keys" to authenticate view function calls.
When deployed on TEN, this code guarantees that all users can only access their own values, and nobody can read the _storedValues
.
Step 4: Emitting Events - Default Visibility
Event logs notify UIs about state changes in smart contracts.
To improve our smart contract, we’ll emit an event when a user stores a value and milestone events when a specific size threshold is met.
Code
contract StorageExample {
mapping(address => uint256) private _storedValues;
uint256 private totalCalls = 0;
event DataChanged(address indexed account, uint256 newValue);
event MilestoneReached(uint256 noStoredValues);
function storeValue(uint256 value) public {
_storedValues[tx.origin] = value;
emit DataChanged(tx.origin, value);
totalCalls++;
if (totalCalls % 1000 == 0) {
emit MilestoneReached(totalCalls);
}
}
function getValue(address account) public view returns (uint256) {
require(tx.origin == account, "Not authorized!");
return _storedValues[account];
}
}
Explanation
Notice how we defined the two events: DataChanged
and MilestoneReached
, and are emitting them in the storeValue
function.
In Ethereum, everyone can query and subscribe to these events. This obviously can't be the case for TEN because it would completely break the functionality.
Notice how in this version, we have no configuration for event log visibility, so we are relying on the default rules.
Rule 1: Event logs that contain EOAs as indexed fields (topics) are only visible to those EOAs. Rule 2: Event logs that don't contain any EOA are visible to everyone.
In our case, the default rules ensure that:
DataChanged
is visible only to the address that is storing the value.MilestoneReached
is publicly visible.
Step 5: Customizing Event Visibility
The default visibility rules are a good starting point, but complex dApps require greater flexibility.
TEN give you explicit control over event visibility.
Code
interface ContractTransparencyConfig {
enum Field { TOPIC1, TOPIC2, TOPIC3, SENDER, EVERYONE }
enum ContractCfg { TRANSPARENT, PRIVATE }
struct EventLogConfig {
bytes32 eventSignature;
Field[] visibleTo;
}
struct VisibilityConfig {
ContractCfg contractCfg;
EventLogConfig[] eventLogConfigs;
}
function visibilityRules() external pure returns (VisibilityConfig memory);
}
contract StorageExample is ContractTransparencyConfig {
mapping(address => uint256) private _storedValues;
uint256 private totalCalls = 0;
event DataChanged(address indexed account, uint256 newValue);
event MilestoneReached(uint256 noStoredValues);
function storeValue(uint256 value) public {
_storedValues[tx.origin] = value;
emit DataChanged(tx.origin, value);
totalCalls++;
if (totalCalls % 1000 == 0) {
emit MilestoneReached(totalCalls);
}
}
function getValue(address account) public view returns (uint256) {
require(tx.origin == account, "Not authorized!");
return _storedValues[account];
}
function visibilityRules() external pure override returns (VisibilityConfig memory) {
EventLogConfig[] memory eventLogConfigs = new EventLogConfig[](2);
// the signature of "event DataChanged(address indexed account, uint256 newValue);"
bytes32 dataChangedEventSig = hex"0xec851d5c322f7f1dd5581f7432e9f6683a8709a4b1ca754ccb164742b82a7d2f";
Field[] memory relevantTo = new Field[](2);
relevantTo[0] = Field.TOPIC1;
relevantTo[1] = Field.SENDER;
eventLogConfigs[0] = EventLogConfig(dataChangedEventSig, relevantTo);
// the signature of "event MilestoneReached(uint256 noStoredValues);"
bytes32 milestoneReachedEventSig = hex"0xd41033274424d56dd572e7196fb4230cf4141d546b91fc00555cab8403965924";
Field[] memory relevantTo = new Field[](1);
relevantTo[0] = Field.EVERYONE;
eventLogConfigs[1] = EventLogConfig(milestoneReachedEventSig, relevantTo);
return VisibilityConfig(ContractCfg.PRIVATE, eventLogConfigs);
}
}
Explanation
The ContractTransparencyConfig
interface is known by the TEN platform.
When a contract is deployed, the platform will call the visibilityRules
function, and store the VisibilityConfig
.
For each event type, you can configure which fields can access it. This allows the developer to configure an event to be public even if it has EOAs or to allow the sender of the transaction to access events emitted even if the address is not in the event.
Notice how in the visibilityRules
above, we configure the DataChanged
event to be visible to the first field and the sender, and the MilestoneReached
to be visible to everyone.
The other configuration: VisibilityConfig.contractCfg
applies to the entire contract:
ContractCfg.TRANSPARENT
: The contracts will have public storage and events, behaving exactly like Ethereum.ContractCfg.PRIVATE
: The default TEN behaviour, where the storage is not accessible and the events are individually configurable.