Four exceptions may be incurred during contract execution:
Asset-style Exception
Require-style Exception
Validation-style Exception
VMillegal-style Exception
Asset-Style Exception
The following triggers an assert-style exception leading to an invalid opcode error, depleting all Energy (including both consumed and unconsumed Energy thus far):
If the array index you are accessing is excessively large or negative (e.g., x[i] where i >= x.length or i < 0).
If you access a fixed length of bytesN, and the index is overly large or negative.
If you attempt division or modulo operations with zero as the divisor (e.g., 5 / 0 or 23 % 0).
If there's a negative digit shift.
If you convert a value that is excessively large or negative to an enumerated type.
If you invoke an uninitialized internal function type variable.
If you call the argument of the assert (expression), and the resulting outcome is false.
Timeout during contract execution.
In case of a PolluxStackOverFlowException.
If an OutofMem exception occurs, surpassing the 3M memory limit.
During contract operation, an overflow transpires, such as in addition.
Require-style Exception
The following conditions result in a require-style exception, leading to a revert error and only consuming the previously consumed energy, excluding the unconsumed energy.
Calling throw.
If you invoke the require parameter (expression), and the final outcome is false.
When calling a function through a message, but the function doesn't conclude correctly (e.g., running out of Energy or encountering its own exception). If Energy isn't specified during the function call, all consumed Energy is passed in and utilized. Note that this function excludes low-level operations like call, send, delegatecall, or callcode. Low-level operations do not throw an exception but return false to denote failure.
Creating a contract with the new keyword, but the contract isn't formed correctly (since Energy can't be specified during contract creation, all Energy is passed in and consumed).
If your contract receives POX via a public function lacking a payable modifier (including constructors, fallback functions, and generic public functions).
When transfer() encounters a failure.
Calling revert().
Reaching the maximum function stack depth of 64.
Validation-style Exception
A PVMIllegal-style exception arises in the following scenarios. This particular type of exception doesn't lead to transaction chaining, but the node initiating the transaction will incur penalties at the network layer for a specific duration.
OwnerAddress and OriginAddress are not equal during contract creation.
Broadcasting a constant request.
Exception Handling Process
The entry is a go() exception that will be caught and processed in go() and will not be passed outside of go().
public void go() {
try {
pvm.play(program);
result = program.getResult();
// If there is Exception or Revert
// Important:
// Exception is thrown in the program settings.
// Revert is a Virtual Machine compiler written into bytecode in advance, arriving by jump.
if (result.getException() != null || result.isRevert()) {
if (result.getException() != null) {
// If Exception,will consume all Energy
program.spendAllEnergy();
// Set runtimeError to indicate the field of the error content
runtimeError = result.getException().getMessage();
// Throw an exception
throw result.getException();
} else {
// If it is Revert and there is no Exception, just set the runtimeError field to indicate the error content.
runtimeError = "REVERT opcode executed";
}
// As long as Exception or Revert occurs, it will not commit. All state changes in the virtual machine execution process will not fall.
} else {
// Without Exception and Revert, commit, all state changes during virtual machine execution will fall.
deposit.commit();
}
}
catch (PVMStackOverFlowException e) {
// PVM or JVM, PVMStackOverFlowException, flags exception.
// The PVMStackOverFLowException will only be caught in go(), which uses PVMStackOverFlowException that occurs in the contract called by call. Or the PVMStackOverFlowException that is called repeatedly. It won't catch up in play(), and will only be caught here.
result.setException(e);
runtimeError = result.getException().getMessage();
}
// Catch all the content that can be thrown
catch (Throwable e) {
// Mark if the exception is unknown.
if (Objects.isNull(result.getException())) {
result.setException(new RuntimeException("Unknown Throwable"));
}
// Ensures the runtimeError has a value
if (StringUtils.isEmpty(runtimeError)) {
runtimeError = result.getException().getMessage();
}
}
}
// After the go() function, result.getException() will not be used, and runtimeError will be filled in the transactionInfo.
The play() function is where the virtual machine actually executes. Three places call play(), go() (described above), call To Address() (the CALL instruction is called, which is called in the contract), and create Contract() (CREATE directive, which is when the contract is created in the contract). The latter two will not handle catching exceptions, and the exceptions thrown out of play will continue to be thrown out in the latter two.
public void play() {
// The actual execution of the virtual machine happens here.
// Handle the execution logic, state changes, and potential exceptions.
}
public void go() {
try {
pvm.play(program);
result = program.getResult();
// If there is Exception or Revert
// Important:
// Exception is thrown in the program settings.
// Revert is a Virtual Machine compiler written into bytecode in advance, arriving by jump.
if (result.getException() != null || result.isRevert()) {
if (result.getException() != null) {
// If Exception,will consume all Energy
program.spendAllEnergy();
// Set runtimeError to indicate the field of the error content
runtimeError = result.getException().getMessage();
// Throw an exception
throw result.getException();
} else {
// If it is Revert and there is no Exception, just set the runtimeError field to indicate the error content.
runtimeError = "REVERT opcode executed";
}
// As long as Exception or Revert occurs, it will not commit. All state changes in the virtual machine execution process will not fall.
} else {
// Without Exception and Revert, commit, all state changes during virtual machine execution will fall.
deposit.commit();
}
} catch (PVMStackOverFlowException e) {
// PVM or JVM, PVMStackOverFlowException, flags exception.
// The PVMStackOverFLowException will only be caught in go(), which uses PVMStackOverFlowException that occurs in the contract called by call. Or the PVMStackOverFlowException that is called repeatedly. It won't catch up in play(), and will only be caught here.
result.setException(e);
runtimeError = result.getException().getMessage();
} catch (Throwable e) {
// Catch all the content that can be thrown
// Mark if the exception is unknown.
if (Objects.isNull(result.getException())) {
result.setException(new RuntimeException("Unknown Throwable"));
}
// Ensures the runtimeError has a value
if (StringUtils.isEmpty(runtimeError)) {
runtimeError = result.getException().getMessage();
}
}
// After the go() function, result.getException() will not be used, and runtimeError will be filled in the transactionInfo.
}
public void callToAddress() {
// CALL instruction is invoked, and play() is called
// No exception handling here; exceptions will propagate upwards.
pvm.play(program);
}
public void createContract() {
// CREATE directive is executed, and play() is called
// No exception handling here; exceptions will propagate upwards.
pvm.play(program);
}
step() function
// Step first catches the RuntimeException, deducts the Energy, and then throws
// Note, the exceptions thrown here are all RuntimeException
public void step(Program program) {
try {
// If the op is illegal, a PVMIllegalOperationException is thrown. In fact, the Invalid is written in advance in the bytecode, and the assert-style operation jumps to invalid.
OpCode op = OpCode.code(program.getCurrentOp());
if (op == null) {
throw Program.Exception.invalidOpCode(program.getCurrentOp());
}
switch (op) {
// 1. Calculate the energy required for the op
// 2. If deduction is not enough, throw an OutOfEnergyException
program.spendEnergy(energyCost, op.name());
// 3. Detect CPU time, timeout, then throw OutOfResourceException
program.checkCPUTimeLimit(op.name());
// 4. Actual execution of OP
// The focus here is on the CREATE instruction and the CALL instruction.
// The steps inside are all similar:
// 4.1 When the depth is up,it will push 0 to stack,then return
// 4.2 If there is value, the balance is insufficient, then push 0 to stack, and return
// 4.3 If there is value, transfer failes, throwing an exception of type RuntimeException.
// 4.4 (Requires execution) to execute the virtual machine
// 4.5 The result of executing the virtual machine, there is an exception. No exception will be thrown, only all Energy will be deducted and push 0 to stack.
// 4.6 The result of executing the virtual machine, if there is a revert, it will return Energy, and push 0 to stack.
// 4.7 Successful execution will return Energy and push 1 to stack.
// Note:
// Revert operation, and after exception, how to deal with, is determined by the virtual machine bytecode. Some will revert and some will be invalid.
// callToPrecompile fails, it will directly throw an exception of RuntimeException type
}
} catch (PVMRuntimeException e) {
// step will first catch the RuntimeException, deduct the Energy, and then throw
program.spendAllEnergy();
// Stop loop
program.stop();
// Throw an exception
throw e;
} finally {
}
}