Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
timelocked updateRebalanceParameters
Browse files Browse the repository at this point in the history
  • Loading branch information
georgercarder committed Jul 25, 2022
1 parent 433f7ad commit f0e96e1
Show file tree
Hide file tree
Showing 30 changed files with 171 additions and 76 deletions.
61 changes: 44 additions & 17 deletions contracts/StrategyController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,13 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
uint256 private constant FEE_BOUND = 200; // Max fee of 20%
int256 private constant PERCENTAGE_BOUND = 10000; // Max 10x leverage

uint256 public constant REBALANCE_TIMELOCK_PERIOD = 5 minutes; // FIXME should this be variable? a different value?

address public immutable factory;

event NewStructure(address indexed strategy, StrategyItem[] items, bool indexed finalized);
event NewValue(address indexed strategy, TimelockCategory category, uint256 newValue, bool indexed finalized);
event StrategyOpen(address indexed strategy);
event StrategySet(address indexed strategy);
event RebalanceParametersUpdated(uint256 indexed rebalanceTimelockPeriod, uint256 indexed rebalanceThreshold, bool indexed finalized);

// hack! these events are called in the `ControllerLibrary`
// but cannot be tracked unless they are defined here!
Expand All @@ -55,6 +54,11 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
* @dev Called to initialize proxy
*/
function initialize() external initializer {
_rebalanceTimelockPeriod = 5 minutes;
_rebalanceThresholdScalar = 2000;
bytes32 key = keccak256(abi.encode(this.updateRebalanceParameters.selector));
_setTimelock(key, _rebalanceTimelockPeriod);

updateAddresses();
_require(address(this)== ControllerLibrary.self(), uint256(0x1bb63a90056c00) /* error_macro_for("Sanity check that Library shares context.") */);
}
Expand Down Expand Up @@ -180,9 +184,10 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I

bytes32 key = keccak256(abi.encode(this.rebalance.selector, strategy));
_require(_timelockIsReady(key), uint256(0x1bb63a90056c04) /* error_macro_for("rebalance timelock not ready.") */);
_setTimelock(key, _rebalanceTimelockPeriod); // in case factory updateRebalanceParameters
_startTimelock(key, new bytes(0));

ControllerLibrary.rebalance(strategy, router, oracle(), _weth, _strategyStates[address(strategy)].rebalanceSlippage, data);
ControllerLibrary.rebalance(strategy, router, oracle(), _weth, _strategyStates[address(strategy)].rebalanceSlippage, _rebalanceThresholdScalar, data);
_removeStrategyLock(strategy);
}

Expand Down Expand Up @@ -409,6 +414,24 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
}
}

function updateRebalanceParameters(uint256 rebalanceTimelockPeriod, uint256 rebalanceThresholdScalar) external override {
_require(msg.sender == factory, uint256(0x1bb63a90056c13) /* error_macro_for("Not factory") */);
_startTimelock(
keccak256(abi.encode(this.updateRebalanceParameters.selector)), // identifier
abi.encode(rebalanceTimelockPeriod, rebalanceThresholdScalar)); // payload
emit RebalanceParametersUpdated(rebalanceTimelockPeriod, rebalanceThresholdScalar, false);
}

function finalizeRebalanceParameters() public {
bytes32 key = keccak256(abi.encode(this.updateRebalanceParameters.selector));
_require(_timelockIsReady(key), uint256(0x1bb63a90056c14) /* error_macro_for("updateRebalanceParameters timelock not ready.") */);
(uint256 rebalanceTimelockPeriod, uint256 rebalanceThresholdScalar) = abi.decode(_getTimelockValue(key), (uint256, uint256));
_resetTimelock(key);
_rebalanceTimelockPeriod = rebalanceTimelockPeriod;
_rebalanceThresholdScalar = rebalanceThresholdScalar;
emit RebalanceParametersUpdated(rebalanceTimelockPeriod, rebalanceThresholdScalar, true);
}

function oracle() public view override returns (IOracle) {
return IOracle(_oracle);
}
Expand All @@ -425,6 +448,10 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
return _pool;
}

function rebalanceThresholdScalar() external view override returns(uint256) {
return _rebalanceThresholdScalar;
}

function _withdrawWETH(
IStrategy strategy,
IStrategyRouter router,
Expand Down Expand Up @@ -458,7 +485,7 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
if (state.social) emit StrategyOpen(strategy);
if (state.set) emit StrategySet(strategy);
bytes32 key = keccak256(abi.encode(this.rebalance.selector, strategy));
_setTimelock(key, REBALANCE_TIMELOCK_PERIOD);
_setTimelock(key, _rebalanceTimelockPeriod);
_startTimelock(key, new bytes(0));
}

Expand All @@ -474,7 +501,7 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
) private {
address weth;
if (msg.value > 0) {
_require(amount == 0, uint256(0x1bb63a90056c13) /* error_macro_for("Ambiguous amount") */);
_require(amount == 0, uint256(0x1bb63a90056c15) /* error_macro_for("Ambiguous amount") */);
amount = msg.value;
weth = _weth;
IWETH(weth).deposit{value: amount}();
Expand Down Expand Up @@ -515,29 +542,29 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
ControllerLibrary.verifyFormerDebt(address(strategy), newDebt, currentDebt);
}
// Check balance
(bool balancedAfter, uint256 totalAfter, ) = ControllerLibrary.verifyBalance(strategy, oracle(), false); // outer=false
_require(balancedAfter, uint256(0x1bb63a90056c14) /* error_macro_for("Not balanced") */);
(bool balancedAfter, uint256 totalAfter, ) = ControllerLibrary.verifyBalance(strategy, oracle(), 0);
_require(balancedAfter, uint256(0x1bb63a90056c16) /* error_macro_for("Not balanced") */);
_checkSlippage(totalAfter, totalBefore, _strategyStates[address(strategy)].restructureSlippage);
strategy.updateTokenValue(totalAfter, strategy.totalSupply());
}

function _checkSlippage(uint256 slippedValue, uint256 referenceValue, uint256 slippage) private pure {
_require(
slippedValue >= referenceValue.mul(slippage).div(DIVISOR),
uint256(0x1bb63a90056c15) /* error_macro_for("Too much slippage") */
uint256(0x1bb63a90056c17) /* error_macro_for("Too much slippage") */
);
}

function _checkDivisor(uint256 value) private pure {
_require(value <= DIVISOR, uint256(0x1bb63a90056c16) /* error_macro_for("Out of bounds") */);
_require(value <= DIVISOR, uint256(0x1bb63a90056c18) /* error_macro_for("Out of bounds") */);
}

function _checkFee(uint256 value) private pure {
_require(value <= FEE_BOUND, uint256(0x1bb63a90056c17) /* error_macro_for("Fee too high") */);
_require(value <= FEE_BOUND, uint256(0x1bb63a90056c19) /* error_macro_for("Fee too high") */);
}

function _checkTimelock(uint256 value) private {
_require(value <= 30 days, uint256(0x1bb63a90056c18) /* error_macro_for("Timelock is too long") */);
_require(value <= 30 days, uint256(0x1bb63a90056c1a) /* error_macro_for("Timelock is too long") */);
}

function _checkAndEmit(address strategy, TimelockCategory category, uint256 value, bool finalized) private {
Expand Down Expand Up @@ -570,21 +597,21 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
* @notice Checks that strategy is initialized
*/
function _isInitialized(address strategy) private view {
_require(initialized(strategy), uint256(0x1bb63a90056c19) /* error_macro_for("Not initialized") */);
_require(initialized(strategy), uint256(0x1bb63a90056c1b) /* error_macro_for("Not initialized") */);
}

/**
* @notice Checks that router is whitelisted
*/
function _onlyApproved(address account) private view {
_require(whitelist().approved(account), uint256(0x1bb63a90056c1a) /* error_macro_for("Not approved") */);
_require(whitelist().approved(account), uint256(0x1bb63a90056c1c) /* error_macro_for("Not approved") */);
}

/**
* @notice Checks if msg.sender is manager
*/
function _onlyManager(IStrategy strategy) private view {
_require(msg.sender == strategy.manager(), uint256(0x1bb63a90056c1b) /* error_macro_for("Not manager") */);
_require(msg.sender == strategy.manager(), uint256(0x1bb63a90056c1d) /* error_macro_for("Not manager") */);
}

/**
Expand All @@ -593,19 +620,19 @@ contract StrategyController is IStrategyController, StrategyControllerStorage, I
function _socialOrManager(IStrategy strategy) private view {
_require(
msg.sender == strategy.manager() || _strategyStates[address(strategy)].social,
uint256(0x1bb63a90056c1c) /* error_macro_for("Not manager") */
uint256(0x1bb63a90056c1e) /* error_macro_for("Not manager") */
);
}

function _notSet(address strategy) private view {
_require(!_strategyStates[strategy].set, uint256(0x1bb63a90056c1d) /* error_macro_for("Strategy cannot change") */);
_require(!_strategyStates[strategy].set, uint256(0x1bb63a90056c1f) /* error_macro_for("Strategy cannot change") */);
}

function _timelockData(bytes32 identifier) internal override returns(TimelockData storage) {
return __timelockData[identifier];
}

receive() external payable {
_require(msg.sender == _weth, uint256(0x1bb63a90056c1e) /* error_macro_for("Not WETH") */);
_require(msg.sender == _weth, uint256(0x1bb63a90056c20) /* error_macro_for("Not WETH") */);
}
}
5 changes: 4 additions & 1 deletion contracts/StrategyControllerStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ contract StrategyControllerStorage is StrategyTypes {
address internal _pool;
mapping(bytes32 => TimelockData) internal __timelockData;

uint256 internal _rebalanceTimelockPeriod;
uint256 internal _rebalanceThresholdScalar;

// Gap for future storage changes
uint256[48] private __gap;
uint256[46] private __gap;
}
4 changes: 4 additions & 0 deletions contracts/StrategyProxyFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ contract StrategyProxyFactory is IStrategyProxyFactory, StrategyProxyFactoryStor
emit NewStreamingFee(uint256(fee));
}

function updateRebalanceParameters(uint256 rebalanceTimelockPeriod, uint256 rebalanceThresholdScalar) external onlyOwner {
IStrategyController(controller).updateRebalanceParameters(rebalanceTimelockPeriod, rebalanceThresholdScalar);
}

/*
* @dev This function is called by StrategyProxyAdmin
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,10 @@ contract StrategyControllerPaused is IStrategyController, StrategyControllerStor
}
}

function updateRebalanceParameters(uint256 rebalanceTimelockPeriod, uint256 rebalanceThresholdScalar) external override {
revert("StrategyControllerPaused.");
}

function oracle() public view override returns (IOracle) {
return IOracle(_oracle);
}
Expand All @@ -288,6 +292,10 @@ contract StrategyControllerPaused is IStrategyController, StrategyControllerStor
return _pool;
}

function rebalanceThresholdScalar() external view override returns(uint256) {
return _rebalanceThresholdScalar;
}

// Internal Strategy Functions
/**
* @notice Deposit eth or weth into strategy
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/IStrategyController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ interface IStrategyController is StrategyTypes {
uint256 newValue
) external;

function updateRebalanceParameters(uint256 rebalanceTimelockPeriod, uint256 rebalanceThresholdScalar) external;

function finalizeValue(IStrategy strategy) external;

function openStrategy(IStrategy strategy) external;
Expand All @@ -90,4 +92,6 @@ interface IStrategyController is StrategyTypes {
function weth() external view returns (address);

function pool() external view returns (address);

function rebalanceThresholdScalar() external view returns(uint256);
}
12 changes: 6 additions & 6 deletions contracts/libraries/ControllerLibrary.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ library ControllerLibrary {
using AddressArrays for address[];

int256 private constant DIVISOR = 1000;
uint256 private constant REBALANCE_THRESHOLD_SCALAR = 2; // FIXME tune

uint256 private constant PRECISION = 10**18;
uint256 private constant WITHDRAW_UPPER_BOUND = 10**17; // Upper condition for including pool's tokens as part of burn during withdraw
Expand Down Expand Up @@ -154,18 +153,19 @@ library ControllerLibrary {
IOracle oracle,
address weth,
uint256 rebalanceSlippage,
uint256 rebalanceThresholdScalar,
bytes memory data
) public {
_onlyApproved(address(router));
strategy.settleSynths();
(bool balancedBefore, uint256 totalBefore, int256[] memory estimates) = verifyBalance(strategy, oracle, true); // outer=true
(bool balancedBefore, uint256 totalBefore, int256[] memory estimates) = verifyBalance(strategy, oracle, rebalanceThresholdScalar);
require(!balancedBefore, "Balanced");
if (router.category() != IStrategyRouter.RouterCategory.GENERIC)
data = abi.encode(totalBefore, estimates);
// Rebalance
_useRouter(strategy, router, router.rebalance, weth, data);
// Recheck total
(bool balancedAfter, uint256 totalAfter, ) = verifyBalance(strategy, oracle, false); // outer=false
(bool balancedAfter, uint256 totalAfter, ) = verifyBalance(strategy, oracle, 0);
require(balancedAfter, "Not balanced");
_checkSlippage(totalAfter, totalBefore, rebalanceSlippage);
strategy.updateTokenValue(totalAfter, strategy.totalSupply());
Expand Down Expand Up @@ -377,10 +377,10 @@ library ControllerLibrary {
* whether the strategy is balanced. Necessary to confirm the balance
* before and after a rebalance to ensure nothing fishy happened
*/
function verifyBalance(IStrategy strategy, IOracle oracle, bool outer) public view returns (bool, uint256, int256[] memory) {
function verifyBalance(IStrategy strategy, IOracle oracle, uint256 rebalanceThresholdScalar) public view returns (bool, uint256, int256[] memory) {
uint256 threshold = strategy.rebalanceThreshold();
if (outer) { // wider threshold
threshold = threshold.mul(REBALANCE_THRESHOLD_SCALAR);
if (rebalanceThresholdScalar > 0) { // wider threshold
threshold = threshold.mul(rebalanceThresholdScalar) / uint256(DIVISOR);
}
return _verifyBalance(strategy, oracle, threshold);
}
Expand Down
8 changes: 5 additions & 3 deletions contracts/test/LibraryWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,21 @@ contract LibraryWrapper is StrategyTypes{

IOracle public oracle;
IStrategy public strategy;
IStrategyController public controller;

constructor(address oracle_, address strategy_) public {
constructor(address oracle_, address strategy_, address controller_) public {
oracle = IOracle(oracle_);
strategy = IStrategy(strategy_);
controller = IStrategyController(controller_);
}

function isBalanced() external view returns (bool balanced) {
(balanced,,) = ControllerLibrary.verifyBalance(strategy, oracle, true); // outer=true
(balanced,,) = ControllerLibrary.verifyBalance(strategy, oracle, controller.rebalanceThresholdScalar());
return balanced;
}

function isBalancedInner() external view returns (bool balanced) {
(balanced,,) = ControllerLibrary.verifyBalance(strategy, oracle, false); // outer=false
(balanced,,) = ControllerLibrary.verifyBalance(strategy, oracle, 0);
return balanced;
}

Expand Down
26 changes: 14 additions & 12 deletions errors/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,20 @@
"1bb63a90056c10": "Timelock active",
"1bb63a90056c11": "Strategy already open",
"1bb63a90056c12": "Strategy already set",
"1bb63a90056c13": "Ambiguous amount",
"1bb63a90056c14": "Not balanced",
"1bb63a90056c15": "Too much slippage",
"1bb63a90056c16": "Out of bounds",
"1bb63a90056c17": "Fee too high",
"1bb63a90056c18": "Timelock is too long",
"1bb63a90056c19": "Not initialized",
"1bb63a90056c1a": "Not approved",
"1bb63a90056c1b": "Not manager",
"1bb63a90056c1c": "Not manager",
"1bb63a90056c1d": "Strategy cannot change",
"1bb63a90056c1e": "Not WETH"
"1bb63a90056c13": "Not factory",
"1bb63a90056c14": "updateRebalanceParameters timelock not ready.",
"1bb63a90056c15": "Ambiguous amount",
"1bb63a90056c16": "Not balanced",
"1bb63a90056c17": "Too much slippage",
"1bb63a90056c18": "Out of bounds",
"1bb63a90056c19": "Fee too high",
"1bb63a90056c1a": "Timelock is too long",
"1bb63a90056c1b": "Not initialized",
"1bb63a90056c1c": "Not approved",
"1bb63a90056c1d": "Not manager",
"1bb63a90056c1e": "Not manager",
"1bb63a90056c1f": "Strategy cannot change",
"1bb63a90056c20": "Not WETH"
}
},
{
Expand Down
14 changes: 9 additions & 5 deletions test/aave-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ describe('AaveAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress)
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress, controller.address)
await wrapper.deployed()

//await displayBalances(wrapper, strategyItems.map((item) => item.item), weth)
Expand Down Expand Up @@ -548,7 +548,7 @@ describe('AaveAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress)
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress, controller.address)
await wrapper.deployed()

//await displayBalances(wrapper, strategyItems.map((item) => item.item), weth)
Expand Down Expand Up @@ -631,7 +631,7 @@ describe('AaveAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress)
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress, controller.address)
await wrapper.deployed()

//await displayBalances(wrapper, strategyItems.map((item) => item.item), weth)
Expand Down Expand Up @@ -721,7 +721,7 @@ describe('AaveAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress)
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress, controller.address)
await wrapper.deployed()

//await displayBalances(wrapper, strategyItems.map((item) => item.item), weth)
Expand Down Expand Up @@ -812,7 +812,11 @@ describe('AaveAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
let metaWrapper = await LibraryWrapper.connect(accounts[0]).deploy(oracle.address, strategyAddress)
let metaWrapper = await LibraryWrapper.connect(accounts[0]).deploy(
oracle.address,
strategyAddress,
controller.address
)
await metaWrapper.deployed()

//await displayBalances(basicWrapper, basicStrategyItems.map((item) => item.item), weth)
Expand Down
2 changes: 1 addition & 1 deletion test/balancer-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe('BalancerAdapter', function () {
ControllerLibrary: controllerLibrary.address,
},
})
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress)
wrapper = await LibraryWrapper.deploy(oracle.address, strategyAddress, controller.address)
await wrapper.deployed()

//await displayBalances(wrapper, strategyItems, weth)
Expand Down
Loading

0 comments on commit f0e96e1

Please sign in to comment.