Skip to content

Commit

Permalink
01 - Weight changer (#4)
Browse files Browse the repository at this point in the history
* WeightChanger contract

* add null controller for factory sample

* Update .eslintrc.js

* formatting

* rm prettier from lint

* lint

* update yarn.lock

* lint

* WeightChanger cleanup

* Unit tests

* not-rely-on-time solhint error turned off

* reorganized files

* Removed extra interfaces folder

* edits to weightchanger and factory

* Updated unit tests

* README added

* lint fix

* Fix workflows

* Revert "Fix workflows"

This reverts commit 82c0d02.

* Adding compostability library

* move to _ensureEnabled format. this change will be added to NullControllerFactory in an upcoming PR

* Partial fix of PR comments

* slight code cleanup

* tests with weight checks at each interval

* Factory Access Control tests added

* Removed packageManager section from package.josn

* prettier settings added back

* testWeightChange function added to reduce duplicate code

* prettier json 4 space tabs

* prettier format fix

* indent fix

* tab added

* formatting fix

* remove unnecessary return values from _updateWeights()

* Factory update

* Updated README

* changed isDisabled() to public

* use state variable _disabled for check

* Natspec comments added

* Natspec param fix

* Changed _verify functions to _check

* remove return uint256

---------

Co-authored-by: gerg <gerrrrrrrg@gmail.com>
  • Loading branch information
0xSpraggins and gerrrg committed Jun 30, 2023
1 parent b92b903 commit 6c14b23
Show file tree
Hide file tree
Showing 5 changed files with 583 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -11,6 +11,7 @@ This repository contains simplified examples of possible functionality in a Mana
| Number | Controller | Description |
| ----------- | ----------- | ----------- |
| 00 | [NullController](./pkg/mpc-examples/contracts/00-null-controller/README.md) | Empty controller that does nothing |
| 01 | [WeightChangerController](./pkg/mpc-examples/contracts/01-weight-changer/README.md) | Controller that allows users to gradually update token weights |

## Build and Test

Expand Down
35 changes: 35 additions & 0 deletions pkg/mpc-examples/contracts/01-weight-changer/README.md
@@ -0,0 +1,35 @@
# WeightChangerController

## Summary
WeightChangerController is a Managed Pool Controller that has the ability gradually update token weights to a set of predefined weights.

## Details
The WeightChangerController allows users to update the weights of a Managed Pool's tokens to one of the predefined weights sets over the course of 7 days.

## Access Control
### WeightChangerController
The controller has no access control; all public functions can be executed by any account.

### WeightChangerControllerFactory
The factory has one permissioned function: `disable()`. Using OZ's Ownable, the factory restricts permission to only the contract `owner`. Ownable was chosen as it is a very simple concept that requires little explanation; however, it may be desirable to grant this permission to more than a single `owner`. Using a solution such as Balancer's [SingletonAuthentication](https://github.com/balancer/balancer-v2-monorepo/blob/3e99500640449585e8da20d50687376bcf70462f/pkg/solidity-utils/contracts/helpers/SingletonAuthentication.sol) could be a useful system for many controller factories.

## Managed Pool Functions
The following list is a list of permissioned functions in a Managed Pool that a controller could potentially call. The WeightChangerController can call the functions below that are denoted with a checked box:

- Gradual Updates
- [ ] `pool.updateSwapFeeGradually(...)`
- [x] `pool.updateWeightsGradually(...)`
- Enable/Disable Interactions
- [ ] `pool.setSwapEnabled(...)`
- [ ] `pool.setJoinExitEnabled(...)`
- LP Allowlist Management
- [ ] `pool.setMustAllowlistLPs(...)`
- [ ] `pool.addAllowedAddress(...)`
- [ ] `pool.removeAllowedAddress(...)`
- Add/Remove Token
- [ ] `pool.addToken(...)`
- [ ] `pool.removeToken(...)`
- Circuit Breaker Management
- [ ] `pool.setCircuitBreakers(...)`
- Management Fee
- [ ] `pool.setManagementAumFeePercentage(...)`
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;

// This contract shows an example of how a managed pool controller can modify a pools weight
// Gradual weight updates over 7 days for each change
import "@balancer-labs/v2-interfaces/contracts/pool-utils/ILastCreatedPoolFactory.sol";
import "@balancer-labs/v2-interfaces/contracts/pool-utils/IManagedPool.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-solidity-utils/contracts/math/FixedPoint.sol";
import "@balancer-labs/v2-pool-utils/contracts/lib/ComposablePoolLib.sol";
import "../interfaces/IManagedPoolFactory.sol";

// solhint-disable not-rely-on-time

contract WeightChangerController {
using FixedPoint for uint256;
IERC20[] private _tokens;

uint256 private constant _REWEIGHT_DURATION = 7 days;

// Minimum and maximum weight limits
uint256 private constant _MIN_WEIGHT = 1e16; // 1%
uint256 private constant _MAX_WEIGHT = 99e16; // 99%

IVault private immutable _vault;
bytes32 private immutable _poolId;

constructor(IVault vault) {
// Get poolId from the factory
bytes32 poolId = IManagedPool(ILastCreatedPoolFactory(msg.sender).getLastCreatedPool()).getPoolId();

// Set the global tokens variables
(IERC20[] memory tokens, , ) = vault.getPoolTokens(poolId);
_setTokens(tokens);

_vault = vault;
_poolId = poolId;
}

/**
* @dev Starts the gradual reweight process to bring the token's weights to 50/50.
* @dev Gradual reweight will start when this function is called and take _REWEIGHT_DURATION to complete.
*/
function make5050() public {
uint256[] memory newWeights = new uint256[](2);
newWeights[0] = 50e16;
newWeights[1] = 50e16;
_updateWeights(block.timestamp, block.timestamp + _REWEIGHT_DURATION, getTokens(), newWeights);
}

/**
* @dev Starts the gradual reweight process to bring the token's weights to 80/20.
* @dev Gradual reweight will start when this function is called and take _REWEIGHT_DURATION to complete.
*/
function make8020() public {
uint256[] memory newWeights = new uint256[](2);
newWeights[0] = 80e16;
newWeights[1] = 20e16;
_updateWeights(block.timestamp, block.timestamp + _REWEIGHT_DURATION, getTokens(), newWeights);
}

/**
* @dev Starts the gradual reweight process to bring the token's weights to 99/01.
* @dev Gradual reweight will start when this function is called and take _REWEIGHT_DURATION to complete.
*/
function make9901() public {
uint256[] memory newWeights = new uint256[](2);
newWeights[0] = 99e16;
newWeights[1] = 1e16;
_updateWeights(block.timestamp, block.timestamp + _REWEIGHT_DURATION, getTokens(), newWeights);
}

// === Public Getters ===
function getMaximumWeight() public pure returns (uint256) {
return _MAX_WEIGHT;
}

function getMinimumWeight() public pure returns (uint256) {
return _MIN_WEIGHT;
}

function getCurrentWeights() public view returns (uint256[] memory) {
return _getPool().getNormalizedWeights();
}

function getReweightDuration() public pure returns (uint256) {
return _REWEIGHT_DURATION;
}

function getTokens() public view returns (IERC20[] memory) {
return _tokens;
}

function getPoolId() public view returns (bytes32) {
return _poolId;
}

function getVault() public view returns (IVault) {
return _vault;
}

/// === Private and Internal ===
function _checkWeight(uint256 normalizedWeight) internal pure {
require(normalizedWeight >= _MIN_WEIGHT, "Weight under minimum");
require(normalizedWeight <= _MAX_WEIGHT, "Weight over maximum");
}

function _checkWeights(uint256[] memory normalizedWeights) internal pure {
uint256 normalizedSum = 0;
for (uint256 i = 0; i < normalizedWeights.length; i++) {
_checkWeight(normalizedWeights[i]);
normalizedSum = normalizedSum.add(normalizedWeights[i]);
}

require(normalizedSum == FixedPoint.ONE, "Weights must sum to one");
}

function _getPoolFromId(bytes32 poolId) internal pure returns (IManagedPool) {
// 12 byte logical shift left to remove the nonce and specialization setting. We don't need to mask,
// since the logical shift already sets the upper bits to zero.
return IManagedPool(address(uint256(poolId) >> (12 * 8)));
}

function _getPool() internal view returns (IManagedPool) {
return _getPoolFromId(getPoolId());
}

function _setTokens(IERC20[] memory tokens) internal {
_tokens = ComposablePoolLib.dropBptFromTokens(tokens);
}

/**
* @dev Updates the weights of the managed pool.
* @param startTime The timestamp, in seconds, at when the gradual weight update process starts.
* @param endTime The timestamp, in seconds, at when the gradual weight update process is complete.
* @param tokens An array of tokens, IERC20, that make up the managed pool.
* @param weights The desired end weights of the pool tokens. Must correspond with the tokens parameter.
*/
function _updateWeights(
uint256 startTime,
uint256 endTime,
IERC20[] memory tokens,
uint256[] memory weights
) internal {
_checkWeights(weights);
_getPool().updateWeightsGradually(startTime, endTime, tokens, weights);
}
}
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

pragma solidity ^0.7.0;
pragma experimental ABIEncoderV2;

import "@balancer-labs/v2-interfaces/contracts/pool-utils/IManagedPool.sol";
import "@balancer-labs/v2-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v2-solidity-utils/contracts/openzeppelin/Create2.sol";

import "@openzeppelin/contracts/access/Ownable.sol";

import "./WeightChangerController.sol";
import "../interfaces/IManagedPoolFactory.sol";

/**
* @title WeightChangerControllerFactory
* @notice Factory for a Managed Pool and Weight Changer Controller.
* @dev Determines controller deployment address, deploys pool (w/ controller address as argument), then controller.
*/
contract WeightChangerControllerFactory is Ownable {
mapping(address => bool) public isControllerFromFactory;

address public managedPoolFactory;
IVault public balancerVault;
bool private _disabled;

uint256 private _nextControllerSalt;
address private _lastCreatedPool;

// This struct is a subset of IManagedPoolFactory.NewPoolParams which omits arguments
// that this factory will override and are therefore unnecessary to provide. It will
// ultimately be used to populate IManagedPoolFactory.NewPoolParams.
struct MinimalPoolParams {
string name;
string symbol;
IERC20[] tokens;
uint256[] normalizedWeights;
uint256 swapFeePercentage;
bool swapEnabledOnStart;
uint256 managementAumFeePercentage;
uint256 aumFeeId;
}

event ControllerCreated(address indexed controller, IVault vault, bytes32 poolId);
event Disabled();

constructor(IVault vault, address factory) {
balancerVault = vault;
managedPoolFactory = factory;
}

/**
* @dev Return the address of the most recently created pool.
*/
function getLastCreatedPool() external view returns (address) {
return _lastCreatedPool;
}

function create(MinimalPoolParams calldata minimalParams) external {
_ensureEnabled();

bytes32 controllerSalt = bytes32(_nextControllerSalt);
_nextControllerSalt += 1;

bytes memory controllerCreationCode = abi.encodePacked(
type(WeightChangerController).creationCode,
abi.encode(balancerVault)
);
address expectedControllerAddress = Create2.computeAddress(controllerSalt, keccak256(controllerCreationCode));

// Build arguments to deploy pool from factory.
address[] memory assetManagers = new address[](minimalParams.tokens.length);
for (uint256 i = 0; i < assetManagers.length; i++) {
assetManagers[i] = expectedControllerAddress;
}

// Populate IManagedPoolFactory.NewPoolParams with arguments from MinimalPoolParams and
// other arguments that this factory provides itself.
IManagedPoolFactory.NewPoolParams memory fullParams;
fullParams.name = minimalParams.name;
fullParams.symbol = minimalParams.symbol;
fullParams.tokens = minimalParams.tokens;
fullParams.normalizedWeights = minimalParams.normalizedWeights;
// Asset Managers set to the controller address, not known by deployer until creation.
fullParams.assetManagers = assetManagers;
fullParams.swapFeePercentage = minimalParams.swapFeePercentage;
fullParams.swapEnabledOnStart = minimalParams.swapEnabledOnStart;
// Factory enforces public LPs for MPs with WeightChanger.
fullParams.mustAllowlistLPs = false;
fullParams.managementAumFeePercentage = minimalParams.managementAumFeePercentage;
fullParams.aumFeeId = minimalParams.aumFeeId;

IManagedPool pool = IManagedPool(
IManagedPoolFactory(managedPoolFactory).create(fullParams, expectedControllerAddress)
);
_lastCreatedPool = address(pool);

address actualControllerAddress = Create2.deploy(0, controllerSalt, controllerCreationCode);
require(expectedControllerAddress == actualControllerAddress, "Deploy failed");

// Log controller locally.
isControllerFromFactory[actualControllerAddress] = true;

// Log controller publicly.
emit ControllerCreated(actualControllerAddress, balancerVault, pool.getPoolId());
}

/**
* @dev Allow the owner to disable the factory, preventing future deployments.
* @notice owner is initially the factory deployer, but this role can be transferred.
* @dev The onlyOwner access control paradigm is an example. Any access control can
* be implemented to allow for different needs.
*/
function disable() external onlyOwner {
_ensureEnabled();
_disabled = true;
emit Disabled();
}

/**
* @dev Query whether this controller factory is disabled.
*/
function isDisabled() external view returns (bool) {
return _disabled || _isPoolFactoryDisabled();
}

/**
* @dev Query whether the pool factory is disabled.
*/
function _isPoolFactoryDisabled() internal view returns (bool) {
return IManagedPoolFactory(managedPoolFactory).isDisabled();
}

/**
* @dev Revert if the factory is disabled.
*/
function _ensureEnabled() internal view {
require(!_disabled, "Controller factory disabled");
require(!IManagedPoolFactory(managedPoolFactory).isDisabled(), "Pool factory disabled");
}
}

0 comments on commit 6c14b23

Please sign in to comment.