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

test: StableMath rounding #526

Merged
merged 14 commits into from
Jun 5, 2024
Merged

test: StableMath rounding #526

merged 14 commits into from
Jun 5, 2024

Conversation

0xJabberwock
Copy link
Contributor

@0xJabberwock 0xJabberwock commented Apr 24, 2024

Description

Since a test framework for the rounding in the math libraries was deemed necessary, I introduce in this PR a RoundingMock which emulates the pertinent FixedPoint functions with the possibility of setting the rounding direction via parameter.

By taking advantage of RoundingMock in the context of StableMathMock, mock functions with a configurable rounding permutation are made possible; for them to be meaningful, their logic should be equivalent to the base ones'.

StableMathTest tests the rounding of the functions of interest by comparing their base result with their mock result, so it is assertable whether the rounding direction permutation is actually favoring the protocol, not so much or not at all. This methodology has proven to be effective, as a few rounding bugs have been encountered (the resolution of which is out of the scope of this PR).

Besides, I refactored the StableMathMock and FixedPointMock so their naming and arrangement matched the base contracts'.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Dependency changes
  • Code refactor / cleanup
  • Documentation or wording changes
  • Other

Checklist:

  • The diff is legible and has no extraneous changes
  • Complex code has been commented, including external interfaces
  • Tests have 100% code coverage
  • The base branch is either main, or there's a description of how to merge

Issue Resolution

Closes #523.

@0xJabberwock 0xJabberwock marked this pull request as ready for review April 24, 2024 22:33
Comment on lines 132 to 137
assertLe(
invariant,
invariantRoundedDown,
"Output should be less than or equal to the rounded down mock value."
);
assertLe(invariant, invariantRoundedUp, "Output should be less than or equal to the rounded up mock value.");
Copy link
Collaborator

Choose a reason for hiding this comment

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

mhmhmh, how come? raw is < roundDown & raw < roundUp?

Copy link
Contributor Author

@0xJabberwock 0xJabberwock Apr 29, 2024

Choose a reason for hiding this comment

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

In the StableMath functions, the protocol aims for computing a value either as small as possible or as large as possible by means of rounding; in order to achieve this, it may use different rounding directions during the calculations. Then, the objective of the test case is to verify that the implemented rounding permutation behaves more effectively than other solutions (e.g., always rounding down or always rounding up, which are the permutations that RoundingMock offers).

Comment on lines 336 to 337
// BUG: Revise rounding in `computeBptOutGivenExactTokensIn()`
// assertLe(bptOutGivenExactTokensIn, bptOutGivenExactTokensInRoundedUp, "Output should be less than or equal to the rounded up mock value.");
Copy link
Collaborator

@wei3erHase wei3erHase Apr 25, 2024

Choose a reason for hiding this comment

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

pinging @jubeira here 👀

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting finding.

At first glance the current rounding directions make sense; I don't see anything that is clearly wrong. I'd need to take a deeper look.

Copy link
Collaborator

@wei3erHase wei3erHase left a comment

Choose a reason for hiding this comment

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

looks amazing! i'm not entirely sure about the BUGs mentioned tbh, i'd add some comments explaining the expected behaviour, and please explain why the amounts are lesser than the rounded ones (despite the rounding direction)

@wei3erHase wei3erHase requested a review from jubeira April 25, 2024 08:02
Copy link
Contributor

@jubeira jubeira left a comment

Choose a reason for hiding this comment

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

Thanks for putting this together @0xJabberwock, interesting approach.

Overall the tests make sense. I'd need to take a deeper look at the code to understand why some of the test cases are not passing, but if the tests are correct we might find something.

My main concern with this is that we need to copy the code to the mock to be able to override the rounding directions. In principle I don't see an easy way around this, but I do think it could become a maintenance issue. E.g. if I modify stable math, the tests will still pass and won't catch potential mistakes that I might be introducing unless I also modify the mock, which defeats part of the purpose of the tests. Do you think there's a way to mitigate this problem?

pkg/solidity-utils/contracts/test/StableMathMock.sol Outdated Show resolved Hide resolved
balances[tokenIndexIn] -= tokenAmountIn;
}

return balances[tokenIndexOut] - finalBalanceOut - 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why -1? Is that a round down?

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 haven't found an explicit explanatory comment regarding this line in StableMath (from which the logic was copied and pasted). It might have to do with the Newton-Raphson approximation.

In the context of the whole computation, it may work as a rounding mechanism given that it subtracts in computeOutGivenExactIn() but adds in computeInGivenExactOut().

Copy link
Contributor

@joaobrunoah joaobrunoah May 7, 2024

Choose a reason for hiding this comment

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

It makes sense. Exact_In wants to round down the tokenOut result to ensure it's in the vault's favor, and Exact_Out wants to round up the amount of tokens in, so the user pays 1 extra token to account for rounding errors.

I think that's unrelated to Newton-Raphson since it's just a numeric method for iteratively achieving a function result. Instead, it seems to be related to the fact that with a limited number of decimals, rounding issues occur, and we need to make sure the vault doesn't pay for it. (By the way, sorry if I'm being obvious, hahaha)

balances[tokenIndexOut] += tokenAmountOut;
}

return finalBalanceIn - balances[tokenIndexIn] + 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same, why +1? Is it rounding up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same answer as in #526 (comment).

pkg/solidity-utils/contracts/test/StableMathMock.sol Outdated Show resolved Hide resolved
pkg/solidity-utils/contracts/test/RoundingMock.sol Outdated Show resolved Hide resolved
Comment on lines 336 to 337
// BUG: Revise rounding in `computeBptOutGivenExactTokensIn()`
// assertLe(bptOutGivenExactTokensIn, bptOutGivenExactTokensInRoundedUp, "Output should be less than or equal to the rounded up mock value.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting finding.

At first glance the current rounding directions make sense; I don't see anything that is clearly wrong. I'd need to take a deeper look.

Copy link

openzeppelin-code bot commented May 1, 2024

test: StableMath rounding

Generated at commit: 21ab6b76aed85d67f656096adeebea3777363656

🚨 Report Summary

Severity Level Results
Contracts Critical
High
Medium
Low
Note
Total
1
1
0
10
36
48
Dependencies Critical
High
Medium
Low
Note
Total
0
0
0
0
0
0

For more details view the full report in OpenZeppelin Code Inspector

Copy link
Collaborator

@EndymionJkb EndymionJkb left a comment

Choose a reason for hiding this comment

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

Juani: My main concern with this is that we need to copy the code to the mock to be able to override the rounding directions. In principle I don't see an easy way around this, but I do think it could become a maintenance issue.

We are unlikely to change StableMath at this point, but we could at least add a note in the StableMath.sol header noting that there is a mock, and any changes need to be propagated there.

My concern with the approach is that it is a fairly blunt instrument. Many functions have multiple operations with rounding in different directions that influence/reinforce each other. If a function has three operations (down,up,down), we're testing 1) all up; 2) all down; 3) native implementation. So, (down,down,down), (up,up,up), and (down,up,down) -- but not (up,down,up), (up,up,down), (down, up, up), (down,down,up), or (up,down,down). Maybe one of those combinations is actually best.

The most general would be a more white box approach where each function took an array of rounding inputs according to the number of rounding operations, and roundingMock direction was set before each operation, instead of once at the beginning (e.g., the mulDown,divUp functions could all have a rounding direction parameter, vs. reading a global variable). The test functions could then be called with all 2^n combinations

@0xJabberwock
Copy link
Contributor Author

After af3778f, both RoundingMock and StableMathMock can be made libraries—is there any reason not to make them so?

On a similar note, I've seen that FixedPointMock and WeightedMathMock are only there to externalize their underlying library's functions. What is the rationale behind deploying such mocks rather than directly interacting with the libraries in the test files?

Apart from that, currently StableMath.computeInvariant() does not call any of the FixedPoint functions and, thus, its testing artifacts in StableMathMock and StableMathTest may be deleted.

@jubeira
Copy link
Contributor

jubeira commented May 3, 2024

On a similar note, I've seen that FixedPointMock and WeightedMathMock are only there to externalize their underlying library's functions. What is the rationale behind deploying such mocks rather than directly interacting with the libraries in the test files?

@0xJabberwock Hardhat :)

Copy link
Collaborator

@wei3erHase wei3erHase left a comment

Choose a reason for hiding this comment

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

LGTM! 🚀
@jubeira would like your 👍 before merging, this one adds also #549 removing the unused StableMath methods

Copy link
Contributor

@jubeira jubeira left a comment

Choose a reason for hiding this comment

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

Better late than never; looks good! Let's 🚢

@jubeira jubeira merged commit 1e642bf into main Jun 5, 2024
10 checks passed
@jubeira jubeira deleted the test/rounding-math branch June 5, 2024 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Test rounding scenarios in Base Math, Weighted Math and Stable Math Test StableMath rounding
5 participants