-
Notifications
You must be signed in to change notification settings - Fork 20
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: rounding in the protocol's favor #468
Conversation
fix: rounding in the protocol's favour
🚨 Report Summary
For more details view the full report in OpenZeppelin Code Inspector |
for (uint256 i = 0; i < balances.length; i++) { | ||
for (uint256 i = 0; i < balances.length; ++i) { |
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.
Prefix increments and decrements (++i
; --i
) are cheaper than postfix increments and decrements (i++
; i--
) in terms of gas.
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.
how come no gas snapshots have changed? 👀
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 there are a lot of loops around the codebase, let's try that in a new PR and see the gas snapshots improvements (avoid cross-function prs when possible)
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.
Also, there's a 1% threshold on the gas snapshots, so maybe they did change, just less 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've made further research regarding the gas costs incurred when using prefix or postfix increments or decrements, specifically within for
loop declarations. It seems that compiling via_ir = true
equals the gas expenditures. For more details, refer to ethereum/solidity#14595.
if (newBalances[index] > invariantRatio.mulUp(currentBalances[index])) { | ||
uint256 taxableAmount = newBalances[index] - invariantRatio.mulUp(currentBalances[index]); | ||
if (newBalances[index] > invariantRatio.mulDown(currentBalances[index])) { | ||
uint256 taxableAmount = newBalances[index] - invariantRatio.mulDown(currentBalances[index]); |
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 protocol is interested in taxing a larger amount in order to charge more swap fees which, in turn, help in reducing the amount of BPT
to bid during addLiquidityUnbalanced
.
// mulUp/divDown maximize the amount of tokens burned for the security reasons | ||
bptAmountIn = totalSupply.mulUp(currentInvariant - invariantWithFeesApplied).divDown(currentInvariant); | ||
// mulUp/divUp maximize the amount of tokens burned for the security reasons | ||
bptAmountIn = totalSupply.mulUp(currentInvariant - invariantWithFeesApplied).divUp(currentInvariant); |
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 protocol is interested in maximizing the amount of BPT
to ask for during removeLiquiditySingleTokenExactOut
.
// Calculate the non-taxable balance proportionate to the BPT burnt. | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divDown(totalSupply); | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divUp(totalSupply); | ||
|
||
// Compute the taxable amount: the difference between the non-taxable balance and actual withdrawal. | ||
uint256 taxableAmount = nonTaxableBalance - newBalance; |
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 protocol is interested in taxing a larger amount in order to charge more swap fees which, in turn, help in reducing the amount of tokenOut
to bid during removeLiquiditySingleTokenExactIn
.
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.
but isn't this increasing the nonTaxable
part?
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.
Perhaps rename nonTaxableBalance
, the tax is calculated from it, which implies the tax is actually a part of nonTaxableBalance
.
balanceBeforeTax
maybe?
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 points. balanceBeforeTax
or newProportionalBalance
could be better suited. I had the same question as @wei3erHase when we first put this code together.
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.
Should check if there's anywhere else where "taxable" or "nonTaxable" is used confusingly. Should add a comment about the rounding direction after fixing the names.
// Calculate the taxable amount, which is the difference | ||
// between the actual amount in and the non-taxable balance | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenInIndex]).divDown(totalSupply); | ||
uint256 nonTaxableBalance = newSupply.mulDown(currentBalances[tokenInIndex]).divDown(totalSupply); | ||
|
||
uint256 taxableAmount = (amountIn + currentBalances[tokenInIndex]) - nonTaxableBalance; |
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 protocol is interested in taxing a larger amount in order to charge more swap fees which, in turn, help in augmenting the amount of tokenIn
to ask for during addLiquiditySingleTokenExactOut
.
// solhint-disable-next-line no-inline-assembly | ||
assembly { | ||
let cap := add(numTokens, 1) | ||
for { | ||
let i := 1 | ||
} lt(i, cap) { | ||
i := add(i, 1) | ||
} { | ||
let pos := mul(i, 0x20) | ||
mstore(add(newBalances, pos), mload(add(currentBalances, pos))) | ||
} | ||
} | ||
|
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.
interesting, would you run ./pkg/vault$ yarn test:forge
to see if there are gas savings with 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.
I think this TODO is inherited from V2 code. I'd leave it out of this PR; it doesn't sound very important.
Sidenote: there's now an mcopy
opcode since the last hardfork.
There are other places where we're copying arrays, although it's usually for scaling (we have to do math on every element anyways, so perhaps mcopy
doesn't save much). See #308.
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 V3 we're avoiding assembly if at all possible (e.g., not with transients quite yet), generally favoring clarity/ease of review/audit over speed.
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.
interesting, would you run
./pkg/vault$ yarn test:forge
to see if there are gas savings with this?
Indeed, there are some gas savings as a result of both gas optimizations introduced. However, I agree that they should be addressed separately in a further PR so as to target single concerns.
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, thanks @0xJabberwock!
As @wei3erHase points out, let's please try to keep PRs targeting single concerns whenever possible.
For this one in particular, the pre-increments in the for loops are fine, but let's leave the assembly optimization for another one. The rest looks good!
// solhint-disable-next-line no-inline-assembly | ||
assembly { | ||
let cap := add(numTokens, 1) | ||
for { | ||
let i := 1 | ||
} lt(i, cap) { | ||
i := add(i, 1) | ||
} { | ||
let pos := mul(i, 0x20) | ||
mstore(add(newBalances, pos), mload(add(currentBalances, pos))) | ||
} | ||
} | ||
|
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 this TODO is inherited from V2 code. I'd leave it out of this PR; it doesn't sound very important.
Sidenote: there's now an mcopy
opcode since the last hardfork.
There are other places where we're copying arrays, although it's usually for scaling (we have to do math on every element anyways, so perhaps mcopy
doesn't save much). See #308.
// Calculate the non-taxable balance proportionate to the BPT burnt. | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divDown(totalSupply); | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divUp(totalSupply); | ||
|
||
// Compute the taxable amount: the difference between the non-taxable balance and actual withdrawal. | ||
uint256 taxableAmount = nonTaxableBalance - newBalance; |
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 points. balanceBeforeTax
or newProportionalBalance
could be better suited. I had the same question as @wei3erHase when we first put this code together.
@@ -265,8 +274,8 @@ library BasePoolMath { | |||
swapFeeAmounts = new uint256[](numTokens); | |||
swapFeeAmounts[tokenOutIndex] = fee; | |||
|
|||
// mulUp/divDown maximize the amount of tokens burned for the security reasons | |||
bptAmountIn = totalSupply.mulUp(currentInvariant - invariantWithFeesApplied).divDown(currentInvariant); | |||
// mulUp/divUp maximize the amount of tokens burned for the security reasons |
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.
// mulUp/divUp maximize the amount of tokens burned for the security reasons | |
// mulUp/divUp maximizes the amount of tokens burned for security reasons |
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 we just want to say "to favor the protocol," as elsewhere, instead of "for security reasons"? We are favoring the protocol for security reasons, but maybe we can say that in one place at the top, e.g.: "For security reasons, to help ensure that for all possible "round trip" paths, the caller always receives the same or fewer tokens than supplied, we have chosen the rounding direction to favor the protocol in all cases."
@@ -159,7 +159,7 @@ library BasePoolMath { | |||
|
|||
// Calculate the amount of BPT to mint. This is done by multiplying the | |||
// total supply with the ratio of the change in invariant. | |||
// mulDown/divDown minize amount of pool tokens to mint. | |||
// mulDown/divDown minimize amount of pool tokens to mint. |
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.
// mulDown/divDown minimize amount of pool tokens to mint. | |
// mulDown/divDown minimizes the amount of pool tokens to mint. |
For consistency with other comments. It seems like it should be singular if you read it "mulDown and divDown", but you can think of it as "the combination of mulDown/divDown," or the "mulDown/divDown expression" which would make it minimizes.
@@ -141,11 +141,11 @@ library BasePoolMath { | |||
uint256 invariantRatio = newInvariant.divDown(currentInvariant); | |||
|
|||
// Loop through each token to apply fees if necessary. | |||
for (uint256 index = 0; index < currentBalances.length; index++) { | |||
for (uint256 index = 0; index < numTokens; ++index) { | |||
// Check if the new balance is greater than the proportional balance. | |||
// If so, calculate the taxable amount. |
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.
// If so, calculate the taxable amount. | |
// If so, calculate the taxable amount, rounding in favor of the protocol. | |
// We round the invariantRatio term down to subtract less and get a higher `taxableAmount`, which | |
// charges higher swap fees, reducing the amount of BPT that will be minted. |
As elsewhere, I'd comment the reasoning for the rounding direction, so that it can be explicitly reviewed, and we're not always reverse engineering it.
@@ -191,13 +191,13 @@ library BasePoolMath { | |||
uint256 newSupply = exactBptAmountOut + totalSupply; | |||
// Calculate the initial amount of the input token needed for the desired amount of BPT out | |||
// "divUp" leads to a higher "newBalance," which in turn results in a larger "amountIn." | |||
// This leads to receiving more tokens for the same amount of BTP minted. | |||
// This leads to receiving more tokens for the same amount of BPT minted. | |||
uint256 newBalance = computeBalance(currentBalances, tokenInIndex, newSupply.divUp(totalSupply)); | |||
uint256 amountIn = newBalance - currentBalances[tokenInIndex]; | |||
|
|||
// Calculate the taxable amount, which is the difference | |||
// between the actual amount in and the non-taxable 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.
// between the actual amount in and the non-taxable balance | |
// between the actual amount in and the non-taxable balance. Round the `nonTaxableBalance` down to | |
// favor the protocol by increasing the taxable amount, which charges higher swap fees, ultimately increasing | |
// the amount of `tokenIn` that will be transferred from the caller. |
// solhint-disable-next-line no-inline-assembly | ||
assembly { | ||
let cap := add(numTokens, 1) | ||
for { | ||
let i := 1 | ||
} lt(i, cap) { | ||
i := add(i, 1) | ||
} { | ||
let pos := mul(i, 0x20) | ||
mstore(add(newBalances, pos), mload(add(currentBalances, pos))) | ||
} | ||
} | ||
|
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 V3 we're avoiding assembly if at all possible (e.g., not with transients quite yet), generally favoring clarity/ease of review/audit over speed.
@@ -265,8 +274,8 @@ library BasePoolMath { | |||
swapFeeAmounts = new uint256[](numTokens); | |||
swapFeeAmounts[tokenOutIndex] = fee; | |||
|
|||
// mulUp/divDown maximize the amount of tokens burned for the security reasons | |||
bptAmountIn = totalSupply.mulUp(currentInvariant - invariantWithFeesApplied).divDown(currentInvariant); | |||
// mulUp/divUp maximize the amount of tokens burned for the security reasons |
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 we just want to say "to favor the protocol," as elsewhere, instead of "for security reasons"? We are favoring the protocol for security reasons, but maybe we can say that in one place at the top, e.g.: "For security reasons, to help ensure that for all possible "round trip" paths, the caller always receives the same or fewer tokens than supplied, we have chosen the rounding direction to favor the protocol in all cases."
// Calculate the non-taxable balance proportionate to the BPT burnt. | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divDown(totalSupply); | ||
uint256 nonTaxableBalance = newSupply.mulUp(currentBalances[tokenOutIndex]).divUp(totalSupply); | ||
|
||
// Compute the taxable amount: the difference between the non-taxable balance and actual withdrawal. | ||
uint256 taxableAmount = nonTaxableBalance - newBalance; |
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.
Should check if there's anywhere else where "taxable" or "nonTaxable" is used confusingly. Should add a comment about the rounding direction after fixing the names.
// Compute the taxable amount: the difference between the new proportional and disproportional balances. | ||
uint256 taxableAmount = newBalanceBeforeTax - newBalance; |
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.
During computeRemoveLiquiditySingleTokenExactIn
, it turns out that, in order for the taxableAmount
to be as large as possible, we would want newBalanceBeforeTax
to be rounded up and newBalance
to be rounded down.
Nevertheless, newBalance
was previously subtracted from currentBalances[tokenOutIndex]
so as to compute amountOut
; since we'd want the latter to be as low as possible, newBalance
had been rounded up initially.
In consequence, a conflict of interests arises regarding the rounding of newBalance
.
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's fine.
Taxable balance is not as important as amounts out to withdraw.
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.
Might be good to document this rounding conflict and priority in rounding amountOut over taxableAmount
so we need not repeat thinking through this again :)
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.
Yes, we should always document the reason for the rounding: see #468 (comment)
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 is looking good IMO, thanks @0xJabberwock !
@EndymionJkb @joaobrunoah would you mind giving it one last look before merging?
/cc @HickupHH3
// Compute the taxable amount: the difference between the new proportional and disproportional balances. | ||
uint256 taxableAmount = newBalanceBeforeTax - newBalance; |
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's fine.
Taxable balance is not as important as amounts out to withdraw.
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 great. The only thing we might still want to address is the comment in computeRemoveLiquiditySingleTokenExactIn
:
"In consequence, a conflict of interests arises regarding the rounding of newBalance."
Description
After thoroughly reviewing the
BasePoolMath
library in search of rounding inconsistencies, I've found some cases in which multiplications (mulUp
/mulDown
) or divisions (divUp
/divDown
) weren't favorable to the protocol.Type of change
Checklist:
main
, or there's a description of how to mergeIssue Resolution
Closes #395.