-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Fix offer crossing with transfer fee over AMM and other fixes #5016
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #5016 +/- ##
=========================================
+ Coverage 71.0% 71.1% +0.1%
=========================================
Files 796 796
Lines 66912 66997 +85
Branches 10988 10979 -9
=========================================
+ Hits 47540 47635 +95
+ Misses 19372 19362 -10
|
auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); | ||
if (uNodeNext == 0) | ||
return nTrustLines == 2 || nTrustLines == 3; | ||
currentIndex = keylet::page(root, uNodeNext); |
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.
This linear search is kinda terrifying. I looked in a recent ledger and the largest number of owner directory pages owned by a single account was 284,820. But there were quite a few accounts with over 10,000 owner directory pages. This linear search signs us up for a potentially huge amount of computing. Calling this function from a transactor looks really scary to me.
Is there anything we know about AMM Liquidity Providers that will limit the number of directory pages that they can own? Is there any way that could be leveraged to limit this linear search to just a few directory nodes?
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.
There are two things in this function that prevent this possible huge number of iterations. One is that the limit is set to at most 10 iterations. Two is that if there is more than one Liquidity Provider (LP) then we'll break out of the loop a lot sooner because any LP can have at most three trustlines to AMM: one for LPToken and at most two for IOU. So we should break out after at most four iterations in the worst case scenario if there are more than one LP.
if ((lowLimit.getIssuer() != lpAccount &&
highLimit.getIssuer() != lpAccount))
return false;
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.
Sorry. I missed what limit
was up to when I first looked at the while
loop. But I still don't understand why 10 directory pages is a legit upper bound.
Owner directories can hold a lot of things other than trust lines. The number of Offer
s (both IOU and NFT), Escrow
s, PayChan
s, and DepositPreauth
s they can hold has a very large upper bound. Additionally, an account can own up to 250 tickets.
Now, maybe it's impossible for an AMM Liquidity Provider account to own any of these kinds of objects. In which case 10 iterations is (probably?) plenty. But I am not familiar enough with the AMM to know whether that's the case. If it is the case I think there should be a comment explaining why 10 iterations will always be sufficient.
There should probably also be some kind of error indication if the number of expected directory pages is ever surpassed. It may not be possible for an AMM Liquidity Provider to ever exceed that number of pages today. But people are excited about AMMs, which means rules will be changing in the future. So either the function must throw or the return value needs to be more complicated than a bool
. Maybe an Expected
?
Does that make any sense?
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.
We are iterating over the trustlines for AMM account. AMM account holds one AMM object and the rest are LP trustlines. Each LP has one trustline for LPToken. If an issuer created AMM then the issuer could have two more trustlines for IOU. Actually the function should return true
if trustlines >= 1 && truslines <= 3, not 2 or 3.
It makes sense to return an error in some cases: there are 0 trustlines, non-issuer LP has more than 1 trustline, an issuer LP has more than 3 trustlines, there are more than 1 AMM object. Expected
with tecINTERNAL
works.
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 think it is sufficient to just look for LPToken trustlines and return an error if it is 0 or greater than 1.
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 just did a little more reading about the AMM implementation. I'm sorry that a) I'm not really up on how the AMM works and b) that I did not look more closely at the loop before I commented.
Given the constraints as I (finally) understand them, we can have high confidence that the number of loops will be very bounded by adding one constraint to the loop. If nTrustLines > 3
return false
. I think if that constraint is added then (assuming there's only one AMM object) the number of directory pages examined cannot exceed 6.
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.
Yeah, I don't love this. I don't see a better solution 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.
I like all the error checking added to this function. Good changes!
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.
This looks OK to me, but I want to take another look tomorrow before I approve it.
@@ -103,44 +103,45 @@ AMMTestBase::testAMM( | |||
std::optional<std::pair<STAmount, STAmount>> const& pool, | |||
std::uint16_t tfee, | |||
std::optional<jtx::ter> const& ter, | |||
std::optional<FeatureBitset> const& features) | |||
std::vector<FeatureBitset> const& vfeatures) |
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 don't love this interface - running the same test with a different set of features seems like you'd want another function call (with possibly another ter
). I'm not that picky about test code, so no change needed if you prefer 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.
This ter
is for AMMCreate
failure, in which case the call back is not very useful. If ter
is included and a different behavior (AMMCreate fails or succeeds) is expected for different features then testAMM
should be called separately for each feature. Actually, ter
was not used at all in this function. I updated to use it.
The vector of features is intended for a case when AMMCreate succeeds, there are a number of transactions in the call back, and the transactions may behave differently or have different results for different features.
auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); | ||
if (uNodeNext == 0) | ||
return nTrustLines == 2 || nTrustLines == 3; | ||
currentIndex = keylet::page(root, uNodeNext); |
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.
Yeah, I don't love this. I don't see a better solution though.
@@ -165,31 +165,48 @@ adjustAmountsByLPTokens( | |||
|
|||
if (lpTokensActual < lpTokens) | |||
{ | |||
bool const ammRoundingEnabled = [&]() { | |||
if (auto const& rules = getCurrentTransactionRules(); | |||
rules && rules->enabled(fixAMMRounding)) |
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.
When this is merged with develop will fixAMMRounding
be an additional amendment? Or will you be leveraging the recently added fixAMMv1_1
? I'm fine either way, but it's worth being aware of the merge consequences.
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'll leverage fixAMMv1_1
.
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.
We should probably update this PR to use fixAMMv1_1.
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 need to merge with the other AMM PR #4983.
return {res.error(), false}; | ||
else if ( | ||
res.value() && | ||
withinRelativeDistance( |
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'm not understanding the reasoning behind the withinRelativeDistance
check. If it's the only trust line why do we check if there's a relative distance? I'm guessing this is as a sanity check. But I would probably make the sanity check looser if this is the case.
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.
Right, it's a sanity check and I made it more restrictive. Do you feel e-7 is sufficient?
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 don't think there's a value where we can safely say "the sanity check must always be below this value". This sanity check is really protecting against a bug in isOnlyLiquidityProvider
. As long as that's solid we don't need the sanity check. Thinking...
If this is the last remaining trust line and the sanity check fails, what should happen? If we return a tec
we may be saving ourselves against an exploit/bad bug at the cost of not allowing the last person to get their funds. If we don't have a check, we open ourselves up to bugs in isOnlyLiqueidityProvider
. If we keep it as-is, then what? Isn't that still a bug if the sanity check fails and we keep the code as-is?
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.
The only reason to have the sanity check is in case isOnlyLiquidityProvider
is not solid. I think it looks right but then essentially all tokens are assigned to the user. I don't see a way around it.
If the sanity check fails then it's still a bug, only more obscure. The transaction might succeed or might fail. It might make sense to return tec
. The user can still withdraw with a fewer tokens.
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 vote that we loosen the constraint considerably - maybe 10^-1? 10^-2? - and also change this to return a tec if the sanity check fails.
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 was thinking e-4 at least to prevent/minimize a possible exploit if there is one. I'm fine with e-2 if everyone votes for it.
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.
It's your call. I'm also find with e-4.
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.
Let's wait for @scottschurr vote.
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 don't really have an opinion, but I trust @gregtatcam's take. Shall we go with e-4?
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.
Let's do e-3.
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.
👍 Looks good to me. I found one typo in a comment, but nothing that critically needs to be changed as far as I was able to see.
std::uint8_t nTrustLines = 0; | ||
// Liquidity Provider (LP) must have one LPToken trustline | ||
std::uint8_t nLPTokenTrustLines = 0; | ||
// There are at most two IOU truslines. One or both could be to the LP |
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.
Typo: trusline -> trustline
// AMM LP has at most three trustlines and only one AMM object must exist. | ||
// If there are more than five objects then it's either an error or | ||
// there are more than one LP. Ten pages should be sufficient to include | ||
// five objects. |
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 these comments! They helped me a lot.
isOnlyLiquidityProvider( | ||
ReadView const& view, | ||
AccountID const& ammAccount, | ||
Issue const& ammIssue, |
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.
Good call on passing in the Issue
. Thanks.
auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); | ||
if (uNodeNext == 0) | ||
return nTrustLines == 2 || nTrustLines == 3; | ||
currentIndex = keylet::page(root, uNodeNext); |
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 like all the error checking added to this function. Good changes!
return {res.error(), false}; | ||
else if ( | ||
res.value() && | ||
withinRelativeDistance( |
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 don't really have an opinion, but I trust @gregtatcam's take. Shall we go with e-4?
The fix is to return the actual adjusted lp tokens and amounts by the function.
Single path AMM offer has to factor in the transfer in rate when calculating the upper bound quality and the quality function because single path AMM's offer quality is not constant. This fix factors in the transfer fee in BookStep::adjustQualityWithFees().
Due to the rounding, LPTokenBalance of the last Liquidity Provider (LP), might not match this LP's trustline balance. This fix sets LPTokenBalance on last LP withdrawal to this LP's LPToken trustline balance.
261e750
to
15390be
Compare
High Level Overview of Change
This PR addresses three issues:
and LPTokenBalance when last Liquidity Provider withdraws all tokens
Type of Change
.gitignore
, formatting, dropping support for older tooling)Before / After
qualityUpperBound()
andQualityFunction
calculation doesn't factor in the transfer fee on offer crossing.This is correct behavior when crossing an account offer. Single-path with AMM limits the
out
value basedon the limitQuality. If the transfer fee is not factored in then the out value is going to be such that
the generated AMM offer quality matches the limitQuality. But the effective quality is smaller because
takerPays
amount factors in the transfer fee when the strand's quality is calculated. The fix factors inthe transfer fee when the quality is adjusted for the fees on offer crossing.
adjustAmountsByLPTokens()
function is not returning adjusted tokens and amounts in some cases.The fix returns the adjusted LPToken and adjusted amounts.
This results in a dust amount left in the balances on the last LP withdrawal or the withdrawal transaction failing. The fix
sets the LPTokenBalance to the trustline balance if LP is the only remaining LP on withdrawal transaction.
Test Plan
Updated AMM_test and AMMExtended unit-tests.