-
Notifications
You must be signed in to change notification settings - Fork 15
More descriptive error upon transfer simulation failure when placing order #1458
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1458 +/- ##
==========================================
- Coverage 65.76% 65.40% -0.37%
==========================================
Files 155 159 +4
Lines 30396 30049 -347
==========================================
- Hits 19991 19654 -337
+ Misses 10405 10395 -10 |
// If fee and sell amount overflow u256 | ||
SellAmountOverflow, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be OK with a generic 500-error in these cases (unless this actually is happening to our users).
return Ok((order, unsubsidized_fee)); | ||
} | ||
|
||
if !self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it maybe makes sense to change BalanceFetching
to have something like:
trait BalanceFetching {
fn simulate_transfer(...) -> Result<(), TransferError>;
}
enum TransferError {
InsufficientBalance,
InsufficientAllowance,
Other(...),
}
The advantage of doing it this way is that we keep the details of testing transfers and the possible places it could go wrong in the BalanceFetching
component.
Some other advantages:
- It allows us to use a JSON RPC batch call for the balance and allowance query
- We won't accidentally forget to update
has_sufficient_balance
to also pass in theSellTokenSource
so that it works with Vault internal balances once we support it.
.balance_fetcher | ||
.has_sufficient_balance(order_creation.sell_token, owner, min_balance) | ||
.await | ||
.unwrap_or(false) | ||
{ | ||
return Err(ValidationError::InsufficientFunds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uber-naming-nit: This should probably be renamed to InsufficientBalance
now.
@anxolin would renaming this error code be a breaking change for the FE?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @nlordell for notifying, indeed is relevant.
@@ -215,6 +227,32 @@ impl BalanceFetching for Web3BalanceFetcher { | |||
}; | |||
Ok(success) | |||
} | |||
|
|||
async fn has_sufficient_balance(&self, token: H160, from: H160, amount: U256) -> Result<bool> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory, this also needs to pass in the SellTokenSource
because we would need to query the Vault::internal_balance
in the case of SellTokenSource::Internal
.
instance.allowance(from, self.vault_relayer).call().await? >= amount | ||
} | ||
SellTokenSource::External => { | ||
self.can_manage_user_balance_call(token, from, amount).await |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One slight worry is that this has an implicit requirement that balance_fetcher.has_sufficient_balance()
was previously checked, otherwise has_sufficient_allowance
would generate false-positives when the user has a sufficient ERC20 allowance on the Vault and set relayer approval but has not more ERC20 token balance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general code looks good modulo false positives in has_sufficient_allowance
when the user has approval, but not enough ERC20 token balance. I think it should be possible to:
- batch the
ERC20::balance_of
,ERC20::allowance
andVault::get_relayer_approval
calls to determine the reason we can't transfer - hide the "complexities" of this check in the
account_balances
module and not have to "leak" it in the order creation logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to finally have some refined error messages.
// TODO - None happens when checked_add overflows - not insufficient funds... | ||
// This error should be changed to SellAmountOverflow. | ||
None => return Err(ValidationError::InsufficientFunds), | ||
None => return Err(ValidationError::SellAmountOverflow), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for todo-ing this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lgtm, although I would agree with @nlordell comment about converting return value of can_transfer
from Result<bool>
to Result<(), TransferError>
, would make the code a bit clearer.
I removed Would you be able to take another look if this works now? |
pub enum TransferSimulationResult { | ||
Ok, | ||
InsufficientAllowance, | ||
InsufficientBalance, | ||
TransferFailed, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ubernit: Not a fan of these CustomResult
types because they miss out on all the Result
goodies (?
support, all the standard library methods like map
, unwrap_or_else
, etc.) - I would much prefer:
pub enum TransferSimulationResult { | |
Ok, | |
InsufficientAllowance, | |
InsufficientBalance, | |
TransferFailed, | |
} | |
pub enum TransferSimulationError { | |
InsufficientAllowance, | |
InsufficientBalance, | |
TransferFailed, | |
Other(anyhow::Error), | |
} |
And then return Result<(), TransferSimulationError>
.
In terms of how to go from anyhow::Error
to TransferSimulationError
, you can either
impl From<anyhow::Error> for TransferSimulationError {
fn from(err: anyhow::Error) -> Self {
Self::Other(err)
}
}
Or, you can manually do it at the call site:
erc20_balance_query(&mut batch, token, from, self.vault_relayer)
.await
.map_err(TransferSimulationError::Other)?;
@@ -748,7 +784,7 @@ mod tests { | |||
.await | |||
.unwrap_err() | |||
), | |||
"InsufficientFunds" | |||
"SellAmountOverflow" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: completely unrelated to this PR (so feel free to not change that here), but this makes the test rely on how #[derive(Debug)]
and seems a bit flaky to me... I think it would be much better to:
assert!(matches!(
validator.validate_and_construct_order(...).await,
Err(ValidationError::SellAmountOverflow),
));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - just a small nit regarding the return type which feels a bit "un-Rust-y". Not blocking though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
}) | ||
.await, | ||
Err(PartialValidationError::TransferEthToContract) | ||
)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
# Summary Adds new errors for the backend. Now they have more granular description of the error. See gnosis/gp-v2-services#1458 (comment)
Fixes #1085
We have gotten mutliple complaints from people trying to integrate with CowSwap that our order creation errors are the same (InsufficientFunds) in these two cases
This PR makes these order creation errors distinct. In particular we use the following errors in the following scenarios:
In order to not create more roundtrips in the happy path (e.g. for frontends that make sure users cannot place orders without having given prior approval) we keep the transfer simulation (which covers all 3 cases in one call) and only check the exact failure reason in case it fails.
Test Plan
Added an integration test for the balance fetcher functions. It would be nice to use the order placement script with a custom endpoint (so I can easily create test orders locally)