Skip to content

Commit

Permalink
Dyn exceptions (#90)
Browse files Browse the repository at this point in the history
* Custom exception base class

* tests for DynamicException

* finish template behavior

* Test dynamic excpetion in prod code

* Fix member init

* Fix linter errors

* add more test cases

* test ethCall thrpw

* remove duplicate test

* Change runtime excpetions to DynamicExceptions
  • Loading branch information
jcarraror authored Feb 8, 2024
1 parent 0848444 commit 82284c6
Show file tree
Hide file tree
Showing 52 changed files with 739 additions and 424 deletions.
9 changes: 5 additions & 4 deletions src/contract/contract.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ See the LICENSE.txt file in the project root for more information.
#include "../utils/strings.h"
#include "../utils/tx.h"
#include "../utils/utils.h"
#include "../utils/dynamicexception.h"
#include "variables/safebase.h"

// Forward declarations.
Expand Down Expand Up @@ -138,21 +139,21 @@ class BaseContract : public ContractLocals {
* Invoke a contract function using a tuple of (from, to, gasLimit, gasPrice,
* value, data). Should be overriden by derived classes.
* @param data The tuple of (from, to, gasLimit, gasPrice, value, data).
* @throw std::runtime_error if the derived class does not override this.
* @throw DynamicException if the derived class does not override this.
*/
virtual void ethCall(const ethCallInfo& data) {
throw std::runtime_error("Derived Class from Contract does not override ethCall()");
throw DynamicException("Derived Class from Contract does not override ethCall()");
}

/**
* Do a contract call to a view function.
* Should be overriden by derived classes.
* @param data The tuple of (from, to, gasLimit, gasPrice, value, data).
* @return A string with the answer to the call.
* @throw std::runtime_error if the derived class does not override this.
* @throw DynamicException if the derived class does not override this.
*/
virtual const Bytes ethCallView(const ethCallInfo &data) const {
throw std::runtime_error("Derived Class from Contract does not override ethCall()");
throw DynamicException("Derived Class from Contract does not override ethCallView()");
}

/// Getter for `contractAddress`.
Expand Down
14 changes: 7 additions & 7 deletions src/contract/contractfactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,27 @@ class ContractFactory {
* Setup data for a new contract before creating/validating it.
* @param callInfo The call info to process.
* @return A pair containing the contract address and the ABI decoder.
* @throw std::runtime_error if non contract creator tries to create a contract.
* @throw std::runtime_error if contract already exists.
* @throw DynamicException if non contract creator tries to create a contract.
* @throw DynamicException if contract already exists.
*/
template <typename TContract> auto setupNewContract(const ethCallInfo &callInfo) {
// Check if caller is creator
// TODO: Check if caller is creator of the contract, not the creator of the transaction
// Allow contracts to create other contracts though.
if (this->manager_.getOrigin() != this->manager_.getContractCreator()) {
throw std::runtime_error("Only contract creator can create new contracts");
throw DynamicException("Only contract creator can create new contracts");
}

// Check if contract address already exists on the Dynamic Contract list
const Address derivedAddress = this->manager_.deriveContractAddress();
if (this->manager_.contracts_.contains(derivedAddress)) {
throw std::runtime_error("Contract already exists as a Dynamic Contract");
throw DynamicException("Contract already exists as a Dynamic Contract");
}

// Check if contract address already exists on the Protocol Contract list
for (const auto &[name, address] : ProtocolContractAddresses) {
if (address == derivedAddress) {
throw std::runtime_error("Contract already exists as a Protocol Contract");
throw DynamicException("Contract already exists as a Protocol Contract");
}
}

Expand All @@ -96,7 +96,7 @@ class ContractFactory {
using ConstructorArguments = typename TContract::ConstructorArguments;
auto setupResult = this->setupNewContract<TContract>(callInfo);
if (!ContractReflectionInterface::isContractFunctionsRegistered<TContract>()) {
throw std::runtime_error("Contract " + Utils::getRealTypeName<TContract>() + " is not registered");
throw DynamicException("Contract " + Utils::getRealTypeName<TContract>() + " is not registered");
}

Address derivedAddress = setupResult.first;
Expand Down Expand Up @@ -141,7 +141,7 @@ class ContractFactory {
/// But the variables owned by the contract were registered as used in the ContractCallLogger.
/// Meaning: we throw here, the variables are freed (as TContract ceases from existing), but a reference to the variable is still
/// in the ContractCallLogger. This causes a instant segfault when ContractCallLogger tries to revert the variable
throw std::runtime_error(
throw DynamicException(
"Could not construct contract " + Utils::getRealTypeName<TContract>() + ": " + ex.what()
);
}
Expand Down
29 changes: 16 additions & 13 deletions src/contract/contractmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ See the LICENSE.txt file in the project root for more information.
#include "customcontracts.h"
#include "../core/rdpos.h"
#include "../core/state.h"
#include "../utils/dynamicexception.h"

ContractManager::ContractManager(
const std::unique_ptr<DB>& db, State* state,
Expand All @@ -32,7 +33,7 @@ ContractManager::ContractManager(
for (const DBEntry& contract : contractsFromDB) {
Address address(contract.key);
if (!this->loadFromDB<ContractTypes>(contract, address)) {
throw std::runtime_error("Unknown contract: " + Utils::bytesToString(contract.value));
throw DynamicException("Unknown contract: " + Utils::bytesToString(contract.value));
}
}
}
Expand Down Expand Up @@ -82,15 +83,17 @@ void ContractManager::ethCall(const ethCallInfo& callInfo) {
Functor functor = std::get<5>(callInfo);
std::function<void(const ethCallInfo&)> f;
f = this->factory_->getCreateContractFunc(functor.asBytes());
if (!f) throw std::runtime_error("Invalid function call with functor: " + functor.hex().get());
if (!f) {
throw DynamicException("Invalid function call with functor: ", functor.hex().get());
}
f(callInfo);
}

const Bytes ContractManager::ethCallView(const ethCallInfo& data) const {
const auto& functor = std::get<5>(data);
// This hash is equivalent to "function getDeployedContracts() public view returns (string[] memory, address[] memory) {}"
if (functor == Hex::toBytes("0xaa9a068f")) return this->getDeployedContracts();
throw std::runtime_error("Invalid function call");
throw DynamicException("Invalid function call");
}

void ContractManager::callContract(const TxBlock& tx, const Hash&, const uint64_t& txIndex) {
Expand All @@ -104,7 +107,7 @@ void ContractManager::callContract(const TxBlock& tx, const Hash&, const uint64_
} catch (std::exception &e) {
this->callLogger_.reset();
this->eventManager_->revertEvents();
throw std::runtime_error(e.what());
throw DynamicException(e.what());
}
this->callLogger_->shouldCommit();
this->callLogger_.reset();
Expand All @@ -119,7 +122,7 @@ void ContractManager::callContract(const TxBlock& tx, const Hash&, const uint64_
} catch (std::exception &e) {
this->callLogger_.reset();
this->eventManager_->revertEvents();
throw std::runtime_error(e.what());
throw DynamicException(e.what());
}
this->callLogger_->shouldCommit();
this->callLogger_.reset();
Expand All @@ -132,7 +135,7 @@ void ContractManager::callContract(const TxBlock& tx, const Hash&, const uint64_
if (it == this->contracts_.end()) {
this->callLogger_.reset();
this->eventManager_->revertEvents();
throw std::runtime_error(std::string(__func__) + "(void): Contract does not exist");
throw DynamicException(std::string(__func__) + "(void): Contract does not exist");
}

const std::unique_ptr<DynamicContract>& contract = it->second;
Expand All @@ -142,7 +145,7 @@ void ContractManager::callContract(const TxBlock& tx, const Hash&, const uint64_
} catch (std::exception &e) {
this->callLogger_.reset();
this->eventManager_->revertEvents();
throw std::runtime_error(e.what());
throw DynamicException(e.what());
}

if (contract->isPayableFunction(functor)) {
Expand All @@ -159,7 +162,7 @@ const Bytes ContractManager::callContract(const ethCallInfo& callInfo) const {
if (to == ProtocolContractAddresses.at("rdPoS")) return rdpos_->ethCallView(callInfo);
std::shared_lock<std::shared_mutex> lock(this->contractsMutex_);
if (!this->contracts_.contains(to)) {
throw std::runtime_error(std::string(__func__) + "(Bytes): Contract does not exist");
throw DynamicException(std::string(__func__) + "(Bytes): Contract does not exist");
}
return this->contracts_.at(to)->ethCallView(callInfo);
}
Expand Down Expand Up @@ -208,7 +211,7 @@ bool ContractManager::validateCallContractWithTx(const ethCallInfo& callInfo) {
contract->ethCall(callInfo);
} catch (std::exception &e) {
this->callLogger_.reset();
throw std::runtime_error(e.what());
throw DynamicException(e.what());
}
this->callLogger_.reset();
return true;
Expand Down Expand Up @@ -268,7 +271,7 @@ void ContractManagerInterface::registerVariableUse(SafeBase& variable) {
}

void ContractManagerInterface::populateBalance(const Address &address) const {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
if (!this->manager_.callLogger_->hasBalance(address)) {
Expand All @@ -280,7 +283,7 @@ void ContractManagerInterface::populateBalance(const Address &address) const {
}

uint256_t ContractManagerInterface::getBalanceFromAddress(const Address& address) const {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
this->populateBalance(address);
Expand All @@ -290,13 +293,13 @@ uint256_t ContractManagerInterface::getBalanceFromAddress(const Address& address
void ContractManagerInterface::sendTokens(
const Address& from, const Address& to, const uint256_t& amount
) {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
this->populateBalance(from);
this->populateBalance(to);
if (this->manager_.callLogger_->getBalanceAt(from) < amount) {
throw std::runtime_error("ContractManager::sendTokens: Not enough balance");
throw DynamicException("ContractManager::sendTokens: Not enough balance");
}
this->manager_.callLogger_->subBalance(from, amount);
this->manager_.callLogger_->addBalance(to, amount);
Expand Down
34 changes: 17 additions & 17 deletions src/contract/contractmanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class ContractManager : public BaseContract {
* SafeVariables as contract creation actively writes to DB).
* @param data The call info to process.
* @return A string with the requested info.
* @throw std::runtime_error if the call is not valid.
* @throw DynamicException if the call is not valid.
*/
const Bytes ethCallView(const ethCallInfo& data) const override;

Expand All @@ -190,7 +190,7 @@ class ContractManager : public BaseContract {
* @param tx The transaction to process.
* @param blockHash The hash of the block that called the contract. Defaults to an empty hash.
* @param txIndex The index of the transaction inside the block that called the contract. Defaults to the first position.
* @throw std::runtime_error if the call to the ethCall function fails.
* @throw DynamicException if the call to the ethCall function fails.
* TODO: it would be a good idea to revise tests that call this function, default values here only exist as a placeholder
*/
void callContract(const TxBlock& tx, const Hash& blockHash = Hash(), const uint64_t& txIndex = 0);
Expand All @@ -199,7 +199,7 @@ class ContractManager : public BaseContract {
* Make an eth_call to a view function from the contract. Used by RPC.
* @param callInfo The call info to process.
* @return A string with the requested info.
* @throw std::runtime_error if the call to the ethCall function fails
* @throw DynamicException if the call to the ethCall function fails
* or if the contract does not exist.
*/
const Bytes callContract(const ethCallInfo& callInfo) const;
Expand All @@ -215,7 +215,7 @@ class ContractManager : public BaseContract {
* Validate a transaction that calls a function from a given contract.
* @param callInfo The call info to validate.
* @return `true` if the transaction is valid, `false` otherwise.
* @throw std::runtime_error if the validation fails.
* @throw DynamicException if the validation fails.
*/
bool validateCallContractWithTx(const ethCallInfo& callInfo);

Expand Down Expand Up @@ -330,14 +330,14 @@ class ContractManagerInterface {
const uint256_t& value,
R(C::*func)(const Args&...), const Args&... args
) {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
if (value) {
this->sendTokens(fromAddr, targetAddr, value);
}
if (!this->manager_.contracts_.contains(targetAddr)) {
throw std::runtime_error(std::string(__func__) + ": Contract does not exist - Type: "
throw DynamicException(std::string(__func__) + ": Contract does not exist - Type: "
+ Utils::getRealTypeName<C>() + " at address: " + targetAddr.hex().get()
);
}
Expand All @@ -346,7 +346,7 @@ class ContractManagerInterface {
try {
return contract->callContractFunction(func, args...);
} catch (const std::exception& e) {
throw std::runtime_error(e.what() + std::string(" - Type: ")
throw DynamicException(e.what() + std::string(" - Type: ")
+ Utils::getRealTypeName<C>() + " at address: " + targetAddr.hex().get()
);
}
Expand All @@ -369,19 +369,19 @@ class ContractManagerInterface {
const Address& txOrigin, const Address& fromAddr, const Address& targetAddr,
const uint256_t& value, R(C::*func)()
) {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
if (value) this->sendTokens(fromAddr, targetAddr, value);
if (!this->manager_.contracts_.contains(targetAddr)) {
throw std::runtime_error(std::string(__func__) + ": Contract does not exist");
throw DynamicException(std::string(__func__) + ": Contract does not exist");
}
C* contract = this->getContract<C>(targetAddr);
this->manager_.callLogger_->setContractVars(contract, txOrigin, fromAddr, value);
try {
return contract->callContractFunction(func);
} catch (const std::exception& e) {
throw std::runtime_error(e.what());
throw DynamicException(e.what());
}
}

Expand All @@ -402,7 +402,7 @@ class ContractManagerInterface {
const uint256_t &gasPriceValue, const uint256_t &callValue,
const Bytes &encoder
) {
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to call ContractState without an active callContract"
);
ethCallInfo callInfo;
Expand Down Expand Up @@ -434,12 +434,12 @@ class ContractManagerInterface {
*/
template <typename T> const T* getContract(const Address &address) const {
auto it = this->manager_.contracts_.find(address);
if (it == this->manager_.contracts_.end()) throw std::runtime_error(
if (it == this->manager_.contracts_.end()) throw DynamicException(
"ContractManager::getContract: contract at address " +
address.hex().get() + " not found."
);
auto ptr = dynamic_cast<T*>(it->second.get());
if (ptr == nullptr) throw std::runtime_error(
if (ptr == nullptr) throw DynamicException(
"ContractManager::getContract: Contract at address " +
address.hex().get() + " is not of the requested type: " + Utils::getRealTypeName<T>()
);
Expand All @@ -456,12 +456,12 @@ class ContractManagerInterface {
*/
template <typename T> T* getContract(const Address& address) {
auto it = this->manager_.contracts_.find(address);
if (it == this->manager_.contracts_.end()) throw std::runtime_error(
if (it == this->manager_.contracts_.end()) throw DynamicException(
"ContractManager::getContract: contract at address " +
address.hex().get() + " not found."
);
auto ptr = dynamic_cast<T*>(it->second.get());
if (ptr == nullptr) throw std::runtime_error(
if (ptr == nullptr) throw DynamicException(
"ContractManager::getContract: Contract at address " +
address.hex().get() + " is not of the requested type: " + Utils::getRealTypeName<T>()
);
Expand All @@ -471,7 +471,7 @@ class ContractManagerInterface {
/**
* Emit an event from a contract. Called by DynamicContract's emitEvent().
* @param event The event to emit.
* @throw std::runtime_error if there's an attempt to emit the event outside a contract call.
* @throw DynamicException if there's an attempt to emit the event outside a contract call.
*/
void emitContractEvent(Event& event) {
// Sanity check - events should only be emitted during successful contract
Expand All @@ -481,7 +481,7 @@ class ContractManagerInterface {
// C++ itself already takes care of events not being emitted on pure/view
// functions due to its built-in const-correctness logic.
// TODO: check later if events are really not emitted on transaction revert
if (!this->manager_.callLogger_) throw std::runtime_error(
if (!this->manager_.callLogger_) throw DynamicException(
"Contracts going haywire! Trying to emit an event without an active contract call"
);
this->manager_.eventManager_->registerEvent(std::move(event));
Expand Down
Loading

0 comments on commit 82284c6

Please sign in to comment.