Skip to content
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 finance USD conversion mechanism #1177

Merged
merged 5 commits into from
Jun 25, 2020
Merged

Conversation

Evalir
Copy link
Contributor

@Evalir Evalir commented Jun 24, 2020

Fixes a problem that arose with the move to using bn.js everywhere, where the conversion from Token Amount to USD was being done incorrectly, as bn.js itself can't handle decimals, and they were being passed onto the function. This adds a few things:

  • A function to "shift" numbers a certain amount of decimals safely and counting the amount of places it was shifted (toDecimal and fromDecimal exist, but required a bit of hacking to make them work for this, so I implemented another version)
  • A function to transform the passed token taking into account its decimals and the exchange rate correctly, using just bn.js and not relying on pure Javascript numbers.
  • Moves all conversion related things to conversion-utils.js to remove bloat from the Balances.js file.

Few screenshots from the AAVE Org (where the problem arose due to WBTC breaking the previous implementation):
Screen Shot 2020-06-23 at 9 59 32 PM
Screen Shot 2020-06-23 at 9 59 43 PM

Comment on lines 24 to 31
// Apart from always considering the USD decimals (2),
// if there's the strange case that the above is negative,
// we take it as a carry as we know we already shifted to far,
// and will compensate by shifting the token amount by this much
const carryAmount =
decimalsToShift.toNumber() < 0
? decimalsToShift.add(USD_DECIMALS)
: USD_DECIMALS
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll admit I haven't found practical examples, but I'm just making sure! 😃

@coveralls
Copy link

Coverage Status

Coverage remained the same at 97.642% when pulling 932c6e4 on fix-finance-usd-conversion into ea97425 on master.

Copy link
Contributor

@bpierre bpierre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ouch I completely missed that!

I was wondering if a good strategy could be to do:

  • Scale the conversion ratio up to the same number of decimals than the given token.
  • Use divideRoundBigInt() to get the result in USD and scale the amount down to 2 decimals.
  • Use formatTokenAmount(usdAmount, 2) to display it.

Let me know what you think!

@Evalir
Copy link
Contributor Author

Evalir commented Jun 24, 2020

This sounds good! Wondering on how would we scale up the conversion ratio up to the proper number of decimals without potentially running into overflow issues though? It's really why there's a lot of string manipulation here.

Copy link
Contributor

@sohkai sohkai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also do this for the Agent app!

apps/finance/app/src/lib/conversion-utils.js Outdated Show resolved Hide resolved
// Return it to its original precision
// Note that we don't have to subtract the "extra carry"
// as it's undone during the division
.mul(precisionTarget)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we need to consider any USD decimals here, as our "USD" converted amount is still in the original token's decimals after the conversion.

Wondering if we can simplify everything down to:

const [whole = '', dec = ''] = convertRate.split('.')
const parsedDec = dec.replace(/0*$/, '')
const carryAmount = parsedDec.length
const rate = `${whole}${parsedDec}`.replace(/^0*/, '')

return amount
  .mul(new BN('10').pow(carryAmount))
  .div(new BN(rate))

As we don't need to use the decimals at all.

Copy link
Contributor

@bpierre bpierre Jun 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could completely cut the string manipulations by multiplying the two values and relying on formatTokenAmount(). What do you guys think?

PR: #1180

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only worry with the above is overflow issues on many tokens due to the ceiling for safe numbers in Javascript (Number.MAX_SAFE_INTEGER) is around 9*10^15, having 3 decimals less than most tokens, which have 18. This is really the only reason why I considered doing this string manipulation, rather than just shifting the decimals by normal math.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the loss of precision with Number is exactly why are using BN.js, and the proposed solution is taking care of the token decimals… but I realize now that rate itself, once scaled up, could also easily become too large :-(

I am wondering if it would be easier to implement TokenAmount.convert() directly? We would use a mix of string splitting for the rate, and BigInt calculations otherwise.

Copy link
Contributor Author

@Evalir Evalir Jun 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could also easily become too large :-(

Yeah, and the solutions are not pretty :( In general these conversions are a headache when maintaining them in BigInts.

I am wondering if it would be easier to implement TokenAmount.convert() directly? We would use a mix of string splitting for the rate, and BigInt calculations otherwise.

I think it could be good! it's also pretty high leverage as doing these conversions is both hard to get 100% right, and at the same time extremely common to do. Should we wait for .convert() to be implemented, or should we merge and then upgrade once it's done? Asking this mainly because it's breaking Aave's finance app.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that we could add it to TokenAmount directly and use it from here, but we could merge this first to make sure it gets fixed quick yes.

I added the commit with the Jest configuration.

What would you think of:

  1. Perhaps simplify a bit the process, following @sohkai’s comment.
  2. Add some tests to make sure everything works fine.
  3. Merge it until TokenAmount.format() is ready?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! Let's do that.

@Evalir Evalir merged commit 41ac706 into master Jun 25, 2020
@Evalir Evalir deleted the fix-finance-usd-conversion branch June 25, 2020 18:53
Evalir added a commit that referenced this pull request Jul 9, 2020
Evalir added a commit that referenced this pull request Jul 14, 2020
* Agent: replicate rate conversion strategy from #1177

* Remove unused parameter

* Remove redundant return on map :)
ramilexe pushed a commit to ConsiderItDone/aragon-apps that referenced this pull request Dec 9, 2021
* Fix finance USD conversion mechanism

* Add Jest

* Simplify conversion function

* Add some tests for the conversion utility

Co-authored-by: Pierre Bertet <[email protected]>
ramilexe pushed a commit to ConsiderItDone/aragon-apps that referenced this pull request Dec 9, 2021
* Agent: replicate rate conversion strategy from aragon#1177

* Remove unused parameter

* Remove redundant return on map :)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants