From 89fa2ccbc15c4d3a677e83e8a49f6ad28e0511b2 Mon Sep 17 00:00:00 2001 From: RedVeil Date: Mon, 28 Oct 2024 12:51:15 +0100 Subject: [PATCH] added roles --- src/interfaces/IPriceOracle.sol | 15 +- .../MultisigVaultController.sol.txt | 47 ----- src/peripheral/OracleVaultController.sol | 164 +++++++++++++++ .../controllerModule/DrawdownModule.sol | 196 +++++++----------- .../DrawdownModuleOracleless.sol.txt | 139 +++++++++++++ .../controllerModule/TakeOverSafeLib.sol | 84 ++++++++ .../controllerModule/WithdrawalModule.sol | 70 ++++--- src/utils/Pausable.sol | 8 + .../phase1/BaseControlledAsyncRedeem.sol | 22 +- src/vaults/multisig/phase1/BaseERC7540.sol | 34 ++- src/vaults/multisig/phase1/MultisigVault.sol | 8 +- 11 files changed, 562 insertions(+), 225 deletions(-) delete mode 100644 src/peripheral/MultisigVaultController.sol.txt create mode 100644 src/peripheral/OracleVaultController.sol create mode 100644 src/peripheral/gnosis/controllerModule/DrawdownModuleOracleless.sol.txt create mode 100644 src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol diff --git a/src/interfaces/IPriceOracle.sol b/src/interfaces/IPriceOracle.sol index d97f26cc..8fa5522c 100644 --- a/src/interfaces/IPriceOracle.sol +++ b/src/interfaces/IPriceOracle.sol @@ -15,7 +15,11 @@ interface IPriceOracle { /// @param base The token that is being priced. /// @param quote The token that is the unit of account. /// @return outAmount The amount of `quote` that is equivalent to `inAmount` of `base`. - function getQuote(uint256 inAmount, address base, address quote) external view returns (uint256 outAmount); + function getQuote( + uint256 inAmount, + address base, + address quote + ) external view returns (uint256 outAmount); /// @notice Two-sided price: How much quote token you would get/spend for selling/buying inAmount of base token. /// @param inAmount The amount of `base` to convert. @@ -23,8 +27,9 @@ interface IPriceOracle { /// @param quote The token that is the unit of account. /// @return bidOutAmount The amount of `quote` you would get for selling `inAmount` of `base`. /// @return askOutAmount The amount of `quote` you would spend for buying `inAmount` of `base`. - function getQuotes(uint256 inAmount, address base, address quote) - external - view - returns (uint256 bidOutAmount, uint256 askOutAmount); + function getQuotes( + uint256 inAmount, + address base, + address quote + ) external view returns (uint256 bidOutAmount, uint256 askOutAmount); } diff --git a/src/peripheral/MultisigVaultController.sol.txt b/src/peripheral/MultisigVaultController.sol.txt deleted file mode 100644 index 74f453b0..00000000 --- a/src/peripheral/MultisigVaultController.sol.txt +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.25; - -interface IControllerModule { - function checkViolation(bytes memory data) external view returns (bool); - - function takeoverSafe(bytes memory data) external; -} - -contract MultisigVaultController { - constructor() {} - - /** - * @notice Take over safe in case of rule violation - * @param controllerModule module which checks and enforces rule violation - * @param data data to prove rule violation - */ - function takeoverSafe( - address controllerModule, - bytes memory data - ) external { - require(isControllerModule[controllerModule], ""); - IControllerModule(controllerModule).takeoverSafe(data); - } - - function executeTakeover( - address[] memory newOwners, - uint256 newThreshold - ) external { - // Call MainControllerModule.takover - // Transfer Security Deposit - } - - /*////////////////////////////////////////////////////////////// - CONTROLLER MODULE LOGIC - //////////////////////////////////////////////////////////////*/ - - mapping(address => bool) public isControllerModule; - - function addControllerModule(address module) external onlyOwner { - isModule[module] = true; - } - - function removeControllerModule(address module) external onlyOwner { - isModule[module] = false; - } -} diff --git a/src/peripheral/OracleVaultController.sol b/src/peripheral/OracleVaultController.sol new file mode 100644 index 00000000..4745e701 --- /dev/null +++ b/src/peripheral/OracleVaultController.sol @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; + +import {Owned} from "src/utils/Owned.sol"; +import {Pausable} from "src/utils/Pausable.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +interface IPushOracle { + function setPrice( + address base, + address quote, + uint256 bqPrice, + uint256 qbPrice + ) external; + + function setPrices( + address[] memory bases, + address[] memory quotes, + uint256[] memory bqPrices, + uint256[] memory qbPrices + ) external; + + function prices( + address base, + address quote + ) external view returns (uint256); +} + +struct Limit { + uint256 jump; // 1e18 = 100% + uint256 drawdown; // 1e18 = 100% +} + +contract OracleVaultController is Owned { + using FixedPointMathLib for uint256; + + IPushOracle public oracle; + + event KeeperUpdated(address previous, address current); + + error NotKeeperNorOwner(); + + constructor(address _oracle, address _owner) Owned(_owner) { + oracle = IPushOracle(_oracle); + } + + /*////////////////////////////////////////////////////////////// + ORACLE LOGIC + //////////////////////////////////////////////////////////////*/ + + struct PriceUpdate { + address vault; + address asset; + uint256 shareValueInAssets; + uint256 assetValueInShares; + } + + mapping(address => uint256) public highWaterMarks; + + function updatePrice(PriceUpdate calldata priceUpdate) external { + _updatePrice(priceUpdate); + } + + function updatePrices(PriceUpdate[] calldata priceUpdates) external { + for (uint256 i; i < priceUpdates.length; i++) { + _updatePrice(priceUpdates[i]); + } + } + + function _updatePrice( + PriceUpdate calldata priceUpdate + ) internal onlyKeeperOrOwner(priceUpdate.vault) { + uint256 lastPrice = oracle.prices(priceUpdate.vault, priceUpdate.asset); + uint256 hwm = highWaterMarks[priceUpdate.vault]; + Limit memory limit = limits[priceUpdate.vault]; + bool paused = Pausable(priceUpdate.vault).paused(); + + // Check for price jump or drawdown + if ( + // Check for price jump down + priceUpdate.shareValueInAssets < + lastPrice.mulDivDown(1e18 - limit.jump, 1e18) || + // Check for price jump up + priceUpdate.shareValueInAssets > + lastPrice.mulDivDown(1e18 + limit.jump, 1e18) || + // Check for drawdown + priceUpdate.shareValueInAssets < + hwm.mulDivDown(1e18 - limit.drawdown, 1e18) + ) { + if (!paused) Pausable(priceUpdate.vault).pause(); + } else if (priceUpdate.shareValueInAssets > hwm) { + // Update HWM if there wasnt a jump or drawdown + highWaterMarks[priceUpdate.vault] = priceUpdate.shareValueInAssets; + } + + oracle.setPrice( + priceUpdate.vault, + priceUpdate.asset, + priceUpdate.shareValueInAssets, + priceUpdate.assetValueInShares + ); + } + + /*////////////////////////////////////////////////////////////// + KEEPER LOGIC + //////////////////////////////////////////////////////////////*/ + + mapping(address => address) public keepers; + + event KeeperUpdated(address vault, address previous, address current); + + function setKeeper(address _vault, address _keeper) external onlyOwner { + emit KeeperUpdated(_vault, keepers[_vault], _keeper); + + keepers[_vault] = _keeper; + } + + modifier onlyKeeperOrOwner(address _vault) { + if (msg.sender != owner && msg.sender != keepers[_vault]) + revert NotKeeperNorOwner(); + _; + } + + /*////////////////////////////////////////////////////////////// + MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + mapping(address => Limit) public limits; + + event LimitUpdated(address vault, Limit previous, Limit current); + + function setLimit(address _vault, Limit memory _limit) external onlyOwner { + _setLimit(_vault, _limit); + } + + function setLimits( + address[] memory _vaults, + Limit[] memory _limits + ) external onlyOwner { + if (_vaults.length != _limits.length) revert("Invalid length"); + + for (uint256 i; i < _vaults.length; i++) { + _setLimit(_vaults[i], _limits[i]); + } + } + + function _setLimit(address _vault, Limit memory _limit) internal { + if (_limit.jump > 1e18 || _limit.drawdown > 1e18) + revert("Invalid limit"); + emit LimitUpdated(_vault, limits[_vault], _limit); + + limits[_vault] = _limit; + } + + /*////////////////////////////////////////////////////////////// + OTHER LOGIC + //////////////////////////////////////////////////////////////*/ + + function acceptOracleOwnership() external onlyOwner { + Owned(address(oracle)).acceptOwnership(); + } +} diff --git a/src/peripheral/gnosis/controllerModule/DrawdownModule.sol b/src/peripheral/gnosis/controllerModule/DrawdownModule.sol index eadfe7a8..5dee61a6 100644 --- a/src/peripheral/gnosis/controllerModule/DrawdownModule.sol +++ b/src/peripheral/gnosis/controllerModule/DrawdownModule.sol @@ -2,33 +2,64 @@ pragma solidity ^0.8.25; import {ControllerModule, ModuleCall, ISafe, Operation} from "src/peripheral/gnosis/controllerModule/MainControllerModule.sol"; -import {MultisigVault} from "src/vaults/multisig/phase1/MultisigVault.sol"; +import {OracleVault, IPriceOracle} from "src/vaults/multisig/phase1/OracleVault.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; import {Owned} from "src/utils/Owned.sol"; import {OwnerManager} from "safe-smart-account/base/OwnerManager.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {TakeOverSafeLib} from "src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol"; +import {OracleVaultController, Limit} from "src/peripheral/OracleVaultController.sol"; contract DrawdownModule is Owned { using FixedPointMathLib for uint256; - MultisigVault public vault; + OracleVault public vault; ControllerModule public controller; + ISafe public safe; ModuleCall[] public tokenBalanceCalls; - address[] public newOwners; - uint256 public newThreshold; - constructor( address vault_, address controller_, - address owner_ + address owner_, + address[] memory newOwners_, + uint256 newThreshold_, + uint256 liquidationBonus_ ) Owned(owner_) { - vault = MultisigVault(vault_); + vault = OracleVault(vault_); controller = ControllerModule(controller_); + safe = ISafe(ControllerModule(controller_).gnosisSafe()); + + newOwners = newOwners_; + newThreshold = newThreshold_; + liquidationBonus = liquidationBonus_; } + /*////////////////////////////////////////////////////////////// + EXECUTION LOGIC + //////////////////////////////////////////////////////////////*/ + function liquidateSafe(ModuleCall[] memory calls) external { + IPriceOracle oracle = vault.oracle(); + address asset = address(vault.asset()); + uint256 shareValue = oracle.getQuote( + 10 ** vault.decimals(), + address(vault), + asset + ); + + OracleVaultController oracleController = OracleVaultController( + Owned(address(oracle)).owner() + ); + uint256 hwm = oracleController.highWaterMarks(address(vault)); + (uint256 jump, uint256 drawdown) = oracleController.limits( + address(vault) + ); + + if (shareValue >= hwm.mulDivDown(1e18 - drawdown, 1e18)) + revert("Drawdown acceptable"); + // Execute calls to liquidate all positions in the safe to the vault asset controller.executeModuleTransactions(calls); @@ -44,133 +75,56 @@ contract DrawdownModule is Owned { if (abi.decode(data, (uint256)) > 0) revert("Leftover token"); } - address asset = address(vault.asset()); + // Transfer funds into this module uint256 assetBalance = ERC20(asset).balanceOf(controller.gnosisSafe()); - uint256 totalAssets = vault.totalAssets(); - // TODO add a drawdown parameter - if (assetBalance < totalAssets) { - // Transfer funds into this module - ModuleCall[] memory calls = new ModuleCall[](1); - calls[0] = ModuleCall({ - to: asset, - value: 0, - data: abi.encodeWithSelector( - ERC20.transfer.selector, - assetBalance, - address(this) - ), - operation: Operation.Call - }); - controller.executeModuleTransactions(calls); - - // Pay out liquidation bounty - uint256 bounty = assetBalance.mulDivDown(100, 10_000); - ERC20(asset).transfer(msg.sender, bounty); - - // Transfer remaining asset to vault - ERC20(asset).transfer(address(vault), assetBalance - bounty); - - // Put DAO in control of the safe - _takeoverSafe(newOwners, newThreshold); - } - } + ModuleCall[] memory calls = new ModuleCall[](1); + calls[0] = ModuleCall({ + to: asset, + value: 0, + data: abi.encodeWithSelector( + ERC20.transfer.selector, + assetBalance, + address(this) + ), + operation: Operation.Call + }); + controller.executeModuleTransactions(calls); - function _takeoverSafe( - address[] memory newOwners_, - uint256 newThreshold_ - ) internal { - address gnosisSafe = controller.gnosisSafe(); - ISafe safe = ISafe(gnosisSafe); - address[] memory owners = safe.getOwners(); - - // remove owners - for (uint256 i = (owners.length - 1); i > 0; --i) { - bool success = safe.execTransactionFromModule({ - to: gnosisSafe, - value: 0, - data: abi.encodeCall( - OwnerManager.removeOwner, - (owners[i - 1], owners[i], 1) - ), - operation: Operation.Call - }); - if (!success) { - revert("SM: owner removal failed"); - } - } + // Pay out liquidation bounty + uint256 bounty = assetBalance.mulDivDown(1e18 - liquidationBonus, 1e18); + ERC20(asset).transfer(msg.sender, bounty); - for (uint256 i = 0; i < newOwners_.length; i++) { - bool success; - if (i == 0) { - if (newOwners_[i] == owners[i]) continue; - success = safe.execTransactionFromModule({ - to: gnosisSafe, - value: 0, - data: abi.encodeCall( - OwnerManager.swapOwner, - (address(0x1), owners[i], newOwners_[i]) - ), - operation: Operation.Call - }); - if (!success) { - revert("SM: owner replacement failed"); - } - continue; - } - success = safe.execTransactionFromModule({ - to: gnosisSafe, - value: 0, - data: abi.encodeCall( - OwnerManager.addOwnerWithThreshold, - (newOwners_[i], 1) - ), - operation: Operation.Call - }); - if (!success) { - revert("SM: owner addition failed"); - } - } + // Transfer remaining assets back to the safe + ERC20(asset).transfer(address(safe), assetBalance - bounty); - if (newThreshold_ > 1) { - bool success = safe.execTransactionFromModule({ - to: gnosisSafe, - value: 0, - data: abi.encodeCall( - OwnerManager.changeThreshold, - (newThreshold_) - ), - operation: Operation.Call - }); - if (!success) { - revert("SM: change threshold failed"); - } - } + // Put DAO in control of the safe + TakeOverSafeLib.takeoverSafe( + address(controller), + newOwners, + newThreshold + ); } - function setTokenBalanceCalls( - ModuleCall[] memory calls - ) external onlyOwner { - for (uint256 i; i < calls.length; i++) { - if (calls[i].to == address(0)) revert("Invalid call"); - if (calls[i].data.length == 0) revert("Invalid call data"); - if (calls[i].value != 0) revert("Invalid call value"); - if (calls[i].operation != Operation.Call) - revert("Invalid call operation"); + /*////////////////////////////////////////////////////////////// + MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ - // We want to get the balance of all tokens in the safe that are not the vault asset - if (calls[i].to == address(vault.asset())) revert("Invalid call"); - - delete tokenBalanceCalls; - - tokenBalanceCalls.push(calls[i]); - } - } + address[] public newOwners; + uint256 public newThreshold; + uint256 public liquidationBonus; function setNewOwners(address[] memory newOwners_) external onlyOwner { newOwners = newOwners_; } function setNewThreshold(uint256 newThreshold_) external onlyOwner { + if (newThreshold_ < 1) revert("Invalid threshold"); + newThreshold = newThreshold_; } + + function setLiquidationBonus(uint256 liquidationBonus_) external onlyOwner { + if (liquidationBonus_ >= 1e18) revert("Invalid bonus"); + liquidationBonus = liquidationBonus_; + } } diff --git a/src/peripheral/gnosis/controllerModule/DrawdownModuleOracleless.sol.txt b/src/peripheral/gnosis/controllerModule/DrawdownModuleOracleless.sol.txt new file mode 100644 index 00000000..c8334364 --- /dev/null +++ b/src/peripheral/gnosis/controllerModule/DrawdownModuleOracleless.sol.txt @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import {ControllerModule, ModuleCall, ISafe, Operation} from "src/peripheral/gnosis/controllerModule/MainControllerModule.sol"; +import {MultisigVault} from "src/vaults/multisig/phase1/MultisigVault.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Owned} from "src/utils/Owned.sol"; +import {OwnerManager} from "safe-smart-account/base/OwnerManager.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; +import {TakeOverSafeLib} from "src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol"; + +contract DrawdownModule is Owned { + using FixedPointMathLib for uint256; + + MultisigVault public vault; + ControllerModule public controller; + ISafe public safe; + ModuleCall[] public tokenBalanceCalls; + + constructor( + address vault_, + address controller_, + address owner_, + address[] memory newOwners_, + uint256 newThreshold_, + uint256 liquidationBonus_ + ) Owned(owner_) { + vault = MultisigVault(vault_); + controller = ControllerModule(controller_); + safe = ISafe(ControllerModule(controller_).gnosisSafe()); + + newOwners = newOwners_; + newThreshold = newThreshold_; + liquidationBonus = liquidationBonus_; + } + + /*////////////////////////////////////////////////////////////// + EXECUTION LOGIC + //////////////////////////////////////////////////////////////*/ + + function liquidateSafe(ModuleCall[] memory calls) external { + // Execute calls to liquidate all positions in the safe to the vault asset + controller.executeModuleTransactions(calls); + + // Make sure there is no leftover token in the safe besides the vault asset + // A malicious liquidator might want to keep leftover tokens in the safe to make the appearance that the safe holds less than totalAssets to enable a liquidation + for (uint256 i; i < tokenBalanceCalls.length; i++) { + (bool success, bytes memory data) = tokenBalanceCalls[i].to.call( + tokenBalanceCalls[i].data + ); + if (!success) revert("Token balance call failed"); + + // TODO add an acceptable dust value instead of 0 + if (abi.decode(data, (uint256)) > 0) revert("Leftover token"); + } + + address asset = address(vault.asset()); + uint256 assetBalance = ERC20(asset).balanceOf(controller.gnosisSafe()); + uint256 totalAssets = vault.totalAssets(); + // TODO add a drawdown parameter + if (assetBalance < totalAssets) { + // Transfer funds into this module + ModuleCall[] memory calls = new ModuleCall[](1); + calls[0] = ModuleCall({ + to: asset, + value: 0, + data: abi.encodeWithSelector( + ERC20.transfer.selector, + assetBalance, + address(this) + ), + operation: Operation.Call + }); + controller.executeModuleTransactions(calls); + + // Pay out liquidation bounty + uint256 bounty = assetBalance.mulDivDown( + 1e18 - liquidationBonus, + 1e18 + ); + ERC20(asset).transfer(msg.sender, bounty); + + // Transfer remaining assets back to the safe + ERC20(asset).transfer(address(safe), assetBalance - bounty); + + // Put DAO in control of the safe + TakeOverSafeLib.takeoverSafe( + address(controller), + newOwners, + newThreshold + ); + + // Pause vault + vault.pause(); + } + } + + /*////////////////////////////////////////////////////////////// + MANAGEMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + address[] public newOwners; + uint256 public newThreshold; + uint256 public liquidationBonus; + + function setNewOwners(address[] memory newOwners_) external onlyOwner { + newOwners = newOwners_; + } + + function setNewThreshold(uint256 newThreshold_) external onlyOwner { + if (newThreshold_ < 1) revert("Invalid threshold"); + + newThreshold = newThreshold_; + } + + function setLiquidationBonus(uint256 liquidationBonus_) external onlyOwner { + if (liquidationBonus_ >= 1e18) revert("Invalid bonus"); + liquidationBonus = liquidationBonus_; + } + + function setTokenBalanceCalls( + ModuleCall[] memory calls + ) external onlyOwner { + for (uint256 i; i < calls.length; i++) { + if (calls[i].to == address(0)) revert("Invalid call"); + if (calls[i].data.length == 0) revert("Invalid call data"); + if (calls[i].value != 0) revert("Invalid call value"); + if (calls[i].operation != Operation.Call) + revert("Invalid call operation"); + + // We want to get the balance of all tokens in the safe that are not the vault asset + if (calls[i].to == address(vault.asset())) revert("Invalid call"); + + delete tokenBalanceCalls; + + tokenBalanceCalls.push(calls[i]); + } + } +} diff --git a/src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol b/src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol new file mode 100644 index 00000000..bddd26c1 --- /dev/null +++ b/src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.25; + +import {ControllerModule, ModuleCall, ISafe, Operation} from "src/peripheral/gnosis/controllerModule/MainControllerModule.sol"; +import {MultisigVault} from "src/vaults/multisig/phase1/MultisigVault.sol"; +import {ERC20} from "solmate/tokens/ERC20.sol"; +import {Owned} from "src/utils/Owned.sol"; +import {OwnerManager} from "safe-smart-account/base/OwnerManager.sol"; +import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; + +library TakeOverSafeLib { + function takeoverSafe( + address controller, + address[] memory newOwners_, + uint256 newThreshold_ + ) internal { + address gnosisSafe = ControllerModule(controller).gnosisSafe(); + ISafe safe = ISafe(gnosisSafe); + address[] memory owners = safe.getOwners(); + + // remove owners + for (uint256 i = (owners.length - 1); i > 0; --i) { + bool success = safe.execTransactionFromModule({ + to: gnosisSafe, + value: 0, + data: abi.encodeCall( + OwnerManager.removeOwner, + (owners[i - 1], owners[i], 1) + ), + operation: Operation.Call + }); + if (!success) { + revert("SM: owner removal failed"); + } + } + + for (uint256 i = 0; i < newOwners_.length; i++) { + bool success; + if (i == 0) { + if (newOwners_[i] == owners[i]) continue; + success = safe.execTransactionFromModule({ + to: gnosisSafe, + value: 0, + data: abi.encodeCall( + OwnerManager.swapOwner, + (address(0x1), owners[i], newOwners_[i]) + ), + operation: Operation.Call + }); + if (!success) { + revert("SM: owner replacement failed"); + } + continue; + } + success = safe.execTransactionFromModule({ + to: gnosisSafe, + value: 0, + data: abi.encodeCall( + OwnerManager.addOwnerWithThreshold, + (newOwners_[i], 1) + ), + operation: Operation.Call + }); + if (!success) { + revert("SM: owner addition failed"); + } + } + + if (newThreshold_ > 1) { + bool success = safe.execTransactionFromModule({ + to: gnosisSafe, + value: 0, + data: abi.encodeCall( + OwnerManager.changeThreshold, + (newThreshold_) + ), + operation: Operation.Call + }); + if (!success) { + revert("SM: change threshold failed"); + } + } + } +} diff --git a/src/peripheral/gnosis/controllerModule/WithdrawalModule.sol b/src/peripheral/gnosis/controllerModule/WithdrawalModule.sol index cdca94c9..f8d365df 100644 --- a/src/peripheral/gnosis/controllerModule/WithdrawalModule.sol +++ b/src/peripheral/gnosis/controllerModule/WithdrawalModule.sol @@ -1,48 +1,50 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.25; -interface IAsyncVault { - struct RedeemRequest { - uint256 shares; - uint256 requestTime; - } - - function redeemRequests( - address recipient, - address multisig - ) external view returns (RedeemRequest memory); +import {ControllerModule, ModuleCall, ISafe, Operation} from "src/peripheral/gnosis/controllerModule/MainControllerModule.sol"; +import {RequestBalance} from "src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol"; +import {Owned} from "src/utils/Owned.sol"; +import {TakeOverSafeLib} from "src/peripheral/gnosis/controllerModule/TakeOverSafeLib.sol"; + +interface IRequestableBalance { + function requestBalances( + address shareController + ) external view returns (RequestBalance memory); } -contract WithdrawalModule { - address controller; - address vault; - uint256 withdrawalPeriod; +contract WithdrawalModule is Owned { + IRequestableBalance public vault; + ControllerModule public controller; - constructor() {} + address[] public newOwners; + uint256 public newThreshold; - function checkViolation(bytes memory data) external view returns (bool) { - return _checkViolation(data); + constructor( + address vault_, + address controller_, + address owner_ + ) Owned(owner_) { + vault = IRequestableBalance(vault_); + controller = ControllerModule(controller_); } - function _checkViolation(bytes memory data) internal view returns (bool) { - (address recipient, address multisig) = abi.decode( - data, - (address, address) - ); + /*////////////////////////////////////////////////////////////// + EXECUTION LOGIC + //////////////////////////////////////////////////////////////*/ - IAsyncVault.RedeemRequest memory redeemRequest = IAsyncVault(vault) - .redeemRequests(recipient, multisig); + function handoverSafeAfterIgnoredWithdrawal( + address shareController + ) external { + RequestBalance memory requestBalance = vault.requestBalances( + shareController + ); - if ( - redeemRequest.requestTime + withdrawalPeriod < block.timestamp && - redeemRequest.shares > 0 - ) { - return true; - } - return false; - } + if (block.timestamp <= requestBalance.requestTime) revert("No timeout"); - function takeoverSafe(bytes memory data) external { - require(_checkViolation(data), "not valid"); + TakeOverSafeLib.takeoverSafe( + address(controller), + newOwners, + newThreshold + ); } } diff --git a/src/utils/Pausable.sol b/src/utils/Pausable.sol index dbb3cd8a..864d86fa 100644 --- a/src/utils/Pausable.sol +++ b/src/utils/Pausable.sol @@ -43,4 +43,12 @@ abstract contract Pausable { paused = false; emit Unpaused(msg.sender); } + + function pause() external virtual whenNotPaused { + _pause(); + } + + function unpause() external virtual whenPaused { + _unpause(); + } } diff --git a/src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol b/src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol index ded740b4..3bbdf799 100644 --- a/src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol +++ b/src/vaults/multisig/phase1/BaseControlledAsyncRedeem.sol @@ -1,5 +1,7 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; import {BaseERC7540} from "./BaseERC7540.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -7,17 +9,17 @@ import {SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; import {FixedPointMathLib} from "solmate/utils/FixedPointMathLib.sol"; import {IERC7540Redeem} from "ERC-7540/interfaces/IERC7540.sol"; +struct RequestBalance { + uint256 pendingShares; + uint256 requestTime; + uint256 claimableShares; + uint256 claimableAssets; +} + abstract contract BaseControlledAsyncRedeem is BaseERC7540, IERC7540Redeem { using FixedPointMathLib for uint256; - struct RequestBalance { - uint256 pendingShares; - uint256 requestTime; - uint256 claimableShares; - uint256 claimableAssets; - } - - mapping(address => RequestBalance) internal requestBalances; + mapping(address => RequestBalance) public requestBalances; /*////////////////////////////////////////////////////////////// ACCOUNTNG LOGIC diff --git a/src/vaults/multisig/phase1/BaseERC7540.sol b/src/vaults/multisig/phase1/BaseERC7540.sol index 5913f68e..3fa8f8f5 100644 --- a/src/vaults/multisig/phase1/BaseERC7540.sol +++ b/src/vaults/multisig/phase1/BaseERC7540.sol @@ -1,5 +1,7 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.25 + +pragma solidity ^0.8.25; import {ERC4626} from "solmate/tokens/ERC4626.sol"; import {ERC20} from "solmate/tokens/ERC20.sol"; @@ -30,17 +32,41 @@ abstract contract BaseERC7540 is string memory _symbol ) Owned(_owner) ERC4626(ERC20(_asset), _name, _symbol) {} + /*////////////////////////////////////////////////////////////// + ROLE LOGIC + //////////////////////////////////////////////////////////////*/ + + mapping(bytes32 => mapping(address => bool)) public hasRole; + + event RoleUpdated(bytes32 role, address account, bool approved); + + function updateRole(bytes32 role, address account, bool approved) public { + hasRole[role][account] = approved; + + emit RoleUpdated(role, account, approved); + } + + modifier onlyRoleOrOwner(bytes32 role) { + require( + hasRole[role][msg.sender] || msg.sender == owner, + "BaseERC7540/not-authorized" + ); + _; + } + /*////////////////////////////////////////////////////////////// PAUSING LOGIC //////////////////////////////////////////////////////////////*/ + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + /// @notice Pause Deposits. Caller must be owner. - function pause() external virtual onlyOwner { + function pause() external override onlyRoleOrOwner(PAUSER_ROLE) { _pause(); } /// @notice Unpause Deposits. Caller must be owner. - function unpause() external virtual onlyOwner { + function unpause() external override onlyOwner { _unpause(); } diff --git a/src/vaults/multisig/phase1/MultisigVault.sol b/src/vaults/multisig/phase1/MultisigVault.sol index 6d011c09..5877130a 100644 --- a/src/vaults/multisig/phase1/MultisigVault.sol +++ b/src/vaults/multisig/phase1/MultisigVault.sol @@ -178,11 +178,11 @@ abstract contract MultisigVault is BaseControlledAsyncRedeem { //////////////////////////////////////////////////////////////*/ function beforeWithdraw(uint256 assets, uint256) internal virtual override { - _takeFees(); + if (!paused) _takeFees(); } function afterDeposit(uint256 assets, uint256) internal virtual override { - _takeFees(); + if (!paused) _takeFees(); SafeTransferLib.safeTransfer(asset, multisig, assets); } @@ -265,7 +265,7 @@ abstract contract MultisigVault is BaseControlledAsyncRedeem { : 0; } - function setFees(Fees memory fees_) public onlyOwner { + function setFees(Fees memory fees_) public onlyOwner whenNotPaused { _takeFees(); _setFees(fees_); @@ -289,7 +289,7 @@ abstract contract MultisigVault is BaseControlledAsyncRedeem { fees = fees_; } - function takeFees() external { + function takeFees() external whenNotPaused { _takeFees(); }