Contracts in SparqNet
In general terms, contracts in SparqNet are developer-created C++ classes that directly interact with the blockchain's current state, similar to Solidity contracts. This chapter will comprehensively cover creating new contracts within OrbiterSDK.
They facilitate diverse logic implementations within the network, while taking advantage of the absence of EVM constraints, since they are composed of native, compiled code.
To create a contract from scratch (assuming you have already forked the project - if not, now's the time), you must develop the contract logic in C++, manually code several methods to parse arguments of transactions calling your contract, and use a database to manage the storage of your local variables.
The rules explained in this chapter ensure that contracts remain compatible with frontend Web3 tools (e.g. MetaMask, ethers.js, web3.js, etc.). Those are designed to interact with Solidity contracts and thus require a similar interface.
To call contracts, you can replicate the function definitions in Solidity and generate the ABI using a tool like Ethereum's Remix or any other of your preference. This ABI can then be used by MetaMask/ethers.js/web3.js to call the contract's functions from the frontend.
OrbiterSDK offers two types of contracts: Dynamic Contracts and Protocol Contracts. The differences between both types primarily come from how they are created and managed within the SDK.
Protocol Contracts:
- Are directly baked in/integrated into the blockchain, thus not linked to the ContractManager class and not contained by it, which removes some restrictions but adds others
- Are completely up to you (for the most part) as to where to place the ownership of them, their variables and commit/revert logic within the source code of the blockchain
Dynamic Contracts:
- Can only be handled by the
ContractManager
class, which enables the chain owner to create an unlimited number of those - Have an additional layer of protection - Safe Variables, which allow a finer control on whether their changes are commited to the state or automatically reverted when necessary (e.g. a transaction is reverted)
- Can only be called and process their functions during a block processing operation
- Are directly loaded into memory and closely resemble Solidity contracts
Currently, OrbiterSDK provides templates for
ERC20
, ERC20Wrapper
, and NativeWrapper
dynamic contracts.The
ContractManager
class (declared in src/contract/contractmanager.h
) is a Protocol Contract, responsible for:- Handling all the logic related to creating and loading Dynamic Contracts
- Managing global variables for contracts, such as the contract's name, address, owner, and balance
If the signature of any function registered within your contract matches the one registered in ContractManager, it will call it. If an error occurs, it will automatically revert the changes made to the account state (in case it's a payable function).
It also has a view function called
getContractList()
which returns an (address[], string[])
map, containing the deployed contracts and their type names.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;
}
The Contract base class
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.Last modified 1mo ago