How contracts work
How smart contracts work on the SparqNet protocol
SparqNet Contracts are custom, developer-made classes that directly interact with the current State of the Blockchain. Similar to Solidity contracts, they can be used to employ any type of logic within the network, but unlike Solidity, they aren’t subject to EVM constraints.
To create a new contract on SparqNet, you have to create the contract logic in C++ and code several transaction parsing methods to parse the arguments of a given transaction calling your contract. Additionally, you have to concern yourself with storing your local variables in a database. These are some of the reasons SparqNet is creating a Solidity to C++ transpiler.
Besides the ability to easily bring already existing Solidity developers into the SparqNet space, a Solidity to C++ transpiler can create the intermediary functions between the transaction in the State of the Blockchain and the Contract itself. Doing this dynamically would be counterproductive to one of our core tenets (performance) and introduce development barriers.
For example, The data field of a transaction of a user calling the function transfer(address to, uint256 value) of a given contract with the arguments
0x7e4aa755550152a522d9578621ea22edab204308 and 840000000000000000000
is going to be: 0xa9059cbb0000000000000000000000007e4aa755550152a522d9578621ea22edab20430800000000000000000000000000000000000000000000002d89577d7d40200000
Where:
0xa9059cbb
is the function functor (keccak256("transfer(address,uint256)").substr(8)
)0000000000000000000000007e4aa755550152a522d9578621ea22edab204308
is the encoded address00000000000000000000000000000000000000000000002d89577d7d40200000
is the encoded uint256
The possibility here is to simply have a
contractManager.processTransaction(tx)
inside the State::processNewBlock
function, where all transactions that call contracts can be routed through a single place.However, between
contractManager.processTransaction(tx)
and transfer(address to, uint256 value)
of said Contract, the arguments need to be parsed. Besides the right function being called, the transpiler translates the solidity source code and the functions needed for argument parsing and function selection.Aside from the argument and function parsing issues that arise when the contract is not running in a VM, the developer has to consider their local variables and store them in a DB when opening/closing the node.
The Solidity to C++ Transpiler handles these local variables inside the contract. One major difference between Solidity EVM and C++ SparqNet contracts is that by default, databases are only used when opening the node (loading a past state when starting node) and closing the node (saving the current state when closing node).
Local variables are kept in the memory, while with Solidity, every call to a local variable is a database call. If the developer Contract needs to load something from the DB during execution, they are free to do so, but transpiled source code will always be at the constructor/destructor of the Contract.
One of the main features of Solidity is direct contract interaction. You can cast an address to a contract and call it. If said address contains a valid contract that matches the Interface specified by the developer, that contract function is successfully called and returned.
For example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
interface IERC20 {
function balanceOf(address addr) external view returns (uint256);
}
contract GetERC20Balance {
function getERC20balance(address contractAddr, address user) external view returns (uint256) {
IERC20 tokenContract = IERC20(contractAddr);
return tokenContract.balanceOf(user);
}
}
In the above contract, the EVM casts the
contractAddress
into a contract with the IERC20 interface, enabling the usage of the balanceOf()
function. On SparqNet, as contracts are compiled directly with the blockchain itself, a class called ContractManager
(declared in contracts/contractmanager.h
) can be used to hold all contract classes’ instances in a polymorphic manner. Additionally, a reference from it can be argued to any contract (default in the base
Contract
class). By using polymorphism, you can cast pointers from a given type (a Generic Contract stored inside unordered_map<Address,Contract>
on ContractManager
) to a desired contract. This is exemplified in the ContractManager topic.The equivalent definition in C++ would be similar to:
uint256_t GetERC20Balance::getERC20Balance(const Address& address, const Address &user) {
auto tokenContract = dynamic_cast<const ERC20&>(this->contractManager.getContract(address));
return tokenContract.balanceOf(address);
}
Remember that
getERC20Balance()
is declared as an external view function in Solidity, forcing the contract cast to be const
only, as this contract function cannot change the state.Transpiler Modes
The Solidity to C++ Transpiler can be used with any type of Solidity source code as long as it's compatible, but there are different ways a developer can use the transpiler. These are the basic mode and advanced mode.
In the basic mode, the developer only declares the functions and local variables to be used, so the transpiler can create argument encoding and database functions necessary for the structure of the contract. This is the recommended way since you will be coding the contract logic in C++.
In the advanced mode, the developer can input a full Solidity contract and it will convert all the logic in the implementation into C++ source code. Of course, as the application grows and starts getting more complex, the Solidity source code and transpiled code are not enough for the performance requirements, and this is where the freedom of C++ shines.
Upcoming Solidity Features and Current Alternatives
The developer is free to do whatever they want with their contract. For example, they can load it directly into the state without contractManager and without having every variable already loaded in the state. All they have to do is pay attention to the lines.
However, some Solidity features are NOT available on SparqNet (through the transpiler), such as:
- Interfaces¹
- Inline Assembly
- Solidity Version < 0.8
- Libraries²
- Some of Solidity global variables (such as basefee, gasleft, and others)
Interfaces are not supported, but you can include a Contract directly instead of using interfaces. And fortunately, Libraries will be implemented in the future. The development of the Solidity to C++ project will follow simple steps.
We will eventually provide more Solidity features once we adapt them in a static contract manner, so in the meantime, you can examine the Solidity compiler and use its Abstract Syntax Tree.
Since the majority of Solidity contracts depend on OpenZeppelin libraries, SparqNet will be creating equivalents for these libraries without requiring the transpilation of the OpenZeppelin contract source code. You simply link with "
openzeppelin
" on the import (e.g. import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
";)The Contract class, declared in contract/contract.h, is the base class which all contracts derive from. This class holds all the Solidity global variables, besides variables common among these contracts (such as contract Address). Its header should look similar to the following:
// class Contract {
private:
// CONTRACT VARIABLES
const Address _contractAddress;
const uint64_t _chainId;
const std::unique_ptr<ContractManager>& _contractManager;
// GLOBAL VARIABLES
static Address _coinbase; // Current Miner Address
static uint256_t _blockNumber; // Current Block Number
static uint256_t _blockTimestamp; // Current Block Timestamp
public:
Contract(const Address& contractAddress, const uint64_t& chainId, std::unique_ptr<ContractManager> &contractManager) : _contractAddress(contractAddress), _chainId(chainId), _contractManager(contractManager) {}
const Address& coinbase() { return _coinbase };
const uint256_t& blockNumber() { return _blockNumber};
const uint256_t blockTimestamp() { return _blockTimestamp};
virtual void callContractWithTransaction(const Tx& transaction);
virtual std::string ethCallContract(const std::string& calldata) const;
friend State; // State can update the private global variables of the contracts
}
Regarding the
callContractWithTransaction
and the ethCallContract
functions, callContractWithTransaction
is used by the State when calling from processNewBlock()
, while ethCallContract
is used by RPC to answer for eth_call
. Strings returned by ethCallContract
are hex strings encoded with the desired function result.The
ContractManager
is the class that holds all the current contract instances in the State, besides being the access point for contracts to access other contracts. It's header should be similar to the following:class ContractManager {
private:
std::unordered_map<Address,std::unique_ptr<Contract>> _contracts;
public:
ContractManager(std::unique_ptr<DBService> &dbService);
std::unique_ptr<Contract>& getContract(Address address);
const std::unique_ptr<const Contract>& getConstContract(Address address) const;
void processTransaction(const Tx& transaction)
}
The contract manager will be responsible for deploying the contracts in the chain, loading them from DB when constructing and saving them to DB when deconstructing. The function
processTransaction
would be similar to thisGiving the example Solidity contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
contract ExampleContract {
mapping(address => uint256) values;c
function setValue(address addr, uint256 value) external {
values[addr] = value;
return;
}
}
The transpiled code should look similar to this:
Declaration
ExampleContract.h
#include <...>
class ExampleContract : public Contract {
private:
std::unordered_map<Address, uint256_t> values;
// Const-reference as they are not changed by the function.
void setValue(const Address &addr, const uint256 &value);
public:
ExampleContract(const Address& contractAddress,
const uint64_t& chainId,
std::unique_ptr<ContractManager> &contractManager, std::unique_ptr<DBService&> db);
void callContractWithTransaction(const Tx& transaction)
}
Definition
ExampleContract.cpp
#include "ExampleContract.h"
ExampleContract(const Address& contractAddress,
const uint64_t& chainId,
std::unique_ptr<ContractManager> &contractManager, std::unique_ptr<DBService&> db) :
Contract(contractAddress, chainId, contractManager) {
// Read the "values" variables from DB
// Code generated by the transpiller from all local variables
// of the solidity contract, on the ExampleContract, you have values as a address => uint256 mapping
...
}
void ExampleContract::setValue(const Address &addr, const uint256 &value) {
this->values[addr] = value;
return
}
void ExampleContract::callContractWithTransaction(const Tx& transaction) {
// CODE GENERATED BY THE TRANSPILLER
// USED TO ROUTE AND DECODE TRANSACTIONS
// THE IF USED HERE IS FOR EXAMPLE PURPOSES
// THE GENERATED CODE WILL BE USING DIFFERENT STRING ALGORITHMS IN ORDER TO MATCH
// FUNCTOR AND ARGUMENTS TO CONTRACT FUNCTION.
std::string_view txData = transaction.getData();
auto functor = txData.substr(0,8);
// Keccak256("setValue(address,uint256)")
if (functor == Utils::hexToBytes("0x48461b56")) {
this->setValue(ABI::Decoder::decodeAddress(txData, 8), ABI::Decoder::decodeUint256(txData, 8 + 32));
}
return;
}
Last modified 27d ago