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

Balancer Composable stable pool strategy #1839

Closed
wants to merge 161 commits into from

Conversation

sparrowDom
Copy link
Member

@sparrowDom sparrowDom commented Sep 26, 2023

Adds support for Balancer's Composable stable pools.

Connected PR: #1849

sparrowDom and others added 30 commits June 14, 2023 17:12
* Generated contract docs

* Refactor Balancer storage variables

* Small Balancer changes

* Natspec updates
Added missing licensing
getPoolAssets gas optimized

* Updated generated Balancer strategy contract diagrams

* fix contract linter

* Removed restrictions on tests

* Small gas improvements
Fixed Slither

* Change BalancerError version

* Updated constant names
Addresses to use checksum format

* JS lint tasks

* Updated Balancer and Aura pool id constants

* Removed getRateProviderRate as it wasn't being used
Refactored to remove poolAssetsMapped storage variable

* Updated OETH Contracts diagrams
Generated new Balancer contract diagrams

* Fix failing test

* Fix merge conflict

* Restored getRateProviderRate

* Natspec updates
Added toPoolAsset override

* Removed unused getRateProviderRate

* Natspec updates
Gas optimization of InitializableAbstractStrategy

* Abstract strategy gas improvements (#1719)

* Refactor base strategy to use immutables

* Fixed strategy deployments in 001_core and fixtures

* Generated new strategy diagrams

* Deploy rETH instead of the stETH Balancer MetaStable Pool

* removed unused Aura config

* Balancer fork tests

* Added check that BPT amount equals Aura LP amount
Added rETH conversion to ETH value

* Updated balancer strat fork tests

* Updated Balancer fork tests

* Added optional deposit with multiple assets to the strategy

* Single asset deposit to use multi asset deposit

* Added optional checkBalance to Balancer strategy

* Added checkBalance() to BaseBalancerStrategy

* Fix slither
Fix curve HH task

* Added multi-asset withdraw to balancer strategy

* Fix multi-asset withdraw

* Updated Balancer and Vault diagrams

* Fix js linter

* Fixed checkBalance of rETH asset in Balancer strategy

* Only wrap assets if amount > 0
Added depositAll fork test for Balancer strat

* Removed Vault changes for multi-asset strategy support

* Updated generated docs

* Add tests for wstETH/WETH Balancer pool (#1725)

* Split deployment and fix fixtures

* Deposit tests for wstETH/WETH pool

* Add withdraw test

* prettier

* remove .only in fork tests

---------

Co-authored-by: Shahul Hameed <[email protected]>
* add alternative implementation of Balancer's checkBalance

* correct the checkBalance implementation
* Added large withdraw tests for Balancer strategy

* fix test log

* Balancer withdraw to handle close to BPT limit

* Small change to Balancer withdraw fork test

* add some comments

* change implementation

---------

Co-authored-by: Domen Grabec <[email protected]>
* Added large withdraw tests for Balancer strategy

* fix test log

* Balancer withdraw to handle close to BPT limit

* Small change to Balancer withdraw fork test

* add some comments

* Add test for read-only reentrancy

* add reentrancy protection to checkBalance functions

* add documentation

* remove the only

---------

Co-authored-by: Nicholas Addison <[email protected]>
Co-authored-by: Domen Grabec <[email protected]>
* prettier

* adjust how checkBalance is calculated
* fix balancer withdrawals

* lint

* prettier
@sparrowDom sparrowDom force-pushed the sparrowDom/balancer-composable-st-pool branch from 1062b37 to 33a71ce Compare November 13, 2023 17:48
* add comment and change enum types to uint8

* add better comment
* make VaultAdmin not fail when withdrawing from all of the strategis  when withdrawAll from a strategy fails

* add a separate withdrawal function

* remove only

* fix fork tests
* add tests for wstETH/WETH composable pool to confirm BPT position works as expected

* fix comment

* prettier

* prettier
Comment on lines +46 to +47
function _btpInExactTokensOutIndex()
internal
Copy link
Collaborator

Choose a reason for hiding this comment

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

This and _exactBptInTokensOutIndex can be constants (can define it in BalancerMetaPoolStrategy and override it here)

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a solidity thing where you can not override constants. Interestingly (written in the same link) you can override a public function in a child contract using an overridden constant if the signatures and return types match.

* a duplicate asset in the _strategyAssets array
*/
require(
poolAssetsAmountsOut[poolAssetIndex[poolAsset]] == 0,
Copy link
Collaborator

Choose a reason for hiding this comment

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

poolAssetIndex[poolAsset] is being read twice

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a great catch. Thanks for the gas optimization tip. done here: 8fb75ec

// store poolAssets storage variable into memory for gas optimizations
address[] memory _poolAssets = poolAssets;

// STEP 1 - Withdraw all Balancer Pool Tokens (BPT) from Aura to this strategy contract
_lpWithdrawAll();
// Get the BPTs withdrawn from Aura plus any that were already in this strategy contract
uint256 BPTtoWithdraw = IERC20(platformAddress).balanceOf(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Perhaps rename it to bptToWithdraw? Also, there's no bptToWithdraw > 0 check anywhere. It's gonna fail IVault.withdrawAllFromStrategies (although we have a faultTolerantWithdrawAllFromStrategies method, would be nice to surface this error)

Copy link
Member Author

Choose a reason for hiding this comment

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

yes uncapitalized is a better way to name it thanks, changed.

Regarding withdrawAll, we can withdraw on an empty strategy and it doesn't revert. Tested here That said there is no need to spend gas when there is nothing to withdraw. Test, renames and gas optimisation done here: 6dbb9cd

shahthepro
shahthepro previously approved these changes Dec 6, 2023
Copy link
Collaborator

@shahthepro shahthepro left a comment

Choose a reason for hiding this comment

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

There're some failing tests, other than that it looks good ot me

Deployment Considerations

Internal State

  • Two new constants are added at the top of the existing contract (BaseBalancerStrategy), however since they're constant they don't affect the storage layout
  • BaseBalancerStrategy.__reserved is updated correctly after adding three new state variables

Potential Attacks/Edge Cases

  • I suspected that doing multiple withdrawAll and depositAll back to back would cause withdrawAll to fail at some point (due to the usage of FRX_ETH_REDEEM_CORRECTION) however it turned out to be not true due a few other checks we have that ensures that there would no dust left. Besides we always resort to faultTolerantWithdrawAllFromStrategies when/if withdrawAll fails

Logic

  • Are there bugs in the logic? No
  • Correct usage of global & local variables. -> they might differentiate only by an underscore that can be overlooked (e.g. address vs _address).
  • Do public/external functions having asset parameter verify the asset is supported (where applicable)

Tests

  • Some fork and unit tests are failing on CI
  • Each publicly callable method has a test
  • Each logical branch has a test
  • Each require() has a test
  • Edge conditions are tested
  • If tests interact with AMM make sure enough edge cases (pool tilts) are tested. Ideally with fuzzing.

Flavor

Could this code be simpler?

Could this code be less vulnerable to other code behaving weirdly?

Overflow

  • Never use "+" or "-", always use safe math or have contract compile in solidity version > 0.8
  • Check that all for loops use uint256

Proxy

  • Make sure proxy implementation contracts don't initialize variable state on variable declaration and do it rather in initialize function.

Black magic

  • Does not contain selfdestruct
  • Does not use delegatecall outside of proxying
  • (If an implementation contract were to call delegatecall under attacker control, it could call selfdestruct the implementation contract, leading to calls through the proxy silently succeeding, even though they were failing.)
  • Address.isContract should be treated as if could return anything at any time, because that's reality.

Dependencies

  • Review any new contract dependencies thoroughly (e.g. OpenZeppelin imports) when new dependencies are added or version of dependencies changes.
  • If OpenZeppelin ACL roles are use review & enumerate all of them.
  • Check OpenZeppelin security vulnerabilities and see if any apply to current PR considering the version of OpenZeppelin contract used.

Deploy

  • Check that any deployer permissions are removed after deploy

Authentication

  • Never use tx.origin
  • Check that every external/public function should actually be external
  • Check that every external/public function has the correct authentication

Cryptographic code

  • Contracts that roll their own crypto are terrifying
  • Note that a failed signature check will result in a 0x00 result. Make sure that the result throws if it returns this.
  • Beware of signed data being used in a replay attack to other contracts.

Gas problems

  • Contracts with for loops must have either:
    • A way to remove items
    • Can be upgraded to get unstuck
    • Size can only controlled by admins
  • Contracts with for loops must not allow end users to add unlimited items to a loop that is used by others or admins.

External calls

  • Contract addresses passed in are validated
  • No unsafe external calls
  • Reentrancy guards on all state changing functions
    • Still doesn't protect against external contracts changing the state of the world if they are called.
  • No malicious behaviors
  • Should not fail from stack depth problems (low level calls must require success)
  • No slippage attacks (we need to validate expected tokens received)
  • Oracles, one of:
    • No oracles
    • Oracles can't be bent
    • If oracle can be bent, it won't hurt us.
  • Don't call balanceOf for external contracts to determine what they will do, when they instead use internal accounting?

Ethereum

  • Contract does not send or receive Ethereum.
  • Contract has no payable methods.

@sparrowDom
Copy link
Member Author

@shahthepro the fork tests related to this PR have been fixed

"BPT token position incorrect"
);

for (uint256 i = 0; i < _assets.length; ++i) {

Choose a reason for hiding this comment

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

I see you are using ++i to save a little bit of gas, this addition could be performed using uncheck.
It's also possible to add uncheck to the following ++i on line 116 and 131

Copy link
Member Author

@sparrowDom sparrowDom Jan 26, 2024

Choose a reason for hiding this comment

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

That is a great point thanks. I have just read up on this how; Solidity (after 0.8.0) doesn't need to perform overflow checks in situations where no overflow is guaranteed. And looping through arrays definitely is one of those situations.

The unchecked does provide some gas savings with a little minor impact on code readability. In this case I would rather not make the change, since first loop is done only on initialisation. And the other 2 are done on join/exit pools. We are running those operations relatively rarely - and are paying for the gas ourselves.

If these optimisations would be part of checkBalance function ( that gets called in Vault's allocate/rebase that are called in most of mints & redeems) then I'd gladly trade the readability impact for the gas savings.

And thanks I will keep the unchecked in mind for future optimization opportunities.

override
returns (uint256[] memory amounts)
{
// remove the position in the array that represents the BPT
Copy link

@pandadefi pandadefi Jan 16, 2024

Choose a reason for hiding this comment

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

I am still learning about this strategy and the parent strategies. I would like to understand why the BPT token amount needs to be removed. Could you give me some explanations?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hey sure, so the Composable Stable Pools have this particularity that they have the BPT token positioned next to other pool assets. This unlocks the ability of more easily nesting / batch swapping with other Balancer pools.

When joining/exiting a Balancer pool (add liquidity / remove liquidity in Curve's terms) encoded userData needs to be passed along the request that encodes expected tokens received after the join/exit. And that function expects the amounts not to have the BPT token amount present in the array (even though it is one of the assets in the Composable Stable Pool).

Additionally when we are calculating the amount of BPT expected to be received from withdrawing given assets the BPT token needs to be omitted from the calculation. Since we are swapping the BPT token in exchange for the pool assets.

Meta stable pools (predecessors of Composable Stable Pools) didn't have the BPT token present as one of the pool's assets and thus the exclusion of BPT from the join/exit requests wasn't necessary.

Does that make sense?

@naddison36
Copy link
Collaborator

Closing as we are no longer doing this strategy

@naddison36 naddison36 closed this Jul 9, 2024
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.

6 participants