Skip to content

Commit

Permalink
Polkadot: Support errors according to solc (hyperledger-solang#1516)
Browse files Browse the repository at this point in the history
Implement `Panic` and custom errors for the Polkadot target:
- Allow catching `Panic` in sema
- Allow custom errors in sema
- Implement selector  calculation for custom errors
- Refactor try-catch in codegen to allow catch clauses on `Panic` errors
- Add any custom errors in the metadata
  • Loading branch information
xermicus authored Sep 14, 2023
1 parent e1a3ada commit efc2dd9
Show file tree
Hide file tree
Showing 28 changed files with 711 additions and 234 deletions.
10 changes: 7 additions & 3 deletions docs/language/statements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,6 @@ An internal function cannot be called from a try catch statement. Not all proble
for example, out of gas cannot be caught. The ``revert()`` and ``require()`` builtins may
be passed a reason code, which can be inspected using the ``catch Error(string)`` syntax.

.. note::
On Polkadot, catching `Panic(uint256)` or custom errors is not supported yet.

.. warning::
On Solana, any transaction that fails halts the execution of a contract. The try-catch statement, thus,
is not supported for Solana contracts and the compiler will raise an error if it detects its usage.
Expand All @@ -145,3 +142,10 @@ This might be useful when no error string is expected, and will generate shorter

.. include:: ../examples/polkadot/statement_try_catch_no_error_handling.sol
:code: solidity

.. note::

Try-catch only supports ``Error`` and ``Panic`` errors with an explicit catch clause.
Calls reverting with a `custom error <https://docs.soliditylang.org/en/latest/abi-spec.html#errors>`_
will be caught in the catch-all clause (``catch (bytes raw)``) instead.
If there is no catch-all clause, custom errors will bubble up to the caller.
13 changes: 6 additions & 7 deletions docs/targets/polkadot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ for more information about when ``Panic`` might be returned).
Uncaught exceptions from calling and instantiating contracts or transferring funds will be bubbled
up back to the caller.

.. note::

Solidity knows about ``Error``, ``Panic`` and custom errors. Please, also refer to the
`Ethereum Solidity documentation <https://docs.soliditylang.org/en/latest/abi-spec.html#errors>`_,
for more information.

The metadata contains all error variants that the contract `knows` about in the ``lang_error`` field.

.. warning::
Expand Down Expand Up @@ -114,13 +120,6 @@ In the following example, the ``Panic`` variant of ``lang_error`` is of type ``1
From this follows that error data matching the ``Panic`` selector of `0x4e487b71` can be decoded
according to type ``10`` (where the decoder must exclude the first 4 selector bytes).

.. note::

Ethereum Solidity knows about ``Error``, ``Panic`` and
`custom errors <https://docs.soliditylang.org/en/latest/abi-spec.html#errors>`_.
Solang does not yet support custom errors. For now, only ``Error`` (selector of `0x08c379a0`)
and ``Panic`` (selector of `0x4e487b71`) are returned and occur in the metadata.

The general process of decoding the output data of Solang Solidity contracts is as follows:

1. The compiler of the contract must be Solang (check the ``compiler`` field in the contract metadata).
Expand Down
6 changes: 0 additions & 6 deletions integration/polkadot/debug_buffer_format.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ describe('Deploy debug_buffer_format.sol and test the debug buffer formatting',
let conn = await createConnection();
const alice = aliceKeypair();


let deployed_contract = await deploy(
conn,
alice,
Expand All @@ -22,16 +21,13 @@ describe('Deploy debug_buffer_format.sol and test the debug buffer formatting',
deployed_contract.address
);



let res = await debug_buffer(conn, contract, "multiple_prints", [])
expect(res).toEqual(`print: Hello!,
call: seal_debug_message=0,
print: I call seal_debug_message under the hood!,
call: seal_debug_message=0,
`)


let res1 = await debug_buffer(conn, contract, "multiple_prints_then_revert", [])
expect(res1).toEqual(`print: Hello!,
call: seal_debug_message=0,
Expand All @@ -41,8 +37,6 @@ runtime_error: sesa!!! revert encountered in debug_buffer_format.sol:10:9-26,
call: seal_debug_message=0,
`)



conn.disconnect();
});
});
2 changes: 1 addition & 1 deletion integration/polkadot/runtime_errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('Deploy runtime_errors.sol and test the debug buffer', () => {
expect(res7).toContain(`runtime_error: assert failure in runtime_errors.sol:32:16-24`)

let res8 = await debug_buffer(conn, contract, `i_will_revert`, [], 0);
expect(res8).toContain(`runtime_error: revert encountered in runtime_errors.sol:77:9-17`)
expect(res8).toContain(`runtime_error: unspecified revert encountered in runtime_errors.sol:77:9-17`)

let res9 = await debug_buffer(conn, contract, `write_integer_failure`, [1], 0);
expect(res9).toContain(`runtime_error: integer too large to write in buffer in runtime_errors.sol:82:18-31`)
Expand Down
48 changes: 48 additions & 0 deletions integration/polkadot/try_catch.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
contract TryCatchCaller {
constructor() payable {}

function test(uint128 div) public payable returns (uint128) {
TryCatchCallee instance = new TryCatchCallee();

try instance.test(div) returns (uint128) {
return 4;
} catch Error(string reason) {
assert(reason == "foo");
return 1;
} catch Panic(uint reason) {
assert(reason == 0x12);
return 0;
} catch (bytes raw) {
if (raw.length == 0) {
return 3;
}
if (raw == hex"bfb4ebcf") {
return 2;
}
}

assert(false);
}
}

contract TryCatchCallee {
error Foo();

// div = 0: Reverts with Panic error
// div = 1: Reverts with Error error
// div = 2: Reverts with Foo error
// div = 3: Reverts with empty error
// div > 3: Doesn't revert
function test(uint128 div) public pure returns (uint128) {
if (div == 1) {
revert("foo");
}
if (div == 2) {
revert Foo();
}
if (div == 3) {
revert();
}
return 123 / div;
}
}
35 changes: 35 additions & 0 deletions integration/polkadot/try_catch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import expect from 'expect';
import { createConnection, deploy, aliceKeypair, query, } from './index';
import { ContractPromise } from '@polkadot/api-contract';
import { ApiPromise } from '@polkadot/api';
import { KeyringPair } from '@polkadot/keyring/types';


describe('Deploy and test the try_catch contract', () => {
let conn: ApiPromise;
let caller: ContractPromise;
let alice: KeyringPair;

before(async function () {
alice = aliceKeypair();

conn = await createConnection();
await deploy(conn, alice, 'TryCatchCallee.contract', 0n);
const caller_contract = await deploy(conn, alice, 'TryCatchCaller.contract', 1000000000n);
caller = new ContractPromise(conn, caller_contract.abi, caller_contract.address);
});

after(async function () {
await conn.disconnect();
});

it('Tests all catch clauses', async function () {
this.timeout(20000);

for (let in_out = 0; in_out < 5; in_out++) {
console.log("Testing case: " + in_out);
const answer = await query(conn, alice, caller, "test", [in_out]);
expect(answer.output?.toJSON()).toStrictEqual(in_out);
}
});
});
2 changes: 1 addition & 1 deletion integration/solana/runtime_errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('Runtime Errors', function () {
let res = await program.methods.iWillRevert().simulate();
} catch (e: any) {
const logs = e.simulationResponse.logs;
expect(logs).toContain(`Program log: runtime_error: revert encountered in runtime_errors.sol:69:9-17,
expect(logs).toContain(`Program log: runtime_error: unspecified revert encountered in runtime_errors.sol:69:9-17,
`)
}

Expand Down
28 changes: 16 additions & 12 deletions src/abi/polkadot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use semver::Version;
use solang_parser::pt;

use crate::{
codegen::revert::{ERROR_SELECTOR, PANIC_SELECTOR},
codegen::revert::{SolidityError, ERROR_SELECTOR, PANIC_SELECTOR},
sema::{
ast::{self, ArrayLength, EventDecl, Function},
tags::render,
Expand Down Expand Up @@ -91,29 +91,26 @@ fn int_to_ty(ty: &ast::Type, registry: &mut PortableRegistryBuilder) -> u32 {
fn lang_error(
ns: &ast::Namespace,
reg: &mut PortableRegistryBuilder,
errors: &[(&str, u32, Vec<ast::Type>)],
errors: Vec<(String, [u8; 4], Vec<ast::Type>)>,
) -> TypeSpec<PortableForm> {
let variants = errors.iter().enumerate().map(|(n, (name, selector, ty))| {
let struct_fields = ty
.iter()
.map(|ty| resolve_ast(ty, ns, reg).into())
.map(|field| Field::new(None, field, None, Default::default()))
.collect::<Vec<_>>();
let ty = Type::new(
path!(format!("0x{}", hex::encode(selector.to_be_bytes()))),
vec![],
TypeDef::Composite(TypeDefComposite::new(struct_fields)),
Default::default(),
);
let path = path!(format!("0x{}", hex::encode(selector)));
let type_def = TypeDef::Composite(TypeDefComposite::new(struct_fields));
let ty = Type::new(path, vec![], type_def, Default::default());
Variant {
name: name.to_string(),
fields: vec![Field::new(None, reg.register_type(ty).into(), None, vec![])],
index: n.try_into().expect("we do not allow custome error types"),
docs: Default::default(),
}
});
let type_def = TypeDefVariant::new(variants);
let path = path!("SolidityError");
let type_def = TypeDefVariant::new(variants);
let id = reg.register_type(Type::new(path.clone(), vec![], type_def, vec![]));
TypeSpec::new(id.into(), path)
}
Expand Down Expand Up @@ -504,10 +501,17 @@ pub fn gen_project(contract_no: usize, ns: &ast::Namespace) -> InkProject {
))
.done();

let error_definitions = &[
("Error", ERROR_SELECTOR, vec![ast::Type::String]),
("Panic", PANIC_SELECTOR, vec![ast::Type::Uint(256)]),
let mut error_definitions = vec![
("Error".into(), ERROR_SELECTOR, vec![ast::Type::String]),
("Panic".into(), PANIC_SELECTOR, vec![ast::Type::Uint(256)]),
];
for (error_no, err) in ns.errors.iter().enumerate() {
let name = err.name.clone();
let exprs = Vec::new();
let selector = SolidityError::Custom { error_no, exprs }.selector(ns);
let types = err.fields.iter().map(|f| f.ty.clone()).collect();
error_definitions.push((name, selector, types));
}

let spec = ContractSpec::new()
.constructors(constructors)
Expand Down
Loading

0 comments on commit efc2dd9

Please sign in to comment.