-
Notifications
You must be signed in to change notification settings - Fork 344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to distinguish errors in smart contracts? #518
Comments
If you consider it's worth it, you can always add specific enums for identifying your errors. |
A best practice test for this looks like let res: InitResult = init(&mut deps, env, msg);
match res.unwrap_err() {
StdError::GenericErr { msg, .. } => {
assert_eq!(msg, "You can only use this contract for migrations")
}
err => panic!("Unexpected error: {:?}", err),
} The create assert_matches can potentially add syntactic sugar to it. I never used it but know it is used by the NEAR team. Probably worth trying it internally at some point and sharing experience to evaluate if it is worth an additional dependency.
This is true for Rust in general. However, |
This is what I come up with: pub enum PermissionErr {
Delegate {},
Redelegate {},
Undelegate {},
Withdraw {},
}
impl Into<String> for PermissionErr {
fn into(self) -> String {
return String::from(match self {
PermissionErr::Redelegate {} => "Redelegate is not allowed",
PermissionErr::Delegate {} => "Delegate is not allowed",
PermissionErr::Undelegate {} => "Undelegate is not allowed",
PermissionErr::Withdraw {} => "Withdraw is not allowed",
});
}
}
impl From<PermissionErr> for StdError {
fn from(err: PermissionErr) -> Self {
let msg: String = err.into();
StdError::generic_err(msg)
}
} From and Into traits enable implicit conversion from PermissionErr to StdErr and PermissionErr to string. Maybe somehow mixing this with |
Instead of impl From<PermissionErr> for StdError {
fn from(err: PermissionErr) -> Self {
StdError::generic_err(err.to_string())
}
} |
This was wrong, it is the other way round: implement |
If you are looking for a one-liner, the following should work as well: assert!(res.unwrap_err().to_string().contains("Redelegate is not allowed")); This is similar to what I wrote above but ignored the StdError enum case that was used to produce the error. |
What we could also do is allow Rather than return:
We would want something like:
We would need something like: /// do_handle should be wrapped in an external "C" export, containing a contract-specific function as arg
pub fn do_handle<T, U, E>(
handle_fn: &dyn Fn(
&mut Extern<ExternalStorage, ExternalApi, ExternalQuerier>,
Env,
T,
) -> Result<HandleResponse<U>, E>,
env_ptr: u32,
msg_ptr: u32,
) -> u32
where
T: DeserializeOwned + JsonSchema,
U: Serialize + Clone + fmt::Debug + PartialEq + JsonSchema,
E: Into<StdError>,
{ } |
Then you need to be able to convert all StdError created by the standard library into |
With this we could have: pub fn handle<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
msg: HandleMsg,
) -> Result<HandleResponse, MyCustomError> { } And then do something like: pub enum MyCustomError {
Std(StdError),
// this is whatever we want
Special{ count: i32 },
}
impl Into<StdError> for MyCustomError {
fn into(self) -> StdError {
match self {
Std(err) => err,
Special{count} => StdError::generic_err(format!("Special #{}", count)),
}
} And to make this nicer when using, eg. impl From<StdError> for MyCustomError {
fn from(orig: StdError) -> Self {
MyCustomError::Std(orig)
}
} It would buy us more clarity in the unit tests (even using |
@webmaster128 was responding with details to your comment when you wrote. I have not tested this code, but it seems like a way to do it. I was trying something similar with |
Looks good. I can try these patterns in the unit tests for one of the contracts I worked on recently, like subkeys or atomic swaps. |
Do we even need the structured error on the VM side at all? We do a lot of work serializing, deserializing StdError and mirroring to it Go and at the end of the day we throw away all the structure in https://github.com/CosmWasm/go-cosmwasm/blob/v0.10.0/lib.go#L148. If we give up StdError on the contract-VM interface, we can have a handle function that returns |
Interesting point. I tried to capture all the info I could for the go-cosmwasm interface, but it is not used in wasmd. When writing |
@maurolacy this will break wasm builds as the various |
I would consider this as a second step (and different PR). For one contract it is not much code to support the current interfaces (and a much smaller change). We can see what that looks like first. Once this works and we see it is adding noticeable overhead to the return errors, let's make the bigger infrastructure changes to make the error a string (which affects 3 repos). I do check the error cases in go-cosmwasm tests, but that is not strictly necessary. |
Integration tests can easily check the error type as well. Just as a string, not as an enum match. I really don't like the conversion |
You can easily test your approach without affecting the framework: pub fn handle<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
msg: HandleMsg,
) -> StdResult<HandleResponse> {
Ok(handle_impl(deps, env, msg)?)
}
pub fn handle_impl<S: Storage, A: Api, Q: Querier>(
deps: &mut Extern<S, A, Q>,
env: Env,
msg: HandleMsg,
) -> Result<HandleResponse, MyCustomError> { } and then test |
That is a very good first step |
What about just Excuse me if there's a strong a reason this wouldn't work (dependency issues?). I'm not familiar enough with the platform(s) yet. Update: I guess the point is that we need a generic handler / container at the higher levels. What about making more errors common across contracts? Many contracts need to check for / error on similar things. And for the other, contract specific errors, Does that makes any sense? |
Let's assume
That is what we tried with |
The issue is if we want to add custom errors on a per-contract basis, we need to downcast to a string (GenericErr) or use a pre-defined category in StdError. We can keep updating StdError with all categories that may be used in some contract, but that is quite unweildy, as it involves changing the cosmwasm-std, cosmwasm-vm, and go-cosmwasm. The idea was to expose these more detailed cases for unit tests, to allow better error checks, but keeping a similar public interface. I agree with Simon that StdError should be simpler if anything. And with Orkun, that he wants more than checking strings to verify the proper error case was triggered in unit tests |
I would leave So we make the unit tests (with backtraces) more specific and customizable, and simplify the public interface (vm, go-cosmwasm) at the same time. I guess this goes along with my emphasis on unit tests over integration tests recently |
This also goes along with respecting that unit tests and integration tests are fundamentally different things. It will make supporting other languages much easier since they only need to implement creating a compatible serialization of |
I will pull out two sub-issues on this that are much clearer in scope. (So someone can do it without reading all the comments) |
I'm not sure this has been discussed before, but I was thinking this morning that another (or complementary) option would be to move to "flat" (String) errors entirely, and provide macros and format strings for error creation and checking. All the errors / format strings for a contract can be listed in the same file for convenience ( This will ensure error style / handling is uniform across contracts / projects. And, the macros will also help with error creation / matching. The macros can define an "error code" or "error identifier", which can be matched easily (also using a macro). And that, without needing to extend / modify Simon is spot on about errors being in the end not more than string messages. This proposal extends or draws in that direction. Update: Upon further reflection, the original proposal sounds like a good compromise... this proposal of using only strings is probably a little bit "outdated". Now, given that Rust has macros... :-) |
As I've seen so far, developers define their errors using
StdErr:generic_err("this is error message")
.In testing, I need to check if a handler throws the expected error. Is pattern matching internal message string the only way?
Update after the discussion, I pulled an approach we agreed upon into two separate issues:
The text was updated successfully, but these errors were encountered: