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

Add LBPool #1001

Merged
merged 74 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
c03edfb
add ,g to sed command
gerrrg Jul 23, 2024
ca249b3
add beginning of LBPool and gradual value change
gerrrg Jul 23, 2024
711609f
Merge branch 'balancer:main' into main
gerrrg Jul 23, 2024
b7c4050
Merge branch 'main' into add-lbpool
gerrrg Jul 23, 2024
1512487
move towards libraries for common functions; add max to FP library; o…
gerrrg Jul 25, 2024
410a9c4
do all imports; track swap fee percentage; enforce 2 tokens; track ma…
gerrrg Aug 23, 2024
1aa5e65
add LBPoolFactory; lint
gerrrg Aug 23, 2024
4c1253d
replace references to paused pool with swap enabled; fix logical inve…
gerrrg Aug 23, 2024
e3fb44a
directly use default liquidity management params
gerrrg Aug 23, 2024
3433090
let vault handle static swap fees via PoolRoleAccounts.swapFeeManager
gerrrg Aug 23, 2024
174aa67
implement getHookFlags and add a TODO to use TrustedRoutersProvider w…
gerrrg Aug 29, 2024
eee1f28
set up (but don't yet use) the TrustedRoutersProvider logic for the L…
gerrrg Aug 29, 2024
b9bbbaf
reorganize comments
gerrrg Aug 29, 2024
652e4b5
fill out sample router allowlist code
gerrrg Aug 29, 2024
92dda53
add temporary lazy trustedRouter as a placeholder until the trusted r…
gerrrg Aug 29, 2024
40c5577
add onRegister function to get hardhat tests to work for LBPool
gerrrg Sep 4, 2024
9146e8b
Merge branch 'main' into add-lbpool
gerrrg Sep 4, 2024
336f899
cleanup/tweak misc things post merge from main
gerrrg Sep 5, 2024
0bdb84b
lint
gerrrg Sep 5, 2024
3a8b009
add tests
gerrrg Sep 9, 2024
37cc15c
make foundry tests work; make BasePoolTests override-able
gerrrg Sep 9, 2024
1887169
generalize the BasePoolTest tests that change the static swap fee to …
gerrrg Sep 10, 2024
2973ad1
inherit from BaseHooks; properly override; directly use TRUSTED_ROUTE…
gerrrg Sep 11, 2024
9348242
make test fns virtual; make getSwapFeeAdmin internal
gerrrg Sep 11, 2024
5a2be27
add test for before/during/after gradual weight update
gerrrg Sep 12, 2024
3bedcd8
lint
gerrrg Sep 12, 2024
73275e0
Merge branch 'main' into add-lbpool
gerrrg Sep 18, 2024
366aefc
apply updates from review by @EndymionJkb
gerrrg Sep 19, 2024
9c06b00
Merge branch 'main' into add-lbpool
gerrrg Sep 19, 2024
78574f1
fix name of return variable to reflect to docstrings and apparent int…
gerrrg Sep 19, 2024
605d5e8
Merge branch 'main' into add-lbpool
gerrrg Sep 20, 2024
fca2e63
revert WeightedMath to what's in main branch
gerrrg Sep 20, 2024
b65cf10
add LBPoolFactory.t.sol
gerrrg Sep 20, 2024
f1b3b42
move swap enabled check from the hook (onBeforeSwap) to the actual sw…
gerrrg Sep 22, 2024
aec77e9
lint
gerrrg Sep 22, 2024
af0dcf2
move LBP to pool-weighted/lbp directory
gerrrg Sep 22, 2024
73f27d4
Merge branch 'main' into add-lbpool
gerrrg Sep 22, 2024
87ea6ff
Merge branch 'main' into add-lbpool
gerrrg Sep 23, 2024
26fc3ed
increase test coverage
gerrrg Sep 23, 2024
b9e1d7a
add input mismatch tests; add non-owner weight update fail test; add …
gerrrg Sep 23, 2024
d93f265
lint
gerrrg Sep 23, 2024
a686bba
add LBPool test for invalid token index (coverage for _getNormalizedW…
gerrrg Sep 24, 2024
a8c8ac7
merge from main; take all changes from main's VaultFactory.t.sol
gerrrg Sep 25, 2024
3f82b06
restore main's VaultFactory.{t.,}sol for this branch
gerrrg Sep 25, 2024
78780d5
clean up BasePoolTest changes
gerrrg Sep 25, 2024
c5fc58e
lint
gerrrg Sep 25, 2024
1e5dd53
Merge branch 'main' into add-lbpool
EndymionJkb Oct 1, 2024
e979db7
Merge branch 'main' into add-lbpool
EndymionJkb Oct 9, 2024
c72c153
Merge branch 'main' into add-lbpool
EndymionJkb Oct 9, 2024
eb7e4f4
chore: update gas
EndymionJkb Oct 9, 2024
f9845ad
Merge branch 'main' into add-lbpool
EndymionJkb Oct 14, 2024
1bc1061
Merge branch 'main' into add-lbpool
EndymionJkb Oct 31, 2024
29a2ccc
Merge branch 'main' into add-lbpool
EndymionJkb Nov 12, 2024
f0da637
Merge branch 'main' into add-lbpool
EndymionJkb Nov 26, 2024
4ddeaa0
fix: hardhat router deployment
EndymionJkb Nov 27, 2024
9405b03
fix: test that triggered roundtrip fee
EndymionJkb Nov 27, 2024
99741e7
Merge branch 'main' into add-lbpool
EndymionJkb Dec 3, 2024
50a6852
Merge branch 'main' into add-lbpool
EndymionJkb Dec 6, 2024
bf4a35b
fix: tests
EndymionJkb Dec 9, 2024
a338167
Merge branch 'main' into add-lbpool
EndymionJkb Dec 10, 2024
93246a9
invert logic in onBeforeAddLiquidity to be more readable
gerrrg Dec 18, 2024
8a86a64
change Ownable to Ownable2Step
gerrrg Dec 18, 2024
2f0acaf
use MINUTE instead of 60
gerrrg Dec 18, 2024
0df5967
Merge branch 'main' into add-lbpool
EndymionJkb Dec 31, 2024
c604ba2
test: add missing import
EndymionJkb Dec 31, 2024
9b64acb
test: adjust to new pool factory function
EndymionJkb Dec 31, 2024
5de8945
chore: update gas
EndymionJkb Dec 31, 2024
46719fc
First approximation to create and init.
Jan 2, 2025
be1c8e8
Improve errors, fix hardhat tests.
Jan 2, 2025
dfd563c
Lint.
Jan 2, 2025
4c29466
Fix tests.
Jan 2, 2025
b870b77
Address comments.
Jan 2, 2025
8ef0572
Add test for getters.
Jan 2, 2025
429c75a
Apply suggestions from code review
jubeira Jan 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/pool-weighted/contracts/WeightedPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ contract WeightedPool is IWeightedPool, BalancerPoolToken, PoolInfo, Version {

// A minimum normalized weight imposes a maximum weight ratio. We need this due to limitations in the
// implementation of the fixed point power function, as these ratios are often exponents.
uint256 private constant _MIN_WEIGHT = 1e16; // 1%
uint256 internal constant _MIN_WEIGHT = 1e16; // 1%

uint256 private immutable _totalTokens;

Expand Down Expand Up @@ -147,7 +147,7 @@ contract WeightedPool is IWeightedPool, BalancerPoolToken, PoolInfo, Version {
}

/// @inheritdoc IBasePool
function onSwap(PoolSwapParams memory request) public view onlyVault returns (uint256) {
function onSwap(PoolSwapParams memory request) public view virtual onlyVault returns (uint256) {
uint256 balanceTokenInScaled18 = request.balancesScaled18[request.indexIn];
uint256 balanceTokenOutScaled18 = request.balancesScaled18[request.indexOut];

Expand Down
2 changes: 0 additions & 2 deletions pkg/pool-weighted/contracts/WeightedPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import { WeightedPool } from "./WeightedPool.sol";
* @dev This is the most general factory, which allows up to eight tokens and arbitrary weights.
*/
contract WeightedPoolFactory is IPoolVersion, BasePoolFactory, Version {
// solhint-disable not-rely-on-time

string private _poolVersion;

constructor(
Expand Down
294 changes: 294 additions & 0 deletions pkg/pool-weighted/contracts/lbp/LBPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity ^0.8.24;

import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { IBasePoolFactory } from "@balancer-labs/v3-interfaces/contracts/vault/IBasePoolFactory.sol";
import { IRouterCommon } from "@balancer-labs/v3-interfaces/contracts/vault/IRouterCommon.sol";
import { IVaultErrors } from "@balancer-labs/v3-interfaces/contracts/vault/IVaultErrors.sol";
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";

import { InputHelpers } from "@balancer-labs/v3-solidity-utils/contracts/helpers/InputHelpers.sol";
import { FixedPoint } from "@balancer-labs/v3-solidity-utils/contracts/math/FixedPoint.sol";
import { BaseHooks } from "@balancer-labs/v3-vault/contracts/BaseHooks.sol";

import { GradualValueChange } from "../lib/GradualValueChange.sol";
import { WeightedPool } from "../WeightedPool.sol";

/**
* @notice Weighted Pool with mutable weights, designed to support v3 Liquidity Bootstrapping.
* @dev Inheriting from WeightedPool is only slightly wasteful (setting 2 immutable weights and `_totalTokens`,
* which will not be used later), and it is tremendously helpful for pool validation and any potential future
* base contract changes.
*/
contract LBPool is WeightedPool, Ownable, BaseHooks {
gerrrg marked this conversation as resolved.
Show resolved Hide resolved
using SafeCast for *;

// Since we have max 2 tokens and the weights must sum to 1, we only need to store one weight.
// Weights are 18 decimal floating point values, which fit in less than 64 bits. Store smaller numeric values
// to ensure the PoolState fits in a single slot. All timestamps in the system are uint32, enforced through
// SafeCast.
struct PoolState {
uint32 startTime;
uint32 endTime;
uint64 startWeight0;
uint64 endWeight0;
bool swapEnabled;
}

// LBPs are constrained to two tokens.auto
uint256 private constant _NUM_TOKENS = 2;

// LBPools are deployed with the Balancer standard router address, which we know reliably reports the true
// originating account on operations. This is important for liquidity operations, as these are permissioned
// operations that can only be performed by the owner of the pool. Without this check, a malicious router
// could spoof the address of the owner, allowing anyone to call permissioned functions.
jubeira marked this conversation as resolved.
Show resolved Hide resolved

// solhint-disable-next-line var-name-mixedcase
address private immutable _TRUSTED_ROUTER;
Copy link
Contributor

Choose a reason for hiding this comment

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

I see no reason in principle for this to change, but it does pose a maintainability question in case we ever migrate routers and the SDK / frontend need to adapt.

If we expect the lifecycle of these pools to be rather bounded I guess this is fine, as it's the simplest / cheapest approach.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also let's add a getter here.

Copy link
Collaborator

@EndymionJkb EndymionJkb Dec 31, 2024

Choose a reason for hiding this comment

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

The other reason is Daniel said we did want to support at least two from launch: basic and batch. If we have the registry (see PR #1296), we could use that. It would be equivalent to gerrg's original "provider" idea, and the maintenance would be done in the registry, so we wouldn't need to worry about it here. (And then we don't need a getter.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would look like:

IBalancerContractRegistry private immutable _contractRegistry;

constructor(
    NewPoolParams memory params,
    IVault vault,
    address owner,
    bool swapEnabledOnStart,
    IBalancerContractRegistry contractRegistry
) WeightedPool(params, vault) Ownable(owner) {
    _contractRegistry = contractRegistry;
}

onBefore hooks:
{
    if (_contractRegistry.isActiveBalancerContract(ContractType.ROUTER, router) == false) {
        revert RouterNotTrusted();
    }

    return IRouterCommon(router).getSender() == owner();
}

Copy link
Contributor

Choose a reason for hiding this comment

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

The other reason is Daniel said we did want to support at least two from launch: basic and batch. If we have the registry (see PR #1296), we could use that. It would be equivalent to gerrg's original "provider" idea, and the maintenance would be done in the registry, so we wouldn't need to worry about it here. (And then we don't need a getter.)

Sounds like an overkill really.
The trusted router is only used to add liquidity, and block non-owners from adding liquidity. You can still use any router to swap.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right... so really the only benefit would be not having to redeploy if we update the basic router (which should be rare compared to adding a new router or updating something more relay-like such as the CLR router)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@jubeira good point that the router only affects liquidity provision. Definitely makes it less of an issue, though I do still think it would be best to allow any "approved" router

For more context, the singular trusted router setup that is in the contract was 100% meant to be a placeholder until there was a router provider/contact registry. Simplifying it to be a single router made it possible for me to get unblocked in the meantime

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks, I'll double check.
I think this is a good starting point anyways.

jubeira marked this conversation as resolved.
Show resolved Hide resolved

PoolState private _poolState;

/**
* @notice Emitted when the owner enables or disables swaps.
* @param swapEnabled True if we are enabling swaps
*/
event SwapEnabledSet(bool swapEnabled);

/**
* @notice Emitted when the owner initiates a gradual weight change (e.g., at the start of the sale).
* @dev Also emitted on deployment, recording the initial state.
* @param startTime The starting timestamp of the update
* @param endTime The ending timestamp of the update
* @param startWeights The weights at the start of the update
* @param endWeights The final weights after the update is completed
*/
event GradualWeightUpdateScheduled(
uint256 startTime,
uint256 endTime,
uint256[] startWeights,
uint256[] endWeights
);

/// @dev Indicates that the router that called the Vault is not trusted, so any operations should revert.
jubeira marked this conversation as resolved.
Show resolved Hide resolved
error RouterNotTrusted();

/// @dev Indicates that the `owner` has disabled swaps.
error SwapsDisabled();

constructor(
NewPoolParams memory params,
IVault vault,
address owner,
bool swapEnabledOnStart,
address trustedRouter
) WeightedPool(params, vault) Ownable(owner) {
// WeightedPool validates `numTokens == normalizedWeights.length`, and ensures valid weights.
// Here we additionally enforce that LBPs must be two-token pools.
InputHelpers.ensureInputLengthMatch(_NUM_TOKENS, params.numTokens);

// Set the trusted router (passed down from the factory).
_TRUSTED_ROUTER = trustedRouter;

// solhint-disable-next-line not-rely-on-time
uint32 currentTime = block.timestamp.toUint32();
_startGradualWeightChange(currentTime, currentTime, params.normalizedWeights, params.normalizedWeights);
_setSwapEnabled(swapEnabledOnStart);
}

/**
* @notice Return start time, end time, and endWeights as an array.
* @dev Current weights should be retrieved via `getNormalizedWeights()`.
* @return startTime The starting timestamp of any ongoing weight change
* @return endTime The ending timestamp of any ongoing weight change
* @return endWeights The "destination" weights, sorted in token registration order
*/
function getGradualWeightUpdateParams()
external
view
returns (uint256 startTime, uint256 endTime, uint256[] memory endWeights)
{
PoolState memory poolState = _poolState;

startTime = poolState.startTime;
endTime = poolState.endTime;

endWeights = new uint256[](_NUM_TOKENS);
endWeights[0] = poolState.endWeight0;
endWeights[1] = FixedPoint.ONE - poolState.endWeight0;
}

/**
* @notice Indicate whether swaps are enabled or not for the given pool.
* @return swapEnabled True if trading is enabled
*/
function getSwapEnabled() external view returns (bool) {
return _getPoolSwapEnabled();
}

/*******************************************************************************
Permissioned Functions
*******************************************************************************/

/**
* @notice Enable/disable trading.
* @dev This is a permissioned function that can only be called by the owner.
* @param swapEnabled True if trading should be enabled
*/
function setSwapEnabled(bool swapEnabled) external onlyOwner {
_setSwapEnabled(swapEnabled);
}

/**
* @notice Start a gradual weight change. Weights will change smoothly from current values to `endWeights`.
* @dev This is a permissioned function that can only be called by the owner.
* If the `startTime` is in the past, the weight change will begin immediately.
*
* @param startTime The timestamp when the weight change will start
* @param endTime The timestamp when the weights will reach their final values
* @param endWeights The final values of the weights
*/
function updateWeightsGradually(
uint256 startTime,
uint256 endTime,
uint256[] memory endWeights
) external onlyOwner {
InputHelpers.ensureInputLengthMatch(_NUM_TOKENS, endWeights.length);

if (endWeights[0] < _MIN_WEIGHT || endWeights[1] < _MIN_WEIGHT) {
revert MinWeight();
}
if (endWeights[0] + endWeights[1] != FixedPoint.ONE) {
revert NormalizedWeightInvariant();
}

// Ensure startTime >= now.
startTime = GradualValueChange.resolveStartTime(startTime, endTime);

// The SafeCast ensures `endTime` can't overflow.
_startGradualWeightChange(startTime.toUint32(), endTime.toUint32(), _getNormalizedWeights(), endWeights);
}

/// @inheritdoc WeightedPool
function onSwap(PoolSwapParams memory request) public view override onlyVault returns (uint256) {
if (!_getPoolSwapEnabled()) {
revert SwapsDisabled();
}
gerrrg marked this conversation as resolved.
Show resolved Hide resolved
return super.onSwap(request);
}

/*******************************************************************************
Hook Functions
*******************************************************************************/

/**
* @notice Hook to be executed when pool is registered.
* @dev Returns true if registration was successful, and false to revert the registration of the pool.
* @param pool Address of the pool
* @return success True if the hook allowed the registration, false otherwise
*/
function onRegister(
address,
address pool,
TokenConfig[] memory,
LiquidityManagement calldata
) public view override onlyVault returns (bool) {
// Since in this case the pool is the hook, we don't need to check anything else.
// We *could* check that it's two tokens, but better to let that be caught later, as it will fail with a more
jubeira marked this conversation as resolved.
Show resolved Hide resolved
// descriptive error.
return pool == address(this);
}

// Return HookFlags struct that indicates which hooks this contract supports
function getHookFlags() public pure override returns (HookFlags memory hookFlags) {
// Ensure the caller is the owner, as only the owner can add liquidity.
hookFlags.shouldCallBeforeAddLiquidity = true;
}

/**
* @notice Check that the caller who initiated the add liquidity operation is the owner.
* @dev We first ensure the caller is the standard router, so that we know we can trust the value it returns
* from `getSender`.
*
* @param router The address (usually a router contract) that initiated the add liquidity operation
*/
function onBeforeAddLiquidity(
Copy link
Contributor

Choose a reason for hiding this comment

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

What about initialize?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well initialize doesn't have the router argument, so we can't do the same approach.

But we can create and initialize in the same tx. I'll add a method in the factory.

address router,
address,
AddLiquidityKind,
uint256[] memory,
uint256,
uint256[] memory,
bytes memory
) public view override onlyVault returns (bool) {
if (router == _TRUSTED_ROUTER) {
return IRouterCommon(router).getSender() == owner();
}
revert RouterNotTrusted();
gerrrg marked this conversation as resolved.
Show resolved Hide resolved
}

/*******************************************************************************
Internal Functions
*******************************************************************************/

// This is unused in this contract, but must be overridden from WeightedPool for consistency.
function _getNormalizedWeight(uint256 tokenIndex) internal view virtual override returns (uint256) {
if (tokenIndex < _NUM_TOKENS) {
return _getNormalizedWeights()[tokenIndex];
}

revert IVaultErrors.InvalidToken();
}
jubeira marked this conversation as resolved.
Show resolved Hide resolved

function _getNormalizedWeights() internal view override returns (uint256[] memory) {
uint256[] memory normalizedWeights = new uint256[](_NUM_TOKENS);
normalizedWeights[0] = _getNormalizedWeight0();
normalizedWeights[1] = FixedPoint.ONE - normalizedWeights[0];

return normalizedWeights;
}

function _getNormalizedWeight0() internal view virtual returns (uint256) {
PoolState memory poolState = _poolState;
uint256 pctProgress = GradualValueChange.calculateValueChangeProgress(poolState.startTime, poolState.endTime);

return GradualValueChange.interpolateValue(poolState.startWeight0, poolState.endWeight0, pctProgress);
}

function _getPoolSwapEnabled() private view returns (bool) {
return _poolState.swapEnabled;
}

function _setSwapEnabled(bool swapEnabled) private {
_poolState.swapEnabled = swapEnabled;

emit SwapEnabledSet(swapEnabled);
}

/**
* @dev When calling updateWeightsGradually again during an update, reset the start weights to the current weights,
* if necessary.
*/
function _startGradualWeightChange(
uint32 startTime,
uint32 endTime,
uint256[] memory startWeights,
uint256[] memory endWeights
) internal virtual {
PoolState memory poolState = _poolState;

poolState.startTime = startTime;
poolState.endTime = endTime;

// These have been validated, but SafeCast anyway out of an abundance of caution.
poolState.startWeight0 = startWeights[0].toUint64();
poolState.endWeight0 = endWeights[0].toUint64();

_poolState = poolState;

emit GradualWeightUpdateScheduled(startTime, endTime, startWeights, endWeights);
}
}
Loading
Loading