From 6b5ab8ea0ce7b5017954b36bf43a08339d859235 Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 01:33:09 -0500 Subject: [PATCH 01/12] feat: initial work on sett upgrades --- .../badger-remote/DefenderStorageless.sol | 19 ++ .../badger-remote/PauseableStorageless.sol | 69 +++++ contracts/badger-remote/RemoteDefender.sol | 28 ++ contracts/badger-remote/RemoteFreezer.sol | 29 ++ contracts/badger-remote/RemotePauser.sol | 25 ++ contracts/badger-sett/DiggSett.sol | 6 +- contracts/badger-sett/sett-forks/README.md | 42 +++ contracts/badger-sett/sett-forks/Sett_A.sol | 293 ++++++++++++++++++ contracts/badger-sett/sett-forks/Sett_B.sol | 284 +++++++++++++++++ contracts/badger-sett/sett-forks/Sett_C.sol | 285 +++++++++++++++++ ...ategy_registry.py => contract_registry.py} | 2 +- interfaces/remote/IRemoteDefender.sol | 11 + interfaces/remote/IRemoteFreezer.sol | 11 + interfaces/remote/IRemotePauser.sol | 11 + scripts/deploy/upgrade.py | 59 ++++ scripts/systems/badger_system.py | 97 +++--- scripts/systems/constants.py | 6 + scripts/systems/digg_system.py | 4 +- scripts/systems/upgrade_system.py | 59 ++++ 19 files changed, 1291 insertions(+), 49 deletions(-) create mode 100644 contracts/badger-remote/DefenderStorageless.sol create mode 100644 contracts/badger-remote/PauseableStorageless.sol create mode 100644 contracts/badger-remote/RemoteDefender.sol create mode 100644 contracts/badger-remote/RemoteFreezer.sol create mode 100644 contracts/badger-remote/RemotePauser.sol create mode 100644 contracts/badger-sett/sett-forks/README.md create mode 100644 contracts/badger-sett/sett-forks/Sett_A.sol create mode 100644 contracts/badger-sett/sett-forks/Sett_B.sol create mode 100644 contracts/badger-sett/sett-forks/Sett_C.sol rename helpers/{sett/strategy_registry.py => contract_registry.py} (97%) create mode 100644 interfaces/remote/IRemoteDefender.sol create mode 100644 interfaces/remote/IRemoteFreezer.sol create mode 100644 interfaces/remote/IRemotePauser.sol create mode 100644 scripts/deploy/upgrade.py create mode 100644 scripts/systems/upgrade_system.py diff --git a/contracts/badger-remote/DefenderStorageless.sol b/contracts/badger-remote/DefenderStorageless.sol new file mode 100644 index 00000000..2fa31ba2 --- /dev/null +++ b/contracts/badger-remote/DefenderStorageless.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "interfaces/remote/IRemoteDefender.sol"; + +/* + DefenderStorageless is a no-storage required inheritable defender of unapproved contract access. + Contracts may safely inherit this w/o messing up their internal storage layout. + */ +contract DefenderStorageless { + // Defend against access by unapproved contracts (EOAs are allowed access). + modifier defend(address defender) { + require(IRemoteDefender(defender).approved(msg.sender) || + msg.sender == tx.origin, + "Access denied for caller"); + _; + } +} diff --git a/contracts/badger-remote/PauseableStorageless.sol b/contracts/badger-remote/PauseableStorageless.sol new file mode 100644 index 00000000..712bfd22 --- /dev/null +++ b/contracts/badger-remote/PauseableStorageless.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "interfaces/remote/IRemotePauser.sol"; + +/* + PauseableStorageless is a no-storage required inheritable version of OZ's Pauseable contract. + Contracts may safely inherit this w/o messing up their internal storage layout. + */ +contract PauseableStorageless { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + /** + * @dev Modifier to make a function callable only when the contract is not paused. + * + * Requirements: + * + * - The contract must not be paused. + */ + modifier whenNotPaused(address pauser) { + require(!IRemotePauser(pauser).paused(), "Pausable: paused"); + _; + } + + /** + * @dev Modifier to make a function callable only when the contract is paused. + * + * Requirements: + * + * - The contract must be paused. + */ + modifier whenPaused(address pauser) { + require(IRemotePauser(pauser).paused(), "Pausable: not paused"); + _; + } + + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause(address pauser) internal virtual whenNotPaused { + IRemotePauser(pauser).pause(); + emit Paused(msg.sender); + } + + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + IRemotePauser(pauser).unpause(); + emit Unpaused(msg.sender); + } +} diff --git a/contracts/badger-remote/RemoteDefender.sol b/contracts/badger-remote/RemoteDefender.sol new file mode 100644 index 00000000..d927f48c --- /dev/null +++ b/contracts/badger-remote/RemoteDefender.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/* + RemoteDefender defends against unapproved address access. + */ +contract RemoteDefender is OwnableUpgradeable { + mapping(address => bool) private _approved; + + function initialize() public initializer { + __Ownable_init(); + } + + function approved(address account) external view returns (bool) { + return _approved[account]; + } + + function approve(address account) external onlyOwner { + _approved[account] = true; + } + + function revoke(address account) external onlyOwner { + _approved[account] = false; + } +} diff --git a/contracts/badger-remote/RemoteFreezer.sol b/contracts/badger-remote/RemoteFreezer.sol new file mode 100644 index 00000000..cfae4f71 --- /dev/null +++ b/contracts/badger-remote/RemoteFreezer.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/* + RemoteFreezer handles reporting of frozen state and + freezing of addresses by the owner. + */ +contract RemoteFreezer is OwnableUpgradeable { + mapping(address => bool) private _frozen; + + function initialize() public initializer { + __Ownable_init(); + } + + function frozen(address account) external view returns (bool) { + return _frozen[account]; + } + + function freeze(address account) external onlyOwner { + _frozen[account] = true; + } + + function unfreeze(address account) external onlyOwner { + _frozen[account] = false; + } +} diff --git a/contracts/badger-remote/RemotePauser.sol b/contracts/badger-remote/RemotePauser.sol new file mode 100644 index 00000000..06b51ee6 --- /dev/null +++ b/contracts/badger-remote/RemotePauser.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "interfaces/remote/IPauser.sol"; + +/* + RemotePauser only handles paused state for msg.sender (only msg.sender can modifies his own state) + and takes care of state storage away from the originating contract. + */ +contract RemotePauser { + mapping(address => bool) private _paused; + + function paused() external view returns (bool) { + return _paused[msg.sender]; + } + + function pause() external { + _paused[msg.sender] = true; + } + + function unpause() external { + _paused[msg.sender] = false; + } +} diff --git a/contracts/badger-sett/DiggSett.sol b/contracts/badger-sett/DiggSett.sol index e8561ef6..c62a2951 100644 --- a/contracts/badger-sett/DiggSett.sol +++ b/contracts/badger-sett/DiggSett.sol @@ -94,6 +94,7 @@ contract DiggSett is Sett { // Check balance uint256 _sharesInSett = digg.sharesOf(address(this)); + uint256 _fee; // If we don't have sufficient idle want in Sett, withdraw from Strategy if (_sharesInSett < _sharesToRedeem) { @@ -109,9 +110,12 @@ contract DiggSett is Sett { if (_diff < _toWithdraw) { _sharesToRedeem = _sharesInSett.add(_diff); } + _fee = _processWithdrawalFee(digg.sharesToFragments(_sharesToRedeem.sub(_sharesInSett))); + } else { + _fee = _processWithdrawalFee(digg.sharesToFragments(_sharesToRedeem)); } // Transfer the corresponding number of shares, scaled to DIGG fragments, to recipient - digg.transfer(msg.sender, digg.sharesToFragments(_sharesToRedeem)); + digg.transfer(msg.sender, digg.sharesToFragments(_sharesToRedeem).sub(_fee)); } } diff --git a/contracts/badger-sett/sett-forks/README.md b/contracts/badger-sett/sett-forks/README.md new file mode 100644 index 00000000..ce114d08 --- /dev/null +++ b/contracts/badger-sett/sett-forks/README.md @@ -0,0 +1,42 @@ +# Layout Upgradeable + +In order to maintain layout backwards compatibilty w/ different versions of contracts (e.g. Sett). + +Changelog: + +V1.1 +* Strategist no longer has special function calling permissions +* Version function added to contract +* All write functions, with the exception of transfer, are pausable +* Keeper or governance can pause +* Only governance can unpause + +V1.2 +* Transfer functions are now pausable along with all other non-permissioned write functions +* All permissioned write functions, with the exception of pause() & unpause(), are pausable as well + +V1.3 +* Withdrawals are processed from idle want in sett. + + +Looking at it, all the Setts inherit from one of these (except the DIGG sett): +"SettV1": "0xE4Ae305b08434bF3D74e0086592627F913a258A9", +"SettV1.1": "0x175586ac3f8A7463499D1019A30120aa6fC67C5f", +So +Old: + "native.badger": "0x19D97D8fA813EE2f51aD4B4e04EA08bAf4DFfC28", + "native.renCrv": "0x6dEf55d2e18486B9dDfaA075bc4e4EE0B28c1545", + "native.sbtcCrv": "0xd04c48A53c111300aD41190D63681ed3dAd998eC", + "native.tbtcCrv": "0xb9D076fDe463dbc9f915E5392F807315Bf940334", + "native.uniBadgerWbtc": "0x235c9e24D3FB2FAFd58a2E49D454Fdcd2DBf7FF1", + "harvest.renCrv": "0xAf5A1DECfa95BAF63E0084a35c62592B774A2A87", +New: + "native.sushiWbtcEth": "0x758A43EE2BFf8230eeb784879CdcFF4828F2544D", + "native.sushiBadgerWbtc": "0x1862A18181346EBd9EdAf800804f89190DeF24a5", + "native.digg": "0x7e7E112A68d8D2E221E11047a72fFC1065c38e1a", + "native.uniDiggWbtc": "0xC17078FDd324CC473F8175Dc5290fae5f2E84714", + "native.sushiDiggWbtc": "0x88128580ACdD9c04Ce47AFcE196875747bF2A9f6" +Basically pausable vs not pausable +DiggSett extends from the new variety +Which makes the in-place migration a lot more palatable +LegacySett / Sett diff --git a/contracts/badger-sett/sett-forks/Sett_A.sol b/contracts/badger-sett/sett-forks/Sett_A.sol new file mode 100644 index 00000000..6a51b5dd --- /dev/null +++ b/contracts/badger-sett/sett-forks/Sett_A.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.11; + +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; + +import "../../interfaces/badger/IController.sol"; +import "../../interfaces/erc20/IERC20Detailed.sol"; +import "../badger-remote/PauseableStorageless.sol"; +import "../badger-remote/DefenderStorageless.sol"; +import "../SettAccessControlDefended.sol"; + +/* + Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol +*/ +contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, DefenderStorageless { + using SafeERC20Upgradeable for IERC20Upgradeable; + using AddressUpgradeable for address; + using SafeMathUpgradeable for uint256; + + IERC20Upgradeable public token; + + uint256 public min; + uint256 public constant max = 10000; + + address public controller; + + mapping(address => uint256) public blockLock; + + string internal constant _defaultNamePrefix = "Badger Sett "; + string internal constant _symbolSymbolPrefix = "b"; + + event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); + + address public guardian; + // Remote pauser. + address public pauser; + // Remote defender. + address public defender; + + function initialize( + address _token, + address _controller, + address _governance, + address _keeper, + address _guardian, + bool _overrideTokenName, + string memory _namePrefix, + string memory _symbolPrefix + ) public initializer whenNotPaused(pauser) { + IERC20Detailed namedToken = IERC20Detailed(_token); + string memory tokenName = namedToken.name(); + string memory tokenSymbol = namedToken.symbol(); + + string memory name; + string memory symbol; + + if (_overrideTokenName) { + name = string(abi.encodePacked(_namePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolPrefix, tokenSymbol)); + } else { + name = string(abi.encodePacked(_defaultNamePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolSymbolPrefix, tokenSymbol)); + } + + __ERC20_init(name, symbol); + + token = IERC20Upgradeable(_token); + governance = _governance; + strategist = address(0); + keeper = _keeper; + controller = _controller; + guardian = _guardian; + + min = 9500; + + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + + // Paused on launch + _pause(); + } + + /// ===== Modifiers ===== + + function _onlyController() internal view { + require(msg.sender == controller, "onlyController"); + } + + function _onlyAuthorizedPausers() internal view { + require(msg.sender == guardian || msg.sender == governance, "onlyPausers"); + } + + function _blockLocked() internal view { + require(blockLock[msg.sender] < block.number, "blockLocked"); + } + + /// ===== View Functions ===== + + function version() public view returns (string memory) { + return "1.3"; + } + + function getPricePerFullShare() public virtual view returns (uint256) { + if (totalSupply() == 0) { + return 1e18; + } + return balance().mul(1e18).div(totalSupply()); + } + + /// @notice Return the total balance of the underlying token within the system + /// @notice Sums the balance in the Sett, the Controller, and the Strategy + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); + } + + /// @notice Defines how much of the Setts' underlying can be borrowed by the Strategy for use + /// @notice Custom logic in here for how much the vault allows to be borrowed + /// @notice Sets minimum required on-hand to keep small withdrawals cheap + function available() public virtual view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + /// ===== Public Actions ===== + + /// @notice Deposit assets into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function deposit(uint256 _amount) public whenNotPaused(pauser) defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(_amount); + } + + /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function depositAll() external whenNotPaused(pauser) defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(token.balanceOf(msg.sender)); + } + + /// @notice No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public whenNotPaused(pauser) defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(_shares); + } + + /// @notice Convenience function: Withdraw all shares of the sender + function withdrawAll() external whenNotPaused(pauser) defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(balanceOf(msg.sender)); + } + + /// ===== Permissioned Actions: Governance ===== + + /// @notice Set minimum threshold of underlying that must be deposited in strategy + /// @notice Can only be changed by governance + function setMin(uint256 _min) external whenNotPaused(pauser) { + _onlyGovernance(); + min = _min; + } + + /// @notice Change controller address + /// @notice Can only be changed by governance + function setController(address _controller) public whenNotPaused(pauser) { + _onlyGovernance(); + controller = _controller; + } + + /// @notice Change guardian address + /// @notice Can only be changed by governance + function setGuardian(address _guardian) external whenNotPaused(pauser) { + _onlyGovernance(); + guardian = _guardian; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + _onlyGovernance(); + require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); + withdrawalFee = _withdrawalFee; + } + + /// ===== Permissioned Actions: Controller ===== + + /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + /// @notice Only controller can trigger harvests + function harvest(address reserve, uint256 amount) external whenNotPaused(pauser) { + _onlyController(); + require(reserve != address(token), "token"); + IERC20Upgradeable(reserve).safeTransfer(controller, amount); + } + + /// ===== Permissioned Functions: Trusted Actors ===== + + /// @notice Transfer the underlying available to be claimed to the controller + /// @notice The controller will deposit into the Strategy for yield-generating activities + /// @notice Permissionless operation + function earn() public whenNotPaused(pauser) { + _onlyAuthorizedActors(); + + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + /// @dev Emit event tracking current full price per share + /// @dev Provides a pure on-chain way of approximating APY + function trackFullPricePerShare() external whenNotPaused(pauser) { + _onlyAuthorizedActors(); + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + } + + function pause() external { + _onlyAuthorizedPausers(); + _pause(); + } + + function unpause() external { + _onlyGovernance(); + _unpause(); + } + + /// ===== Internal Implementations ===== + + /// @dev Calculate the number of shares to issue for a given deposit + /// @dev This is based on the realized value of underlying assets between Sett & associated Strategy + function _deposit(uint256 _amount) internal virtual { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + // No rebalance implementation for lower fees and faster swaps + function _withdraw(uint256 _shares) internal virtual { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _toWithdraw = r.sub(b); + IController(controller).withdraw(address(token), _toWithdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _toWithdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function _lockForBlock(address account) internal { + blockLock[account] = block.number; + } + + /// ===== ERC20 Overrides ===== + + /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. + function transfer(address recipient, uint256 amount) public virtual override whenNotPaused(pauser) returns (bool) { + _blockLocked(); + return super.transfer(recipient, amount); + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override whenNotPaused(pauser) returns (bool) { + _blockLocked(); + return super.transferFrom(sender, recipient, amount); + } +} diff --git a/contracts/badger-sett/sett-forks/Sett_B.sol b/contracts/badger-sett/sett-forks/Sett_B.sol new file mode 100644 index 00000000..a8a40678 --- /dev/null +++ b/contracts/badger-sett/sett-forks/Sett_B.sol @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.11; + +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; + +import "../../interfaces/badger/IController.sol"; +import "../../interfaces/erc20/IERC20Detailed.sol"; +import "../badger-remote/DefenderStorageless.sol"; +import "../SettAccessControlDefended.sol"; + +contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless { + using SafeERC20Upgradeable for IERC20Upgradeable; + using AddressUpgradeable for address; + using SafeMathUpgradeable for uint256; + + IERC20Upgradeable public token; + + uint256 public min; + uint256 public constant max = 10000; + + address public controller; + address public guardian; + + mapping(address => uint256) public blockLock; + + string internal constant _defaultNamePrefix = "Badger Sett "; + string internal constant _symbolSymbolPrefix = "b"; + + event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); + + function initialize( + address _token, + address _controller, + address _governance, + address _keeper, + address _guardian, + bool _overrideTokenName, + string memory _namePrefix, + string memory _symbolPrefix + ) public initializer whenNotPaused { + IERC20Detailed namedToken = IERC20Detailed(_token); + string memory tokenName = namedToken.name(); + string memory tokenSymbol = namedToken.symbol(); + + string memory name; + string memory symbol; + + if (_overrideTokenName) { + name = string(abi.encodePacked(_namePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolPrefix, tokenSymbol)); + } else { + name = string(abi.encodePacked(_defaultNamePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolSymbolPrefix, tokenSymbol)); + } + + __ERC20_init(name, symbol); + + token = IERC20Upgradeable(_token); + governance = _governance; + strategist = address(0); + keeper = _keeper; + controller = _controller; + guardian = _guardian; + + min = 9500; + + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + + // Paused on launch + _pause(); + } + + /// ===== Modifiers ===== + + function _onlyController() internal view { + require(msg.sender == controller, "onlyController"); + } + + function _onlyAuthorizedPausers() internal view { + require(msg.sender == guardian || msg.sender == governance, "onlyPausers"); + } + + function _blockLocked() internal view { + require(blockLock[msg.sender] < block.number, "blockLocked"); + } + + /// ===== View Functions ===== + + function version() public view returns (string memory) { + return "1.3"; + } + + function getPricePerFullShare() public virtual view returns (uint256) { + if (totalSupply() == 0) { + return 1e18; + } + return balance().mul(1e18).div(totalSupply()); + } + + /// @notice Return the total balance of the underlying token within the system + /// @notice Sums the balance in the Sett, the Controller, and the Strategy + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); + } + + /// @notice Defines how much of the Setts' underlying can be borrowed by the Strategy for use + /// @notice Custom logic in here for how much the vault allows to be borrowed + /// @notice Sets minimum required on-hand to keep small withdrawals cheap + function available() public virtual view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + /// ===== Public Actions ===== + + /// @notice Deposit assets into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function deposit(uint256 _amount) public whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(_amount); + } + + /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function depositAll() external whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(token.balanceOf(msg.sender)); + } + + /// @notice No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(_shares); + } + + /// @notice Convenience function: Withdraw all shares of the sender + function withdrawAll() external whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(balanceOf(msg.sender)); + } + + /// ===== Permissioned Actions: Governance ===== + + /// @notice Set minimum threshold of underlying that must be deposited in strategy + /// @notice Can only be changed by governance + function setMin(uint256 _min) external whenNotPaused { + _onlyGovernance(); + min = _min; + } + + /// @notice Change controller address + /// @notice Can only be changed by governance + function setController(address _controller) public whenNotPaused { + _onlyGovernance(); + controller = _controller; + } + + /// @notice Change guardian address + /// @notice Can only be changed by governance + function setGuardian(address _guardian) external whenNotPaused { + _onlyGovernance(); + guardian = _guardian; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + _onlyGovernance(); + require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); + withdrawalFee = _withdrawalFee; + } + + /// ===== Permissioned Actions: Controller ===== + + /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + /// @notice Only controller can trigger harvests + function harvest(address reserve, uint256 amount) external whenNotPaused { + _onlyController(); + require(reserve != address(token), "token"); + IERC20Upgradeable(reserve).safeTransfer(controller, amount); + } + + /// ===== Permissioned Functions: Trusted Actors ===== + + /// @notice Transfer the underlying available to be claimed to the controller + /// @notice The controller will deposit into the Strategy for yield-generating activities + /// @notice Permissionless operation + function earn() public whenNotPaused { + _onlyAuthorizedActors(); + + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + /// @dev Emit event tracking current full price per share + /// @dev Provides a pure on-chain way of approximating APY + function trackFullPricePerShare() external whenNotPaused { + _onlyAuthorizedActors(); + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + } + + function pause() external { + _onlyAuthorizedPausers(); + _pause(); + } + + function unpause() external { + _onlyGovernance(); + _unpause(); + } + + /// ===== Internal Implementations ===== + + /// @dev Calculate the number of shares to issue for a given deposit + /// @dev This is based on the realized value of underlying assets between Sett & associated Strategy + function _deposit(uint256 _amount) internal virtual { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + // No rebalance implementation for lower fees and faster swaps + function _withdraw(uint256 _shares) internal virtual { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _toWithdraw = r.sub(b); + IController(controller).withdraw(address(token), _toWithdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _toWithdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function _lockForBlock(address account) internal { + blockLock[account] = block.number; + } + + /// ===== ERC20 Overrides ===== + + /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. + function transfer(address recipient, uint256 amount) public virtual override whenNotPaused returns (bool) { + _blockLocked(); + return super.transfer(recipient, amount); + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override whenNotPaused returns (bool) { + _blockLocked(); + return super.transferFrom(sender, recipient, amount); + } +} diff --git a/contracts/badger-sett/sett-forks/Sett_C.sol b/contracts/badger-sett/sett-forks/Sett_C.sol new file mode 100644 index 00000000..d5a5b711 --- /dev/null +++ b/contracts/badger-sett/sett-forks/Sett_C.sol @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.11; + +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; + +import "../../interfaces/badger/IController.sol"; +import "../../interfaces/erc20/IERC20Detailed.sol"; +import "../badger-remote/DefenderStorageless.sol"; +import "../SettAccessControlDefended.sol"; + +contract Sett_C is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless { + using SafeERC20Upgradeable for IERC20Upgradeable; + using AddressUpgradeable for address; + using SafeMathUpgradeable for uint256; + + IERC20Upgradeable public token; + + uint256 public min; + uint256 public constant max = 10000; + + address public controller; + + mapping(address => uint256) public blockLock; + + string internal constant _defaultNamePrefix = "Badger Sett "; + string internal constant _symbolSymbolPrefix = "b"; + + address public guardian; + + event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); + + function initialize( + address _token, + address _controller, + address _governance, + address _keeper, + address _guardian, + bool _overrideTokenName, + string memory _namePrefix, + string memory _symbolPrefix + ) public initializer whenNotPaused { + IERC20Detailed namedToken = IERC20Detailed(_token); + string memory tokenName = namedToken.name(); + string memory tokenSymbol = namedToken.symbol(); + + string memory name; + string memory symbol; + + if (_overrideTokenName) { + name = string(abi.encodePacked(_namePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolPrefix, tokenSymbol)); + } else { + name = string(abi.encodePacked(_defaultNamePrefix, tokenName)); + symbol = string(abi.encodePacked(_symbolSymbolPrefix, tokenSymbol)); + } + + __ERC20_init(name, symbol); + + token = IERC20Upgradeable(_token); + governance = _governance; + strategist = address(0); + keeper = _keeper; + controller = _controller; + guardian = _guardian; + + min = 9500; + + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + + // Paused on launch + _pause(); + } + + /// ===== Modifiers ===== + + function _onlyController() internal view { + require(msg.sender == controller, "onlyController"); + } + + function _onlyAuthorizedPausers() internal view { + require(msg.sender == guardian || msg.sender == governance, "onlyPausers"); + } + + function _blockLocked() internal view { + require(blockLock[msg.sender] < block.number, "blockLocked"); + } + + /// ===== View Functions ===== + + function version() public view returns (string memory) { + return "1.3"; + } + + function getPricePerFullShare() public virtual view returns (uint256) { + if (totalSupply() == 0) { + return 1e18; + } + return balance().mul(1e18).div(totalSupply()); + } + + /// @notice Return the total balance of the underlying token within the system + /// @notice Sums the balance in the Sett, the Controller, and the Strategy + function balance() public view returns (uint256) { + return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); + } + + /// @notice Defines how much of the Setts' underlying can be borrowed by the Strategy for use + /// @notice Custom logic in here for how much the vault allows to be borrowed + /// @notice Sets minimum required on-hand to keep small withdrawals cheap + function available() public virtual view returns (uint256) { + return token.balanceOf(address(this)).mul(min).div(max); + } + + /// ===== Public Actions ===== + + /// @notice Deposit assets into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function deposit(uint256 _amount) public whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(_amount); + } + + /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user + /// @notice Only callable by EOA accounts that pass the defend() check + function depositAll() external whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _deposit(token.balanceOf(msg.sender)); + } + + /// @notice No rebalance implementation for lower fees and faster swaps + function withdraw(uint256 _shares) public whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(_shares); + } + + /// @notice Convenience function: Withdraw all shares of the sender + function withdrawAll() external whenNotPaused defend(defender) { + _blockLocked(); + + _lockForBlock(msg.sender); + _withdraw(balanceOf(msg.sender)); + } + + /// ===== Permissioned Actions: Governance ===== + + /// @notice Set minimum threshold of underlying that must be deposited in strategy + /// @notice Can only be changed by governance + function setMin(uint256 _min) external whenNotPaused { + _onlyGovernance(); + min = _min; + } + + /// @notice Change controller address + /// @notice Can only be changed by governance + function setController(address _controller) public whenNotPaused { + _onlyGovernance(); + controller = _controller; + } + + /// @notice Change guardian address + /// @notice Can only be changed by governance + function setGuardian(address _guardian) external whenNotPaused { + _onlyGovernance(); + guardian = _guardian; + } + + function setWithdrawalFee(uint256 _withdrawalFee) external { + _onlyGovernance(); + require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); + withdrawalFee = _withdrawalFee; + } + + /// ===== Permissioned Actions: Controller ===== + + /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' + /// @notice Only controller can trigger harvests + function harvest(address reserve, uint256 amount) external whenNotPaused { + _onlyController(); + require(reserve != address(token), "token"); + IERC20Upgradeable(reserve).safeTransfer(controller, amount); + } + + /// ===== Permissioned Functions: Trusted Actors ===== + + /// @notice Transfer the underlying available to be claimed to the controller + /// @notice The controller will deposit into the Strategy for yield-generating activities + /// @notice Permissionless operation + function earn() public whenNotPaused { + _onlyAuthorizedActors(); + + uint256 _bal = available(); + token.safeTransfer(controller, _bal); + IController(controller).earn(address(token), _bal); + } + + /// @dev Emit event tracking current full price per share + /// @dev Provides a pure on-chain way of approximating APY + function trackFullPricePerShare() external whenNotPaused { + _onlyAuthorizedActors(); + emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); + } + + function pause() external { + _onlyAuthorizedPausers(); + _pause(); + } + + function unpause() external { + _onlyGovernance(); + _unpause(); + } + + /// ===== Internal Implementations ===== + + /// @dev Calculate the number of shares to issue for a given deposit + /// @dev This is based on the realized value of underlying assets between Sett & associated Strategy + function _deposit(uint256 _amount) internal virtual { + uint256 _pool = balance(); + uint256 _before = token.balanceOf(address(this)); + token.safeTransferFrom(msg.sender, address(this), _amount); + uint256 _after = token.balanceOf(address(this)); + _amount = _after.sub(_before); // Additional check for deflationary tokens + uint256 shares = 0; + if (totalSupply() == 0) { + shares = _amount; + } else { + shares = (_amount.mul(totalSupply())).div(_pool); + } + _mint(msg.sender, shares); + } + + // No rebalance implementation for lower fees and faster swaps + function _withdraw(uint256 _shares) internal virtual { + uint256 r = (balance().mul(_shares)).div(totalSupply()); + _burn(msg.sender, _shares); + + // Check balance + uint256 b = token.balanceOf(address(this)); + if (b < r) { + uint256 _toWithdraw = r.sub(b); + IController(controller).withdraw(address(token), _toWithdraw); + uint256 _after = token.balanceOf(address(this)); + uint256 _diff = _after.sub(b); + if (_diff < _toWithdraw) { + r = b.add(_diff); + } + } + + token.safeTransfer(msg.sender, r); + } + + function _lockForBlock(address account) internal { + blockLock[account] = block.number; + } + + /// ===== ERC20 Overrides ===== + + /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. + function transfer(address recipient, uint256 amount) public virtual override whenNotPaused returns (bool) { + _blockLocked(); + return super.transfer(recipient, amount); + } + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) public virtual override whenNotPaused returns (bool) { + _blockLocked(); + return super.transferFrom(sender, recipient, amount); + } +} diff --git a/helpers/sett/strategy_registry.py b/helpers/contract_registry.py similarity index 97% rename from helpers/sett/strategy_registry.py rename to helpers/contract_registry.py index 134f8023..57130281 100644 --- a/helpers/sett/strategy_registry.py +++ b/helpers/contract_registry.py @@ -41,5 +41,5 @@ } -def strategy_name_to_artifact(name): +def contract_name_to_artifact(name): return name_to_artifact[name] diff --git a/interfaces/remote/IRemoteDefender.sol b/interfaces/remote/IRemoteDefender.sol new file mode 100644 index 00000000..d288e81f --- /dev/null +++ b/interfaces/remote/IRemoteDefender.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +interface IRemoteDefender { + function appproved(address) external view returns (bool); + + function approve(address) external; + + function revoke(address) external; +} diff --git a/interfaces/remote/IRemoteFreezer.sol b/interfaces/remote/IRemoteFreezer.sol new file mode 100644 index 00000000..b7dd6e43 --- /dev/null +++ b/interfaces/remote/IRemoteFreezer.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +interface IRemoteFreezer { + function frozen(address) external view returns (bool); + + function freeze(address) external; + + function unfreeze(address) external; +} diff --git a/interfaces/remote/IRemotePauser.sol b/interfaces/remote/IRemotePauser.sol new file mode 100644 index 00000000..e0d11dfc --- /dev/null +++ b/interfaces/remote/IRemotePauser.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.6.0; + +interface IRemotePauser { + function paused() external view returns (bool); + + function pause() external; + + function unpause() external; +} diff --git a/scripts/deploy/upgrade.py b/scripts/deploy/upgrade.py new file mode 100644 index 00000000..01817252 --- /dev/null +++ b/scripts/deploy/upgrade.py @@ -0,0 +1,59 @@ +from brownie.network.contract import ProjectContract + +from scripts.systems.badger_system import BadgerSystem +from helpers.gnosis_safe import GnosisSafe, MultisigTxMetadata + + +# Upgrades versioned proxy contract if not latest version. +def upgrade_versioned_proxy( + badger: BadgerSystem, + # upgradeable proxy contract + proxy: ProjectContract, + # latest logic contract + logic: ProjectContract, +) -> None: + # Do nothing if the proxy and logic contracts have the same version + # or if the logic contract has an older version. + proxyVersion = _get_version(proxy) + logicVersion = _get_version(logic) + if ( + proxyVersion == logicVersion or + float(logicVersion) < float(proxyVersion) + ): + return + + multi = GnosisSafe(badger.devMultisig) + + multi.execute( + MultisigTxMetadata(description="Upgrade versioned proxy contract with new logic version",), + { + "to": badger.devProxyAdmin.address, + "data": badger.devProxyAdmin.upgrade.encode_input( + proxy, logic + ), + }, + ) + + +def _get_version(contract: ProjectContract) -> float: + # try all the methods in priority order + methods = [ + "version", + "baseStrategyVersion", + ] + + for method in methods: + version, ok = _try_get_version(contract, method) + if ok: + return version + + # NB: Prior to V1.1, Setts do not have a version function. + return 0.0 + + +def _try_get_version(contract: ProjectContract, method: str) -> (float, bool): + try: + return getattr(contract, method)(), True + except Exception: + return 0.0, False + diff --git a/scripts/systems/badger_system.py b/scripts/systems/badger_system.py index ca450423..13f7ef91 100644 --- a/scripts/systems/badger_system.py +++ b/scripts/systems/badger_system.py @@ -14,19 +14,22 @@ ) from helpers.registry import registry from helpers.time_utils import days -from helpers.sett.strategy_registry import name_to_artifact, strategy_name_to_artifact +from helpers.contract_registry import name_to_artifact, contract_name_to_artifact from helpers.proxy_utils import deploy_proxy, deploy_proxy_admin from helpers.gnosis_safe import GnosisSafe, MultisigTxMetadata -from helpers.sett.strategy_registry import name_to_artifact -from scripts.systems.constants import SettType -from scripts.systems.digg_system import connect_digg -from scripts.systems.constants import SettType from scripts.systems.digg_system import DiggSystem, connect_digg from scripts.systems.claw_system import ClawSystem from scripts.systems.swap_system import SwapSystem from scripts.systems.gnosis_safe_system import connect_gnosis_safe -from scripts.systems.sett_system import deploy_controller, deploy_strategy from scripts.systems.uniswap_system import UniswapSystem +from scripts.systems.upgrade_system import UpgradeSystem +from scripts.systems.constants import ( + SETT_SUFFIX, + STRATEGY_SUFFIX, + GEYSER_SUFFIX, + CONTROLLER_SUFFIX, + SettType, +) from rich.console import Console @@ -119,7 +122,7 @@ def print_to_file(badger, path): if badger.digg: digg = badger.digg digg_system = { - "owner": digg.owner.address, + "owner": digg.owner, "devProxyAdmin": digg.devProxyAdmin.address, "daoProxyAdmin": digg.daoProxyAdmin.address, "uFragments": digg.uFragments.address, @@ -229,7 +232,6 @@ def __init__( ): self.config = config self.contracts_static = [] - self.contracts_upgradeable = {} self.gas_strategy = default_gas_strategy # Digg system ref, lazily set. self.digg = None @@ -237,6 +239,8 @@ def __init__( self.claw = None # Swap system ref, lazily set. self.swap = None + # Upgrade system ref, eagerly set. + self.upgrade = UpgradeSystem(self, deployer) # Unlock accounts in test mode if rpc.is_active(): @@ -298,9 +302,6 @@ def __init__( def track_contract_static(self, contract): self.contracts_static.append(contract) - def track_contract_upgradeable(self, key, contract): - self.contracts_upgradeable[key] = contract - # ===== Contract Connectors ===== def connect_proxy_admins(self, devProxyAdmin, daoProxyAdmin, opsProxyAdmin=None): abi = registry.open_zeppelin.artifacts["ProxyAdmin"]["abi"] @@ -355,7 +356,7 @@ def add_controller(self, id): deployer = self.deployer controller = deploy_controller(self, deployer) self.sett_system.controllers[id] = controller - self.track_contract_upgradeable(id + ".controller", controller) + self.upgrade.track_contract_upgradeable(f"{id}{CONTROLLER_SUFFIX}", controller) return controller def deploy_core_logic(self): @@ -387,8 +388,10 @@ def deploy_sett_strategy_logic(self): def deploy_sett_strategy_logic_for(self, name): deployer = self.deployer - artifact = strategy_name_to_artifact(name) - self.logic[name] = artifact.deploy({"from": deployer}) + artifact = contract_name_to_artifact(name) + self.logic[name] = artifact.deploy( + {"from": deployer} + ) # TODO: Initialize to remove that function @@ -406,7 +409,7 @@ def deploy_rewards_escrow(self): self.logic.RewardsEscrow.initialize.encode_input(), deployer, ) - self.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) + self.upgrade.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) def deploy_badger_tree(self): deployer = self.deployer @@ -427,7 +430,7 @@ def deploy_badger_tree(self): ), deployer, ) - self.track_contract_upgradeable("badgerTree", self.badgerTree) + self.upgrade.track_contract_upgradeable("badgerTree", self.badgerTree) def deploy_badger_hunt(self): deployer = self.deployer @@ -448,7 +451,7 @@ def deploy_badger_hunt(self): ), deployer, ) - self.track_contract_upgradeable("badgerHunt", self.badgerHunt) + self.upgrade.track_contract_upgradeable("badgerHunt", self.badgerHunt) def deploy_dao_badger_timelock(self): deployer = self.deployer @@ -476,7 +479,7 @@ def deploy_dao_badger_timelock(self): ), self.deployer, ) - self.track_contract_upgradeable("daoBadgerTimelock", self.daoBadgerTimelock) + self.upgrade.track_contract_upgradeable("daoBadgerTimelock", self.daoBadgerTimelock) def deploy_dao_digg_timelock(self): deployer = self.deployer @@ -499,7 +502,7 @@ def deploy_team_vesting(self): ), self.deployer, ) - self.track_contract_upgradeable("teamVesting", self.teamVesting) + self.upgrade.track_contract_upgradeable("teamVesting", self.teamVesting) def deploy_logic(self, name, BrownieArtifact, test=False): deployer = self.deployer @@ -574,7 +577,7 @@ def deploy_sett( deployer, ) self.sett_system.vaults[id] = sett - self.track_contract_upgradeable(id + ".sett", sett) + self.upgrade.track_contract_upgradeable(f"{id}{SETT_SUFFIX}", sett) return sett def deploy_strategy( @@ -602,19 +605,18 @@ def deploy_strategy( guardian, ) - Artifact = strategy_name_to_artifact(strategyName) + Artifact = contract_name_to_artifact(strategyName) self.sett_system.strategies[id] = strategy self.set_strategy_artifact(id, strategyName, Artifact) - self.track_contract_upgradeable(id + ".strategy", strategy) + self.upgrade.track_contract_upgradeable(f"{id}{STRATEGY_SUFFIX}", strategy) return strategy def deploy_geyser(self, stakingToken, id): print(stakingToken) - deployer = self.deployer geyser = deploy_geyser(self, stakingToken) self.geysers[id] = geyser - self.track_contract_upgradeable(id + ".geyser", geyser) + self.upgrade.track_contract_upgradeable(f"{id}{GEYSER_SUFFIX}", geyser) return geyser def add_existing_digg(self, digg_system: DiggSystem): @@ -633,7 +635,7 @@ def deploy_digg_rewards_faucet(self, id, diggToken): ) self.sett_system.rewards[id] = rewards - self.track_contract_upgradeable(id + ".rewards", rewards) + self.upgrade.track_contract_upgradeable(id + ".rewards", rewards) return rewards def deploy_sett_staking_rewards_signal_only(self, id, admin, distToken): @@ -651,7 +653,7 @@ def deploy_sett_staking_rewards_signal_only(self, id, admin, distToken): ) self.sett_system.rewards[id] = rewards - self.track_contract_upgradeable(id + ".rewards", rewards) + self.upgrade.track_contract_upgradeable(id + ".rewards", rewards) return rewards def deploy_sett_staking_rewards(self, id, stakingToken, distToken): @@ -669,7 +671,7 @@ def deploy_sett_staking_rewards(self, id, stakingToken, distToken): ) self.sett_system.rewards[id] = rewards - self.track_contract_upgradeable(id + ".rewards", rewards) + self.upgrade.track_contract_upgradeable(id + ".rewards", rewards) return rewards def add_existing_claw(self, claw_system: ClawSystem): @@ -897,66 +899,66 @@ def connect_sett_system(self, sett_system, geysers): self.connect_geyser(key, address) def connect_strategy(self, id, address, strategyArtifactName): - Artifact = strategy_name_to_artifact(strategyArtifactName) + Artifact = contract_name_to_artifact(strategyArtifactName) strategy = Artifact.at(address) self.sett_system.strategies[id] = strategy self.set_strategy_artifact(id, strategyArtifactName, Artifact) - self.track_contract_upgradeable(id + ".strategy", strategy) + self.upgrade.track_contract_upgradeable(f"{id}{STRATEGY_SUFFIX}", strategy) def connect_sett(self, id, address): sett = Sett.at(address) print(f"connecting sett id {id}") self.sett_system.vaults[id] = sett - self.track_contract_upgradeable(id + ".sett", sett) + self.upgrade.track_contract_upgradeable(f"{id}{SETT_SUFFIX}", sett) def connect_controller(self, id, address): controller = Controller.at(address) self.sett_system.controllers[id] = controller - self.track_contract_upgradeable(id + ".controller", controller) + self.upgrade.track_contract_upgradeable(f"{id}{CONTROLLER_SUFFIX}", controller) def connect_geyser(self, id, address): geyser = BadgerGeyser.at(address) self.geysers[id] = geyser - self.track_contract_upgradeable(id + ".geyser", geyser) + self.upgrade.track_contract_upgradeable(f"{id}{GEYSER_SUFFIX}", geyser) def connect_rewards_escrow(self, address): self.rewardsEscrow = RewardsEscrow.at(address) - self.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) + self.upgrade.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) def connect_badger_tree(self, address): self.badgerTree = BadgerTree.at(address) - self.track_contract_upgradeable("badgerTree", self.badgerTree) + self.upgrade.track_contract_upgradeable("badgerTree", self.badgerTree) def connect_badger_hunt(self, address): self.badgerHunt = BadgerHunt.at(address) - self.track_contract_upgradeable("badgerHunt", self.badgerHunt) + self.upgrade.track_contract_upgradeable("badgerHunt", self.badgerHunt) def connect_honeypot_meme(self, address): self.honeypotMeme = HoneypotMeme.at(address) - self.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) + self.upgrade.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) def connect_community_pool(self, address): self.communityPool = RewardsEscrow.at(address) - self.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) + self.upgrade.track_contract_upgradeable("rewardsEscrow", self.rewardsEscrow) def connect_logic(self, logic): for name, address in logic.items(): - Artifact = strategy_name_to_artifact(name) + Artifact = contract_name_to_artifact(name) self.logic[name] = Artifact.at(address) def connect_dao_badger_timelock(self, address): self.daoBadgerTimelock = SimpleTimelock.at(address) - self.track_contract_upgradeable("daoBadgerTimelock", self.daoBadgerTimelock) + self.upgrade.track_contract_upgradeable("daoBadgerTimelock", self.daoBadgerTimelock) def connect_rewards_manager(self, address): self.badgerRewardsManager = BadgerRewardsManager.at(address) - self.track_contract_upgradeable( + self.upgrade.track_contract_upgradeable( "badgerRewardsManager", self.badgerRewardsManager ) def connect_unlock_scheduler(self, address): self.unlockScheduler = UnlockScheduler.at(address) - self.track_contract_upgradeable("unlockScheduler", self.unlockScheduler) + self.upgrade.track_contract_upgradeable("unlockScheduler", self.unlockScheduler) def connect_dao_digg_timelock(self, address): # TODO: Implement with Digg @@ -964,7 +966,7 @@ def connect_dao_digg_timelock(self, address): def connect_team_vesting(self, address): self.teamVesting = SmartVesting.at(address) - self.track_contract_upgradeable("teamVesting", self.teamVesting) + self.upgrade.track_contract_upgradeable("teamVesting", self.teamVesting) # Routes rewards connection to correct underlying contract based on id. def connect_rewards(self, id, address): @@ -980,7 +982,7 @@ def connect_rewards(self, id, address): def connect_sett_staking_rewards(self, id, address): rewards = StakingRewards.at(address) self.sett_system.rewards[id] = rewards - self.track_contract_upgradeable(id + ".rewards", rewards) + self.upgrade.track_contract_upgradeable(id + ".rewards", rewards) # def connect_guardian(self, address): # self.guardian = accounts.at(address) @@ -1000,7 +1002,7 @@ def connect_uni_badger_wbtc_lp(self, address): def connect_digg_rewards_faucet(self, id, address): rewards = DiggRewardsFaucet.at(address) self.sett_system.rewards[id] = rewards - self.track_contract_upgradeable(id + ".rewards", rewards) + self.upgrade.track_contract_upgradeable(id + ".rewards", rewards) def set_strategy_artifact(self, id, artifactName, artifact): self.strategy_artifacts[id] = { @@ -1048,6 +1050,11 @@ def getSett(self, id): return self.sett_system.vaults[id] + def getSettArtifact(self, id): + if id == "native.digg": + return DiggSett + return Sett + def getSettRewards(self, id): return self.sett_system.rewards[id] @@ -1062,7 +1069,7 @@ def getStrategyWant(self, id): return interface.IERC20(self.sett_system.strategies[id].want()) def getStrategyArtifact(self, id): - return self.strategy_artifacts[id].artifact + return self.strategy_artifacts[id]["artifact"] def getStrategyArtifactName(self, id): return self.strategy_artifacts[id]["artifactName"] diff --git a/scripts/systems/constants.py b/scripts/systems/constants.py index 62bc630b..659ea4d3 100644 --- a/scripts/systems/constants.py +++ b/scripts/systems/constants.py @@ -5,3 +5,9 @@ class SettType(Enum): DEFAULT = 1 DIGG = 2 + + +SETT_SUFFIX = ".sett" +STRATEGY_SUFFIX = ".strategy" +GEYSER_SUFFIX = ".geyser" +CONTROLLER_SUFFIX = ".controller" diff --git a/scripts/systems/digg_system.py b/scripts/systems/digg_system.py index a22d4057..66aff79e 100644 --- a/scripts/systems/digg_system.py +++ b/scripts/systems/digg_system.py @@ -1,4 +1,4 @@ -from helpers.sett.strategy_registry import strategy_name_to_artifact +from helpers.contract_registry import contract_name_to_artifact import json from brownie import * from dotmap import DotMap @@ -197,7 +197,7 @@ def connect_logic(self, logic): for name, address in logic.items(): if env_config.debug: print("ConnectLogic:", name, address) - Artifact = strategy_name_to_artifact(name) + Artifact = contract_name_to_artifact(name) self.logic[name] = Artifact.at(address) # ===== Deployers ===== diff --git a/scripts/systems/upgrade_system.py b/scripts/systems/upgrade_system.py new file mode 100644 index 00000000..61c48c85 --- /dev/null +++ b/scripts/systems/upgrade_system.py @@ -0,0 +1,59 @@ +from brownie import BrownieArtifact +from typing import Optional + +from scripts.deploy.upgrade import upgrade_versioned_proxy +from .constants import ( + SETT_SUFFIX, + STRATEGY_SUFFIX, +) + + +class UpgradeSystem: + def __init__(self, badger, deployer): + self.badger = badger + self.deployer = deployer + # NB: Currently the only versioned contracts are strategy/sett contracts. + self.contracts_upgradeable = {} + + def upgrade_sett_contract(self, contractKey: str) -> None: + self._upgrade_with_suffix( + SETT_SUFFIX, + self.badger.getSettArtifact, + contractKey=contractKey, + ) + + def upgrade_strategy_contract(self, contractKey: str) -> None: + self._upgrade_with_suffix( + STRATEGY_SUFFIX, + self.badger.getStrategyArtifact, + contractKey=contractKey, + ) + + def upgrade_sett_contracts(self) -> None: + self._upgrade_with_suffix(SETT_SUFFIX, self.badger.getSettArtifact) + + def upgrade_strategy_contracts(self) -> None: + self._upgrade_with_suffix(STRATEGY_SUFFIX, self.badger.getStrategyArtifact) + + def _upgrade_contract( + self, + suffix: str, + getArtifactFn: lambda str: BrownieArtifact, + contractKey: Optional[str] = None, + ) -> None: + for key, contract in self.contracts_upgradeable.items(): + # If a contract key is specified, only attempt to upgrade that contract. + if contractKey is not None and contractKey != key: + continue + + if key.endswith(suffix): + Artifact = getArtifactFn(key.removesuffix(suffix)) + latest = Artifact.deploy({"from": self.deployer}) + upgrade_versioned_proxy( + self.badger, + contract, + latest, + ) + + def track_contract_upgradeable(self, key, contract) -> None: + self.contracts_upgradeable[key] = contract From ccba0804f0233b094f9b4fee4f6cc615a2d41f79 Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 01:44:00 -0500 Subject: [PATCH 02/12] chore: remove comments --- contracts/badger-sett/sett-forks/README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/contracts/badger-sett/sett-forks/README.md b/contracts/badger-sett/sett-forks/README.md index ce114d08..4facc0eb 100644 --- a/contracts/badger-sett/sett-forks/README.md +++ b/contracts/badger-sett/sett-forks/README.md @@ -19,10 +19,9 @@ V1.3 * Withdrawals are processed from idle want in sett. -Looking at it, all the Setts inherit from one of these (except the DIGG sett): "SettV1": "0xE4Ae305b08434bF3D74e0086592627F913a258A9", "SettV1.1": "0x175586ac3f8A7463499D1019A30120aa6fC67C5f", -So + Old: "native.badger": "0x19D97D8fA813EE2f51aD4B4e04EA08bAf4DFfC28", "native.renCrv": "0x6dEf55d2e18486B9dDfaA075bc4e4EE0B28c1545", @@ -36,7 +35,3 @@ New: "native.digg": "0x7e7E112A68d8D2E221E11047a72fFC1065c38e1a", "native.uniDiggWbtc": "0xC17078FDd324CC473F8175Dc5290fae5f2E84714", "native.sushiDiggWbtc": "0x88128580ACdD9c04Ce47AFcE196875747bF2A9f6" -Basically pausable vs not pausable -DiggSett extends from the new variety -Which makes the in-place migration a lot more palatable -LegacySett / Sett From fde2bf73d881f970575782895feaa063a9e997a9 Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 01:50:53 -0500 Subject: [PATCH 03/12] chore: more version to base contract --- contracts/badger-sett/sett-forks/SettVersion.sol | 10 ++++++++++ contracts/badger-sett/sett-forks/Sett_A.sol | 8 +++----- contracts/badger-sett/sett-forks/Sett_B.sol | 11 ++++++----- contracts/badger-sett/sett-forks/Sett_C.sol | 11 ++++++----- 4 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 contracts/badger-sett/sett-forks/SettVersion.sol diff --git a/contracts/badger-sett/sett-forks/SettVersion.sol b/contracts/badger-sett/sett-forks/SettVersion.sol new file mode 100644 index 00000000..705eb04f --- /dev/null +++ b/contracts/badger-sett/sett-forks/SettVersion.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.11; + +// Track underlying version of all Sett forks in one place. +contract SettVersion { + function version() public view returns (string memory) { + return "1.3"; + } +} diff --git a/contracts/badger-sett/sett-forks/Sett_A.sol b/contracts/badger-sett/sett-forks/Sett_A.sol index 6a51b5dd..a9b4ab10 100644 --- a/contracts/badger-sett/sett-forks/Sett_A.sol +++ b/contracts/badger-sett/sett-forks/Sett_A.sol @@ -15,11 +15,13 @@ import "../../interfaces/erc20/IERC20Detailed.sol"; import "../badger-remote/PauseableStorageless.sol"; import "../badger-remote/DefenderStorageless.sol"; import "../SettAccessControlDefended.sol"; +import "./SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, DefenderStorageless { +contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, + DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -102,10 +104,6 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage /// ===== View Functions ===== - function version() public view returns (string memory) { - return "1.3"; - } - function getPricePerFullShare() public virtual view returns (uint256) { if (totalSupply() == 0) { return 1e18; diff --git a/contracts/badger-sett/sett-forks/Sett_B.sol b/contracts/badger-sett/sett-forks/Sett_B.sol index a8a40678..0b8e641a 100644 --- a/contracts/badger-sett/sett-forks/Sett_B.sol +++ b/contracts/badger-sett/sett-forks/Sett_B.sol @@ -14,8 +14,13 @@ import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; import "../badger-remote/DefenderStorageless.sol"; import "../SettAccessControlDefended.sol"; +import "./SettVersion.sol"; -contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless { +/* + Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol +*/ +contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, + DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -93,10 +98,6 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// ===== View Functions ===== - function version() public view returns (string memory) { - return "1.3"; - } - function getPricePerFullShare() public virtual view returns (uint256) { if (totalSupply() == 0) { return 1e18; diff --git a/contracts/badger-sett/sett-forks/Sett_C.sol b/contracts/badger-sett/sett-forks/Sett_C.sol index d5a5b711..fbd7c88d 100644 --- a/contracts/badger-sett/sett-forks/Sett_C.sol +++ b/contracts/badger-sett/sett-forks/Sett_C.sol @@ -14,8 +14,13 @@ import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; import "../badger-remote/DefenderStorageless.sol"; import "../SettAccessControlDefended.sol"; +import "./SettVersion.sol"; -contract Sett_C is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless { +/* + Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol +*/ +contract Sett_C is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, + DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -94,10 +99,6 @@ contract Sett_C is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// ===== View Functions ===== - function version() public view returns (string memory) { - return "1.3"; - } - function getPricePerFullShare() public virtual view returns (uint256) { if (totalSupply() == 0) { return 1e18; From 8b50a8e29bab204584036fc8d0dd3176c323b959 Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 18:44:13 -0500 Subject: [PATCH 04/12] feat: finish impl of naive forked upgrader/validator --- contracts/badger-sett/DiggSett.sol | 4 +- contracts/badger-sett/Sett.sol | 299 -------------------- contracts/badger-sett/sett-forks/README.md | 21 -- contracts/badger-sett/sett-forks/Sett_C.sol | 3 +- scripts/deploy/upgrade.py | 18 +- scripts/systems/upgrade_system.py | 127 ++++++++- 6 files changed, 132 insertions(+), 340 deletions(-) delete mode 100644 contracts/badger-sett/Sett.sol diff --git a/contracts/badger-sett/DiggSett.sol b/contracts/badger-sett/DiggSett.sol index c62a2951..f36df4e2 100644 --- a/contracts/badger-sett/DiggSett.sol +++ b/contracts/badger-sett/DiggSett.sol @@ -11,7 +11,7 @@ import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradea import "interfaces/badger/IController.sol"; import "interfaces/digg/IDigg.sol"; import "interfaces/digg/IDiggStrategy.sol"; -import "./Sett.sol"; +import "../Sett_C.sol"; /* bDIGG is denominated in scaledShares. @@ -32,7 +32,7 @@ import "./Sett.sol"; * Transfer functions are now pausable along with all other non-permissioned write functions * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well */ -contract DiggSett is Sett { +contract DiggSett is Sett_C { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; diff --git a/contracts/badger-sett/Sett.sol b/contracts/badger-sett/Sett.sol deleted file mode 100644 index 21de1001..00000000 --- a/contracts/badger-sett/Sett.sol +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.6.11; - -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; - -import "../../interfaces/badger/IController.sol"; -import "../../interfaces/erc20/IERC20Detailed.sol"; -import "./SettAccessControlDefended.sol"; - -/* - Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol - - Changelog: - - V1.1 - * Strategist no longer has special function calling permissions - * Version function added to contract - * All write functions, with the exception of transfer, are pausable - * Keeper or governance can pause - * Only governance can unpause - - V1.2 - * Transfer functions are now pausable along with all other non-permissioned write functions - * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well -*/ - -contract Sett is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable { - using SafeERC20Upgradeable for IERC20Upgradeable; - using AddressUpgradeable for address; - using SafeMathUpgradeable for uint256; - - IERC20Upgradeable public token; - - uint256 public min; - uint256 public constant max = 10000; - - address public controller; - - mapping(address => uint256) public blockLock; - - string internal constant _defaultNamePrefix = "Badger Sett "; - string internal constant _symbolSymbolPrefix = "b"; - - address public guardian; - - event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); - - function initialize( - address _token, - address _controller, - address _governance, - address _keeper, - address _guardian, - bool _overrideTokenName, - string memory _namePrefix, - string memory _symbolPrefix - ) public initializer whenNotPaused { - IERC20Detailed namedToken = IERC20Detailed(_token); - string memory tokenName = namedToken.name(); - string memory tokenSymbol = namedToken.symbol(); - - string memory name; - string memory symbol; - - if (_overrideTokenName) { - name = string(abi.encodePacked(_namePrefix, tokenName)); - symbol = string(abi.encodePacked(_symbolPrefix, tokenSymbol)); - } else { - name = string(abi.encodePacked(_defaultNamePrefix, tokenName)); - symbol = string(abi.encodePacked(_symbolSymbolPrefix, tokenSymbol)); - } - - __ERC20_init(name, symbol); - - token = IERC20Upgradeable(_token); - governance = _governance; - strategist = address(0); - keeper = _keeper; - controller = _controller; - guardian = _guardian; - - min = 9500; - - emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); - - // Paused on launch - _pause(); - } - - /// ===== Modifiers ===== - - function _onlyController() internal view { - require(msg.sender == controller, "onlyController"); - } - - function _onlyAuthorizedPausers() internal view { - require(msg.sender == guardian || msg.sender == governance, "onlyPausers"); - } - - function _blockLocked() internal view { - require(blockLock[msg.sender] < block.number, "blockLocked"); - } - - /// ===== View Functions ===== - - function version() public view returns (string memory) { - return "1.2"; - } - - function getPricePerFullShare() public view virtual returns (uint256) { - if (totalSupply() == 0) { - return 1e18; - } - return balance().mul(1e18).div(totalSupply()); - } - - /// @notice Return the total balance of the underlying token within the system - /// @notice Sums the balance in the Sett, the Controller, and the Strategy - function balance() public view returns (uint256) { - return token.balanceOf(address(this)).add(IController(controller).balanceOf(address(token))); - } - - /// @notice Defines how much of the Setts' underlying can be borrowed by the Strategy for use - /// @notice Custom logic in here for how much the vault allows to be borrowed - /// @notice Sets minimum required on-hand to keep small withdrawals cheap - function available() public view virtual returns (uint256) { - return token.balanceOf(address(this)).mul(min).div(max); - } - - /// ===== Public Actions ===== - - /// @notice Deposit assets into the Sett, and return corresponding shares to the user - /// @notice Only callable by EOA accounts that pass the _defend() check - function deposit(uint256 _amount) public whenNotPaused { - _defend(); - _blockLocked(); - - _lockForBlock(msg.sender); - _deposit(_amount); - } - - /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user - /// @notice Only callable by EOA accounts that pass the _defend() check - function depositAll() external whenNotPaused { - _defend(); - _blockLocked(); - - _lockForBlock(msg.sender); - _deposit(token.balanceOf(msg.sender)); - } - - /// @notice No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) public whenNotPaused { - _defend(); - _blockLocked(); - - _lockForBlock(msg.sender); - _withdraw(_shares); - } - - /// @notice Convenience function: Withdraw all shares of the sender - function withdrawAll() external whenNotPaused { - _defend(); - _blockLocked(); - - _lockForBlock(msg.sender); - _withdraw(balanceOf(msg.sender)); - } - - /// ===== Permissioned Actions: Governance ===== - - /// @notice Set minimum threshold of underlying that must be deposited in strategy - /// @notice Can only be changed by governance - function setMin(uint256 _min) external whenNotPaused { - _onlyGovernance(); - min = _min; - } - - /// @notice Change controller address - /// @notice Can only be changed by governance - function setController(address _controller) public whenNotPaused { - _onlyGovernance(); - controller = _controller; - } - - /// @notice Change guardian address - /// @notice Can only be changed by governance - function setGuardian(address _guardian) external whenNotPaused { - _onlyGovernance(); - guardian = _guardian; - } - - /// ===== Permissioned Actions: Controller ===== - - /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' - /// @notice Only controller can trigger harvests - function harvest(address reserve, uint256 amount) external whenNotPaused { - _onlyController(); - require(reserve != address(token), "token"); - IERC20Upgradeable(reserve).safeTransfer(controller, amount); - } - - /// ===== Permissioned Functions: Trusted Actors ===== - - /// @notice Transfer the underlying available to be claimed to the controller - /// @notice The controller will deposit into the Strategy for yield-generating activities - /// @notice Permissionless operation - function earn() public whenNotPaused { - _onlyAuthorizedActors(); - - uint256 _bal = available(); - token.safeTransfer(controller, _bal); - IController(controller).earn(address(token), _bal); - } - - /// @dev Emit event tracking current full price per share - /// @dev Provides a pure on-chain way of approximating APY - function trackFullPricePerShare() external whenNotPaused { - _onlyAuthorizedActors(); - emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); - } - - function pause() external { - _onlyAuthorizedPausers(); - _pause(); - } - - function unpause() external { - _onlyGovernance(); - _unpause(); - } - - /// ===== Internal Implementations ===== - - /// @dev Calculate the number of shares to issue for a given deposit - /// @dev This is based on the realized value of underlying assets between Sett & associated Strategy - function _deposit(uint256 _amount) internal virtual { - uint256 _pool = balance(); - uint256 _before = token.balanceOf(address(this)); - token.safeTransferFrom(msg.sender, address(this), _amount); - uint256 _after = token.balanceOf(address(this)); - _amount = _after.sub(_before); // Additional check for deflationary tokens - uint256 shares = 0; - if (totalSupply() == 0) { - shares = _amount; - } else { - shares = (_amount.mul(totalSupply())).div(_pool); - } - _mint(msg.sender, shares); - } - - // No rebalance implementation for lower fees and faster swaps - function _withdraw(uint256 _shares) internal virtual { - uint256 r = (balance().mul(_shares)).div(totalSupply()); - _burn(msg.sender, _shares); - - // Check balance - uint256 b = token.balanceOf(address(this)); - if (b < r) { - uint256 _toWithdraw = r.sub(b); - IController(controller).withdraw(address(token), _toWithdraw); - uint256 _after = token.balanceOf(address(this)); - uint256 _diff = _after.sub(b); - if (_diff < _toWithdraw) { - r = b.add(_diff); - } - } - - token.safeTransfer(msg.sender, r); - } - - function _lockForBlock(address account) internal { - blockLock[account] = block.number; - } - - /// ===== ERC20 Overrides ===== - - /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. - function transfer(address recipient, uint256 amount) public virtual override whenNotPaused returns (bool) { - _blockLocked(); - return super.transfer(recipient, amount); - } - - function transferFrom( - address sender, - address recipient, - uint256 amount - ) public virtual override whenNotPaused returns (bool) { - _blockLocked(); - return super.transferFrom(sender, recipient, amount); - } -} diff --git a/contracts/badger-sett/sett-forks/README.md b/contracts/badger-sett/sett-forks/README.md index 4facc0eb..a9b588fc 100644 --- a/contracts/badger-sett/sett-forks/README.md +++ b/contracts/badger-sett/sett-forks/README.md @@ -14,24 +14,3 @@ V1.1 V1.2 * Transfer functions are now pausable along with all other non-permissioned write functions * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well - -V1.3 -* Withdrawals are processed from idle want in sett. - - -"SettV1": "0xE4Ae305b08434bF3D74e0086592627F913a258A9", -"SettV1.1": "0x175586ac3f8A7463499D1019A30120aa6fC67C5f", - -Old: - "native.badger": "0x19D97D8fA813EE2f51aD4B4e04EA08bAf4DFfC28", - "native.renCrv": "0x6dEf55d2e18486B9dDfaA075bc4e4EE0B28c1545", - "native.sbtcCrv": "0xd04c48A53c111300aD41190D63681ed3dAd998eC", - "native.tbtcCrv": "0xb9D076fDe463dbc9f915E5392F807315Bf940334", - "native.uniBadgerWbtc": "0x235c9e24D3FB2FAFd58a2E49D454Fdcd2DBf7FF1", - "harvest.renCrv": "0xAf5A1DECfa95BAF63E0084a35c62592B774A2A87", -New: - "native.sushiWbtcEth": "0x758A43EE2BFf8230eeb784879CdcFF4828F2544D", - "native.sushiBadgerWbtc": "0x1862A18181346EBd9EdAf800804f89190DeF24a5", - "native.digg": "0x7e7E112A68d8D2E221E11047a72fFC1065c38e1a", - "native.uniDiggWbtc": "0xC17078FDd324CC473F8175Dc5290fae5f2E84714", - "native.sushiDiggWbtc": "0x88128580ACdD9c04Ce47AFcE196875747bF2A9f6" diff --git a/contracts/badger-sett/sett-forks/Sett_C.sol b/contracts/badger-sett/sett-forks/Sett_C.sol index fbd7c88d..756d8ae1 100644 --- a/contracts/badger-sett/sett-forks/Sett_C.sol +++ b/contracts/badger-sett/sett-forks/Sett_C.sol @@ -18,8 +18,9 @@ import "./SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol + NB: Only Digg sett inherits from/uses this fork. */ -contract Sett_C is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, +contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; diff --git a/scripts/deploy/upgrade.py b/scripts/deploy/upgrade.py index 01817252..37c1d6e6 100644 --- a/scripts/deploy/upgrade.py +++ b/scripts/deploy/upgrade.py @@ -4,7 +4,15 @@ from helpers.gnosis_safe import GnosisSafe, MultisigTxMetadata -# Upgrades versioned proxy contract if not latest version. +# try all the methods in priority order +VERSION_METHODS = [ + "version", + "baseStrategyVersion", +] + + +# Upgrades versioned proxy contract's impl/logic contract if not latest version. +# Contracts must implement a version method above or they default to version 0.0. def upgrade_versioned_proxy( badger: BadgerSystem, # upgradeable proxy contract @@ -36,13 +44,7 @@ def upgrade_versioned_proxy( def _get_version(contract: ProjectContract) -> float: - # try all the methods in priority order - methods = [ - "version", - "baseStrategyVersion", - ] - - for method in methods: + for method in VERSION_METHODS: version, ok = _try_get_version(contract, method) if ok: return version diff --git a/scripts/systems/upgrade_system.py b/scripts/systems/upgrade_system.py index 61c48c85..41552a6a 100644 --- a/scripts/systems/upgrade_system.py +++ b/scripts/systems/upgrade_system.py @@ -1,5 +1,11 @@ -from brownie import BrownieArtifact -from typing import Optional +from brownie import ( + ProjectContract, + DiggSett, + Sett_A, + Sett_B, +) +from typing import Optional, List, Tuple, Any +from rich.console import Console from scripts.deploy.upgrade import upgrade_versioned_proxy from .constants import ( @@ -7,6 +13,58 @@ STRATEGY_SUFFIX, ) +console = Console() + +SETT_VALIDATION_FIELDS = [ + "token", + "min", + "max", + "controller", + "guardian", +] + +# NB: These validation fields are generic to all strategies and will not cover all state vars +# in all strategies as some may inherit from other base contracts (e.g. BaseStrategyMultiSwapper). +# However, checking these should give us some base level of confidence in the upgrade. +STRATEGY_VALIDATION_FIELDS = [ + "want", + "performanceFeeGovernance", + "performanceFeeStrategist", + "withdrawalFee", + "MAX_FEE", + "uniswap", + "controller", + "guardian", +] + +# Track forked sett upgrade paths. +SETT_A = ( + "native.badger", + "native.renCrv", + "native.sbtcCrv", + "native.tbtcCrv", + "native.uniBadgerWbtc", + "harvest.renCrv", +) + +SETT_B = ( + "native.sushiWbtcEth", + "native.sushiBadgerWbtc", + "native.uniDiggWbtc", + "native.sushiDiggWbtc", +) + +# NB: Digg sett is the only sett that inherits from Sett_C +DIGG_SETT = ( + "native.digg", +) + +SETT_TO_ARTIFACT_MAP = { + SETT_A: Sett_A, + SETT_B: Sett_B, + DIGG_SETT: DiggSett, +} + class UpgradeSystem: def __init__(self, badger, deployer): @@ -14,33 +72,41 @@ def __init__(self, badger, deployer): self.deployer = deployer # NB: Currently the only versioned contracts are strategy/sett contracts. self.contracts_upgradeable = {} + # NB: Must define a validator for all support contract suffixes/types. + self.validators = { + SETT_SUFFIX: UpgradeValidator(SETT_VALIDATION_FIELDS), + STRATEGY_SUFFIX: UpgradeValidator(STRATEGY_VALIDATION_FIELDS), + } - def upgrade_sett_contract(self, contractKey: str) -> None: + def upgrade_sett_contract(self, contractKey: str, validate: bool = False) -> None: self._upgrade_with_suffix( SETT_SUFFIX, self.badger.getSettArtifact, contractKey=contractKey, ) - def upgrade_strategy_contract(self, contractKey: str) -> None: + def upgrade_strategy_contract(self, contractKey: str, validate: bool = False) -> None: self._upgrade_with_suffix( STRATEGY_SUFFIX, self.badger.getStrategyArtifact, contractKey=contractKey, ) - def upgrade_sett_contracts(self) -> None: - self._upgrade_with_suffix(SETT_SUFFIX, self.badger.getSettArtifact) + def upgrade_sett_contracts(self, validate: bool = False) -> None: + self._upgrade_with_suffix(SETT_SUFFIX, self.getSettArtifact) - def upgrade_strategy_contracts(self) -> None: + def upgrade_strategy_contracts(self, validate: bool = False) -> None: self._upgrade_with_suffix(STRATEGY_SUFFIX, self.badger.getStrategyArtifact) def _upgrade_contract( self, suffix: str, - getArtifactFn: lambda str: BrownieArtifact, + getArtifactFn: lambda str: ProjectContract, contractKey: Optional[str] = None, + validate: bool = False, ) -> None: + # Must have a validator defined for a contract suffix/type. + validator = self.validators[suffix] for key, contract in self.contracts_upgradeable.items(): # If a contract key is specified, only attempt to upgrade that contract. if contractKey is not None and contractKey != key: @@ -49,11 +115,54 @@ def _upgrade_contract( if key.endswith(suffix): Artifact = getArtifactFn(key.removesuffix(suffix)) latest = Artifact.deploy({"from": self.deployer}) + validator.snapshot(contract) upgrade_versioned_proxy( self.badger, contract, latest, ) + validator.snapshot(contract) + + if not validator.validate(): + console.print("[red]=== Failed to validate upgrade non matching storage vars: {}[/red]".format(validator.snapshots)) - def track_contract_upgradeable(self, key, contract) -> None: + def track_contract_upgradeable(self, key: str, contract: ProjectContract) -> None: self.contracts_upgradeable[key] = contract + + def getSettArtifact(self, key: str) -> ProjectContract: + for (keys, BrownieArtifact) in SETT_TO_ARTIFACT_MAP.items(): + if key in keys: + return BrownieArtifact + raise Exception("{} not found in SETT_TO_ARTIFACT_MAP".format(key)) + + +class UpgradeValidator: + ''' + UpgradeValidator validates a sequence of snapshots taken of a contract's storage vars + during the process of an upgrade. Normally, this is just before/after to ensure that + the storage vars have not been affected by the upgrade. This may be extended in the future + to perform more sophisticated validation. + + NB: OZ takes a more sophisticated approach to solve this problem (this is a js plugin) by crawling + the AST and deriving the storage layout. We take the following more simplistic user defined varialble + checking approach due to time constraints. Could be a future TODO project to port over the AST based approach + and contribute back to brownie. + See: https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/master/packages/core/src/validate/query.ts#L52 + ''' + def __init__(self, fields: List[str]): + self.fields = fields + self.snapshots: List[Tuple[Any]] = [] + + def snapshot(self, contract: ProjectContract) -> None: + snapshot = () + for field in self.fields: + snapshot += getattr(contract, field)() + self.snapshots.append(snapshot) + + def validate(self) -> bool: + prev = self.snapshots[0] + for snapshot in self.snapshots[1:]: + if snapshot != prev: + return False + prev = snapshot + return True From 98be5fb09b8d81b79f57dcd15bc917043c310eda Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 19:44:07 -0500 Subject: [PATCH 05/12] chore: v1.2 --- contracts/badger-sett/sett-forks/SettVersion.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/badger-sett/sett-forks/SettVersion.sol b/contracts/badger-sett/sett-forks/SettVersion.sol index 705eb04f..a6e16b87 100644 --- a/contracts/badger-sett/sett-forks/SettVersion.sol +++ b/contracts/badger-sett/sett-forks/SettVersion.sol @@ -5,6 +5,6 @@ pragma solidity ^0.6.11; // Track underlying version of all Sett forks in one place. contract SettVersion { function version() public view returns (string memory) { - return "1.3"; + return "1.2"; } } From 8d40ea8e2abe9805c1d5753cdea6e2cd6e055575 Mon Sep 17 00:00:00 2001 From: shake Date: Sat, 27 Feb 2021 19:59:52 -0500 Subject: [PATCH 06/12] chore: cleanup setts --- contracts/badger-sett/DiggSett.sol | 6 +----- contracts/badger-sett/sett-forks/Sett_A.sol | 21 +++++++-------------- contracts/badger-sett/sett-forks/Sett_B.sol | 15 +++++++-------- contracts/badger-sett/sett-forks/Sett_C.sol | 21 +++++++-------------- 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/contracts/badger-sett/DiggSett.sol b/contracts/badger-sett/DiggSett.sol index f36df4e2..5e9d77b2 100644 --- a/contracts/badger-sett/DiggSett.sol +++ b/contracts/badger-sett/DiggSett.sol @@ -94,7 +94,6 @@ contract DiggSett is Sett_C { // Check balance uint256 _sharesInSett = digg.sharesOf(address(this)); - uint256 _fee; // If we don't have sufficient idle want in Sett, withdraw from Strategy if (_sharesInSett < _sharesToRedeem) { @@ -110,12 +109,9 @@ contract DiggSett is Sett_C { if (_diff < _toWithdraw) { _sharesToRedeem = _sharesInSett.add(_diff); } - _fee = _processWithdrawalFee(digg.sharesToFragments(_sharesToRedeem.sub(_sharesInSett))); - } else { - _fee = _processWithdrawalFee(digg.sharesToFragments(_sharesToRedeem)); } // Transfer the corresponding number of shares, scaled to DIGG fragments, to recipient - digg.transfer(msg.sender, digg.sharesToFragments(_sharesToRedeem).sub(_fee)); + digg.transfer(msg.sender, digg.sharesToFragments(_sharesToRedeem)); } } diff --git a/contracts/badger-sett/sett-forks/Sett_A.sol b/contracts/badger-sett/sett-forks/Sett_A.sol index a9b4ab10..00cc0f4f 100644 --- a/contracts/badger-sett/sett-forks/Sett_A.sol +++ b/contracts/badger-sett/sett-forks/Sett_A.sol @@ -3,12 +3,12 @@ pragma solidity ^0.6.11; import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; @@ -20,8 +20,7 @@ import "./SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, - DefenderStorageless, SettVersion { +contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -183,12 +182,6 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage guardian = _guardian; } - function setWithdrawalFee(uint256 _withdrawalFee) external { - _onlyGovernance(); - require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); - withdrawalFee = _withdrawalFee; - } - /// ===== Permissioned Actions: Controller ===== /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' diff --git a/contracts/badger-sett/sett-forks/Sett_B.sol b/contracts/badger-sett/sett-forks/Sett_B.sol index 0b8e641a..3e8aa42e 100644 --- a/contracts/badger-sett/sett-forks/Sett_B.sol +++ b/contracts/badger-sett/sett-forks/Sett_B.sol @@ -3,12 +3,12 @@ pragma solidity ^0.6.11; import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; @@ -19,8 +19,7 @@ import "./SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, - DefenderStorageless, SettVersion { +contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; diff --git a/contracts/badger-sett/sett-forks/Sett_C.sol b/contracts/badger-sett/sett-forks/Sett_C.sol index 756d8ae1..a9162a14 100644 --- a/contracts/badger-sett/sett-forks/Sett_C.sol +++ b/contracts/badger-sett/sett-forks/Sett_C.sol @@ -3,12 +3,12 @@ pragma solidity ^0.6.11; import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; @@ -20,8 +20,7 @@ import "./SettVersion.sol"; Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol NB: Only Digg sett inherits from/uses this fork. */ -contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, - DefenderStorageless, SettVersion { +contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -179,12 +178,6 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea guardian = _guardian; } - function setWithdrawalFee(uint256 _withdrawalFee) external { - _onlyGovernance(); - require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); - withdrawalFee = _withdrawalFee; - } - /// ===== Permissioned Actions: Controller ===== /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' From bf8eefb790ce7e52c6b0b3c22d131120f345e631 Mon Sep 17 00:00:00 2001 From: shake Date: Mon, 1 Mar 2021 18:00:23 -0500 Subject: [PATCH 07/12] save --- .../badger-remote/DefenderStorageless.sol | 8 ++++---- ...ender.sol => RemoteDefenderUpgradeable.sol} | 11 +++++++++-- ...reezer.sol => RemoteFreezerUpgradeable.sol} | 7 +++++-- ...ePauser.sol => RemotePauserUpgradeable.sol} | 7 +++++-- contracts/badger-sett/sett-forks/Sett_A.sol | 18 ++++++++++-------- 5 files changed, 33 insertions(+), 18 deletions(-) rename contracts/badger-remote/{RemoteDefender.sol => RemoteDefenderUpgradeable.sol} (59%) rename contracts/badger-remote/{RemoteFreezer.sol => RemoteFreezerUpgradeable.sol} (75%) rename contracts/badger-remote/{RemotePauser.sol => RemotePauserUpgradeable.sol} (67%) diff --git a/contracts/badger-remote/DefenderStorageless.sol b/contracts/badger-remote/DefenderStorageless.sol index 2fa31ba2..409bf7cd 100644 --- a/contracts/badger-remote/DefenderStorageless.sol +++ b/contracts/badger-remote/DefenderStorageless.sol @@ -4,16 +4,16 @@ pragma solidity ^0.6.0; import "interfaces/remote/IRemoteDefender.sol"; +import "./PauseableStorageless.sol"; + /* DefenderStorageless is a no-storage required inheritable defender of unapproved contract access. Contracts may safely inherit this w/o messing up their internal storage layout. */ -contract DefenderStorageless { +contract DefenderStorageless is PauseableStorageless { // Defend against access by unapproved contracts (EOAs are allowed access). modifier defend(address defender) { - require(IRemoteDefender(defender).approved(msg.sender) || - msg.sender == tx.origin, - "Access denied for caller"); + require(IRemoteDefender(defender).approved(msg.sender) || msg.sender == tx.origin, "Access denied for caller"); _; } } diff --git a/contracts/badger-remote/RemoteDefender.sol b/contracts/badger-remote/RemoteDefenderUpgradeable.sol similarity index 59% rename from contracts/badger-remote/RemoteDefender.sol rename to contracts/badger-remote/RemoteDefenderUpgradeable.sol index d927f48c..5bdfa236 100644 --- a/contracts/badger-remote/RemoteDefender.sol +++ b/contracts/badger-remote/RemoteDefenderUpgradeable.sol @@ -4,10 +4,14 @@ pragma solidity ^0.6.0; import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "./RemotePauserUpgradeable.sol"; +import "./RemoteFreezerUpgradeable.sol"; + /* - RemoteDefender defends against unapproved address access. + RemoteDefenderUpgradeable defends against unapproved address access. + It also handles freezing/pausing of contract addresses and EOAs. */ -contract RemoteDefender is OwnableUpgradeable { +contract RemoteDefenderUpgradeable is OwnableUpgradeable, RemoteFreezerUpgradeable, RemotePauserUpgradeable { mapping(address => bool) private _approved; function initialize() public initializer { @@ -25,4 +29,7 @@ contract RemoteDefender is OwnableUpgradeable { function revoke(address account) external onlyOwner { _approved[account] = false; } + + // Reserve storage space for upgrades. + uint256[49] private __gap; } diff --git a/contracts/badger-remote/RemoteFreezer.sol b/contracts/badger-remote/RemoteFreezerUpgradeable.sol similarity index 75% rename from contracts/badger-remote/RemoteFreezer.sol rename to contracts/badger-remote/RemoteFreezerUpgradeable.sol index cfae4f71..feb9fe11 100644 --- a/contracts/badger-remote/RemoteFreezer.sol +++ b/contracts/badger-remote/RemoteFreezerUpgradeable.sol @@ -5,10 +5,10 @@ pragma solidity ^0.6.0; import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; /* - RemoteFreezer handles reporting of frozen state and + RemoteFreezerUpgradeable handles reporting of frozen state and freezing of addresses by the owner. */ -contract RemoteFreezer is OwnableUpgradeable { +contract RemoteFreezerUpgradeable is OwnableUpgradeable { mapping(address => bool) private _frozen; function initialize() public initializer { @@ -26,4 +26,7 @@ contract RemoteFreezer is OwnableUpgradeable { function unfreeze(address account) external onlyOwner { _frozen[account] = false; } + + // Reserve storage space for upgrades. + uint256[49] private __gap; } diff --git a/contracts/badger-remote/RemotePauser.sol b/contracts/badger-remote/RemotePauserUpgradeable.sol similarity index 67% rename from contracts/badger-remote/RemotePauser.sol rename to contracts/badger-remote/RemotePauserUpgradeable.sol index 06b51ee6..52701090 100644 --- a/contracts/badger-remote/RemotePauser.sol +++ b/contracts/badger-remote/RemotePauserUpgradeable.sol @@ -5,10 +5,10 @@ pragma solidity ^0.6.0; import "interfaces/remote/IPauser.sol"; /* - RemotePauser only handles paused state for msg.sender (only msg.sender can modifies his own state) + RemotePauserUpgradeable only handles paused state for msg.sender (only msg.sender can modifies his own state) and takes care of state storage away from the originating contract. */ -contract RemotePauser { +contract RemotePauserUpgradeable { mapping(address => bool) private _paused; function paused() external view returns (bool) { @@ -22,4 +22,7 @@ contract RemotePauser { function unpause() external { _paused[msg.sender] = false; } + + // Reserve storage space for upgrades. + uint256[49] private __gap; } diff --git a/contracts/badger-sett/sett-forks/Sett_A.sol b/contracts/badger-sett/sett-forks/Sett_A.sol index 00cc0f4f..36b3f833 100644 --- a/contracts/badger-sett/sett-forks/Sett_A.sol +++ b/contracts/badger-sett/sett-forks/Sett_A.sol @@ -12,7 +12,6 @@ import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradea import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; -import "../badger-remote/PauseableStorageless.sol"; import "../badger-remote/DefenderStorageless.sol"; import "../SettAccessControlDefended.sol"; import "./SettVersion.sol"; @@ -20,7 +19,7 @@ import "./SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorageless, DefenderStorageless, SettVersion { +contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStorageless, SettVersion { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -40,8 +39,6 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); address public guardian; - // Remote pauser. - address public pauser; // Remote defender. address public defender; @@ -127,7 +124,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage /// @notice Deposit assets into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function deposit(uint256 _amount) public whenNotPaused(pauser) defend(defender) { + function deposit(uint256 _amount) public whenNotPaused(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -136,7 +133,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function depositAll() external whenNotPaused(pauser) defend(defender) { + function depositAll() external whenNotPaused(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -144,7 +141,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage } /// @notice No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) public whenNotPaused(pauser) defend(defender) { + function withdraw(uint256 _shares) public whenNotPaused(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -152,7 +149,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage } /// @notice Convenience function: Withdraw all shares of the sender - function withdrawAll() external whenNotPaused(pauser) defend(defender) { + function withdrawAll() external whenNotPaused(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -281,4 +278,9 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, PauseableStorage _blockLocked(); return super.transferFrom(sender, recipient, amount); } + + function setDefender(address _defender) { + _onlyGovernance(); + defender = _defender + } } From e6bae91838a8e13a09ffc34dcfa3414c9b3f68c7 Mon Sep 17 00:00:00 2001 From: shake Date: Tue, 2 Mar 2021 01:11:27 -0500 Subject: [PATCH 08/12] feat: merge defender logic, clean up sett forks and add test --- .../badger-remote/DefenderStorageless.sol | 5 +- .../badger-remote/PauseableStorageless.sol | 12 +-- .../RemoteDefenderUpgradeable.sol | 75 ++++++++++++++++--- .../RemoteFreezerUpgradeable.sol | 32 -------- .../badger-remote/RemotePauserUpgradeable.sol | 28 ------- contracts/badger-sett/DiggSett.sol | 4 +- .../{sett-forks/Sett_C.sol => Sett.sol} | 68 +++++++++++------ .../{sett-forks => }/SettVersion.sol | 0 contracts/badger-sett/sett-forks/README.md | 32 ++++---- contracts/badger-sett/sett-forks/Sett_A.sol | 59 +++++++-------- contracts/badger-sett/sett-forks/Sett_B.sol | 59 ++++++++------- helpers/gnosis_safe.py | 10 +-- interfaces/remote/IRemoteDefender.sol | 16 +++- interfaces/remote/IRemotePauser.sol | 4 + scripts/deploy/upgrade.py | 3 +- scripts/systems/upgrade_system.py | 29 +++++-- tests/upgrade/test_upgrade.py | 20 +++++ 17 files changed, 263 insertions(+), 193 deletions(-) delete mode 100644 contracts/badger-remote/RemoteFreezerUpgradeable.sol delete mode 100644 contracts/badger-remote/RemotePauserUpgradeable.sol rename contracts/badger-sett/{sett-forks/Sett_C.sol => Sett.sol} (77%) rename contracts/badger-sett/{sett-forks => }/SettVersion.sol (100%) create mode 100644 tests/upgrade/test_upgrade.py diff --git a/contracts/badger-remote/DefenderStorageless.sol b/contracts/badger-remote/DefenderStorageless.sol index 409bf7cd..dd22f379 100644 --- a/contracts/badger-remote/DefenderStorageless.sol +++ b/contracts/badger-remote/DefenderStorageless.sol @@ -4,16 +4,15 @@ pragma solidity ^0.6.0; import "interfaces/remote/IRemoteDefender.sol"; -import "./PauseableStorageless.sol"; - /* DefenderStorageless is a no-storage required inheritable defender of unapproved contract access. Contracts may safely inherit this w/o messing up their internal storage layout. */ -contract DefenderStorageless is PauseableStorageless { +contract DefenderStorageless { // Defend against access by unapproved contracts (EOAs are allowed access). modifier defend(address defender) { require(IRemoteDefender(defender).approved(msg.sender) || msg.sender == tx.origin, "Access denied for caller"); + require(!IRemoteDefender(defender).frozen(msg.sender), "Caller frozen"); _; } } diff --git a/contracts/badger-remote/PauseableStorageless.sol b/contracts/badger-remote/PauseableStorageless.sol index 712bfd22..e360b199 100644 --- a/contracts/badger-remote/PauseableStorageless.sol +++ b/contracts/badger-remote/PauseableStorageless.sol @@ -26,7 +26,7 @@ contract PauseableStorageless { * * - The contract must not be paused. */ - modifier whenNotPaused(address pauser) { + modifier whenNotPausedRemote(address pauser) { require(!IRemotePauser(pauser).paused(), "Pausable: paused"); _; } @@ -38,7 +38,7 @@ contract PauseableStorageless { * * - The contract must be paused. */ - modifier whenPaused(address pauser) { + modifier whenPausedRemote(address pauser) { require(IRemotePauser(pauser).paused(), "Pausable: not paused"); _; } @@ -50,8 +50,8 @@ contract PauseableStorageless { * * - The contract must not be paused. */ - function _pause(address pauser) internal virtual whenNotPaused { - IRemotePauser(pauser).pause(); + function _pause(address pauser) internal virtual { + IRemotePauser(pauser).pause(); emit Paused(msg.sender); } @@ -62,8 +62,8 @@ contract PauseableStorageless { * * - The contract must be paused. */ - function _unpause() internal virtual whenPaused { - IRemotePauser(pauser).unpause(); + function _unpause(address pauser) internal virtual { + IRemotePauser(pauser).unpause(); emit Unpaused(msg.sender); } } diff --git a/contracts/badger-remote/RemoteDefenderUpgradeable.sol b/contracts/badger-remote/RemoteDefenderUpgradeable.sol index 5bdfa236..247a7aca 100644 --- a/contracts/badger-remote/RemoteDefenderUpgradeable.sol +++ b/contracts/badger-remote/RemoteDefenderUpgradeable.sol @@ -4,30 +4,87 @@ pragma solidity ^0.6.0; import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "./RemotePauserUpgradeable.sol"; -import "./RemoteFreezerUpgradeable.sol"; - /* RemoteDefenderUpgradeable defends against unapproved address access. - It also handles freezing/pausing of contract addresses and EOAs. + It handles the following protective functionality (of contract addresses and EOAs): + - approved for contract specific or global access + - frozen from global access + - paused specific contract or globally */ -contract RemoteDefenderUpgradeable is OwnableUpgradeable, RemoteFreezerUpgradeable, RemotePauserUpgradeable { - mapping(address => bool) private _approved; +contract RemoteDefenderUpgradeable is OwnableUpgradeable { + // Is contract address approved for global access? + mapping(address => bool) private _approvedGlobal; + // Is contract address approved for targeted access to msg.sender? + mapping(address => mapping(address => bool)) private _approvedTargeted; + + // Is account address frozen? + mapping(address => bool) private _frozen; + + // Is contract address paused? + mapping(address => bool) private _paused; + // Is everything paused globally? + bool private _pausedGlobal; function initialize() public initializer { __Ownable_init(); } + // Access control functions. function approved(address account) external view returns (bool) { - return _approved[account]; + if (_approvedTargeted[account][msg.sender]) { + return true; + } + return _approvedGlobal[account]; } function approve(address account) external onlyOwner { - _approved[account] = true; + _approvedGlobal[account] = true; } function revoke(address account) external onlyOwner { - _approved[account] = false; + _approvedGlobal[account] = false; + } + + function approveFor(address account, address target) external onlyOwner { + _approvedTargeted[account][target] = true; + } + + function revokeFor(address account, address target) external onlyOwner { + _approvedTargeted[account][target] = false; + } + + // Freezer functions. + function frozen(address account) external view returns (bool) { + return _frozen[account]; + } + + function freeze(address account) external onlyOwner { + _frozen[account] = true; + } + + function unfreeze(address account) external onlyOwner { + _frozen[account] = false; + } + + // Pauser functions. + function paused() external view returns (bool) { + return _pausedGlobal || _paused[msg.sender]; + } + + function pauseGlobal() external onlyOwner { + _pausedGlobal = true; + } + + function unpauseGlobal() external onlyOwner { + _pausedGlobal = false; + } + + function pause() external { + _paused[msg.sender] = true; + } + + function unpause() external { + _paused[msg.sender] = false; } // Reserve storage space for upgrades. diff --git a/contracts/badger-remote/RemoteFreezerUpgradeable.sol b/contracts/badger-remote/RemoteFreezerUpgradeable.sol deleted file mode 100644 index feb9fe11..00000000 --- a/contracts/badger-remote/RemoteFreezerUpgradeable.sol +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.6.0; - -import "deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; - -/* - RemoteFreezerUpgradeable handles reporting of frozen state and - freezing of addresses by the owner. - */ -contract RemoteFreezerUpgradeable is OwnableUpgradeable { - mapping(address => bool) private _frozen; - - function initialize() public initializer { - __Ownable_init(); - } - - function frozen(address account) external view returns (bool) { - return _frozen[account]; - } - - function freeze(address account) external onlyOwner { - _frozen[account] = true; - } - - function unfreeze(address account) external onlyOwner { - _frozen[account] = false; - } - - // Reserve storage space for upgrades. - uint256[49] private __gap; -} diff --git a/contracts/badger-remote/RemotePauserUpgradeable.sol b/contracts/badger-remote/RemotePauserUpgradeable.sol deleted file mode 100644 index 52701090..00000000 --- a/contracts/badger-remote/RemotePauserUpgradeable.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.6.0; - -import "interfaces/remote/IPauser.sol"; - -/* - RemotePauserUpgradeable only handles paused state for msg.sender (only msg.sender can modifies his own state) - and takes care of state storage away from the originating contract. - */ -contract RemotePauserUpgradeable { - mapping(address => bool) private _paused; - - function paused() external view returns (bool) { - return _paused[msg.sender]; - } - - function pause() external { - _paused[msg.sender] = true; - } - - function unpause() external { - _paused[msg.sender] = false; - } - - // Reserve storage space for upgrades. - uint256[49] private __gap; -} diff --git a/contracts/badger-sett/DiggSett.sol b/contracts/badger-sett/DiggSett.sol index 5e9d77b2..e8561ef6 100644 --- a/contracts/badger-sett/DiggSett.sol +++ b/contracts/badger-sett/DiggSett.sol @@ -11,7 +11,7 @@ import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradea import "interfaces/badger/IController.sol"; import "interfaces/digg/IDigg.sol"; import "interfaces/digg/IDiggStrategy.sol"; -import "../Sett_C.sol"; +import "./Sett.sol"; /* bDIGG is denominated in scaledShares. @@ -32,7 +32,7 @@ import "../Sett_C.sol"; * Transfer functions are now pausable along with all other non-permissioned write functions * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well */ -contract DiggSett is Sett_C { +contract DiggSett is Sett { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; diff --git a/contracts/badger-sett/sett-forks/Sett_C.sol b/contracts/badger-sett/Sett.sol similarity index 77% rename from contracts/badger-sett/sett-forks/Sett_C.sol rename to contracts/badger-sett/Sett.sol index a9162a14..0a53b99a 100644 --- a/contracts/badger-sett/sett-forks/Sett_C.sol +++ b/contracts/badger-sett/Sett.sol @@ -2,25 +2,40 @@ pragma solidity ^0.6.11; -import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; -import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/SafeERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import "../../interfaces/badger/IController.sol"; import "../../interfaces/erc20/IERC20Detailed.sol"; import "../badger-remote/DefenderStorageless.sol"; -import "../SettAccessControlDefended.sol"; +import "../badger-remote/PauseableStorageless.sol"; +import "./SettAccessControlDefended.sol"; import "./SettVersion.sol"; /* + All new sett deploys should be based on this contract. There are some maintenance forks under `./sett-forks`. + Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol - NB: Only Digg sett inherits from/uses this fork. + + Changelog: + + V1.1 + * Strategist no longer has special function calling permissions + * Version function added to contract + * All write functions, with the exception of transfer, are pausable + * Keeper or governance can pause + * Only governance can unpause + + V1.2 + * Transfer functions are now pausable along with all other non-permissioned write functions + * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well */ -contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, DefenderStorageless, SettVersion { +contract Sett is SettVersion, ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, DefenderStorageless, PauseableStorageless { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -38,6 +53,8 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea string internal constant _symbolSymbolPrefix = "b"; address public guardian; + // Remote defender. + address public defender; event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); @@ -50,7 +67,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea bool _overrideTokenName, string memory _namePrefix, string memory _symbolPrefix - ) public initializer whenNotPaused { + ) public initializer whenNotPausedRemote(defender) { IERC20Detailed namedToken = IERC20Detailed(_token); string memory tokenName = namedToken.name(); string memory tokenSymbol = namedToken.symbol(); @@ -123,7 +140,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @notice Deposit assets into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function deposit(uint256 _amount) public whenNotPaused defend(defender) { + function deposit(uint256 _amount) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -132,7 +149,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function depositAll() external whenNotPaused defend(defender) { + function depositAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -140,7 +157,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea } /// @notice No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) public whenNotPaused defend(defender) { + function withdraw(uint256 _shares) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -148,7 +165,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea } /// @notice Convenience function: Withdraw all shares of the sender - function withdrawAll() external whenNotPaused defend(defender) { + function withdrawAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -159,21 +176,21 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @notice Set minimum threshold of underlying that must be deposited in strategy /// @notice Can only be changed by governance - function setMin(uint256 _min) external whenNotPaused { + function setMin(uint256 _min) external whenNotPausedRemote(defender) { _onlyGovernance(); min = _min; } /// @notice Change controller address /// @notice Can only be changed by governance - function setController(address _controller) public whenNotPaused { + function setController(address _controller) public whenNotPausedRemote(defender) { _onlyGovernance(); controller = _controller; } /// @notice Change guardian address /// @notice Can only be changed by governance - function setGuardian(address _guardian) external whenNotPaused { + function setGuardian(address _guardian) external whenNotPausedRemote(defender) { _onlyGovernance(); guardian = _guardian; } @@ -182,7 +199,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' /// @notice Only controller can trigger harvests - function harvest(address reserve, uint256 amount) external whenNotPaused { + function harvest(address reserve, uint256 amount) external whenNotPausedRemote(defender) { _onlyController(); require(reserve != address(token), "token"); IERC20Upgradeable(reserve).safeTransfer(controller, amount); @@ -193,7 +210,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @notice Transfer the underlying available to be claimed to the controller /// @notice The controller will deposit into the Strategy for yield-generating activities /// @notice Permissionless operation - function earn() public whenNotPaused { + function earn() public whenNotPausedRemote(defender) { _onlyAuthorizedActors(); uint256 _bal = available(); @@ -203,7 +220,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// @dev Emit event tracking current full price per share /// @dev Provides a pure on-chain way of approximating APY - function trackFullPricePerShare() external whenNotPaused { + function trackFullPricePerShare() external whenNotPausedRemote(defender) { _onlyAuthorizedActors(); emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); } @@ -264,7 +281,7 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea /// ===== ERC20 Overrides ===== /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. - function transfer(address recipient, uint256 amount) public virtual override whenNotPaused returns (bool) { + function transfer(address recipient, uint256 amount) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transfer(recipient, amount); } @@ -273,8 +290,13 @@ contract Sett_C is ERC20Upgradeable, SettAccessControlDefended, PausableUpgradea address sender, address recipient, uint256 amount - ) public virtual override whenNotPaused returns (bool) { + ) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transferFrom(sender, recipient, amount); } + + function setDefender(address _defender) external { + _onlyGovernance(); + defender = _defender; + } } diff --git a/contracts/badger-sett/sett-forks/SettVersion.sol b/contracts/badger-sett/SettVersion.sol similarity index 100% rename from contracts/badger-sett/sett-forks/SettVersion.sol rename to contracts/badger-sett/SettVersion.sol diff --git a/contracts/badger-sett/sett-forks/README.md b/contracts/badger-sett/sett-forks/README.md index a9b588fc..9cb35405 100644 --- a/contracts/badger-sett/sett-forks/README.md +++ b/contracts/badger-sett/sett-forks/README.md @@ -1,16 +1,22 @@ -# Layout Upgradeable +# Sett Forks -In order to maintain layout backwards compatibilty w/ different versions of contracts (e.g. Sett). +In order to maintain layout backwards compatibilty w/ different versions of Sett contracts. -Changelog: +Sett A/B forks service upgrades for the following setts. +``` +SETT_A = ( + "native.badger", + "native.renCrv", + "native.sbtcCrv", + "native.tbtcCrv", + "native.uniBadgerWbtc", + "harvest.renCrv", +) -V1.1 -* Strategist no longer has special function calling permissions -* Version function added to contract -* All write functions, with the exception of transfer, are pausable -* Keeper or governance can pause -* Only governance can unpause - -V1.2 -* Transfer functions are now pausable along with all other non-permissioned write functions -* All permissioned write functions, with the exception of pause() & unpause(), are pausable as well +SETT_B = ( + "native.sushiWbtcEth", + "native.sushiBadgerWbtc", + "native.uniDiggWbtc", + "native.sushiDiggWbtc", +) +``` diff --git a/contracts/badger-sett/sett-forks/Sett_A.sol b/contracts/badger-sett/sett-forks/Sett_A.sol index 36b3f833..960d797c 100644 --- a/contracts/badger-sett/sett-forks/Sett_A.sol +++ b/contracts/badger-sett/sett-forks/Sett_A.sol @@ -10,16 +10,17 @@ import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgra import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "../../interfaces/badger/IController.sol"; -import "../../interfaces/erc20/IERC20Detailed.sol"; -import "../badger-remote/DefenderStorageless.sol"; +import "../../../interfaces/badger/IController.sol"; +import "../../../interfaces/erc20/IERC20Detailed.sol"; +import "../../badger-remote/DefenderStorageless.sol"; +import "../../badger-remote/PauseableStorageless.sol"; import "../SettAccessControlDefended.sol"; -import "./SettVersion.sol"; +import "../SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStorageless, SettVersion { +contract Sett_A is SettVersion, ERC20Upgradeable, SettAccessControlDefended, DefenderStorageless, PauseableStorageless { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -36,12 +37,13 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel string internal constant _defaultNamePrefix = "Badger Sett "; string internal constant _symbolSymbolPrefix = "b"; - event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); - address public guardian; // Remote defender. address public defender; + event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); + + // NB: This is a maintenance fork and initialize is not intended to be used. function initialize( address _token, address _controller, @@ -51,7 +53,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel bool _overrideTokenName, string memory _namePrefix, string memory _symbolPrefix - ) public initializer whenNotPaused(pauser) { + ) public initializer { IERC20Detailed namedToken = IERC20Detailed(_token); string memory tokenName = namedToken.name(); string memory tokenSymbol = namedToken.symbol(); @@ -79,9 +81,6 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel min = 9500; emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); - - // Paused on launch - _pause(); } /// ===== Modifiers ===== @@ -90,8 +89,8 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel require(msg.sender == controller, "onlyController"); } - function _onlyAuthorizedPausers() internal view { - require(msg.sender == guardian || msg.sender == governance, "onlyPausers"); + function _onlyAuthorizeddefenders() internal view { + require(msg.sender == guardian || msg.sender == governance, "onlydefenders"); } function _blockLocked() internal view { @@ -124,7 +123,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @notice Deposit assets into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function deposit(uint256 _amount) public whenNotPaused(defender) defend(defender) { + function deposit(uint256 _amount) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -133,7 +132,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function depositAll() external whenNotPaused(defender) defend(defender) { + function depositAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -141,7 +140,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel } /// @notice No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) public whenNotPaused(defender) defend(defender) { + function withdraw(uint256 _shares) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -149,7 +148,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel } /// @notice Convenience function: Withdraw all shares of the sender - function withdrawAll() external whenNotPaused(defender) defend(defender) { + function withdrawAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -160,21 +159,21 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @notice Set minimum threshold of underlying that must be deposited in strategy /// @notice Can only be changed by governance - function setMin(uint256 _min) external whenNotPaused(pauser) { + function setMin(uint256 _min) external whenNotPausedRemote(defender) { _onlyGovernance(); min = _min; } /// @notice Change controller address /// @notice Can only be changed by governance - function setController(address _controller) public whenNotPaused(pauser) { + function setController(address _controller) public whenNotPausedRemote(defender) { _onlyGovernance(); controller = _controller; } /// @notice Change guardian address /// @notice Can only be changed by governance - function setGuardian(address _guardian) external whenNotPaused(pauser) { + function setGuardian(address _guardian) external whenNotPausedRemote(defender) { _onlyGovernance(); guardian = _guardian; } @@ -183,7 +182,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' /// @notice Only controller can trigger harvests - function harvest(address reserve, uint256 amount) external whenNotPaused(pauser) { + function harvest(address reserve, uint256 amount) external whenNotPausedRemote(defender) { _onlyController(); require(reserve != address(token), "token"); IERC20Upgradeable(reserve).safeTransfer(controller, amount); @@ -194,7 +193,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @notice Transfer the underlying available to be claimed to the controller /// @notice The controller will deposit into the Strategy for yield-generating activities /// @notice Permissionless operation - function earn() public whenNotPaused(pauser) { + function earn() public whenNotPausedRemote(defender) { _onlyAuthorizedActors(); uint256 _bal = available(); @@ -204,19 +203,19 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// @dev Emit event tracking current full price per share /// @dev Provides a pure on-chain way of approximating APY - function trackFullPricePerShare() external whenNotPaused(pauser) { + function trackFullPricePerShare() external whenNotPausedRemote(defender) { _onlyAuthorizedActors(); emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); } function pause() external { - _onlyAuthorizedPausers(); - _pause(); + _onlyAuthorizeddefenders(); + _pause(defender); } function unpause() external { _onlyGovernance(); - _unpause(); + _unpause(defender); } /// ===== Internal Implementations ===== @@ -265,7 +264,7 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel /// ===== ERC20 Overrides ===== /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. - function transfer(address recipient, uint256 amount) public virtual override whenNotPaused(pauser) returns (bool) { + function transfer(address recipient, uint256 amount) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transfer(recipient, amount); } @@ -274,13 +273,13 @@ contract Sett_A is ERC20Upgradeable, SettAccessControlDefended, DefenderStoragel address sender, address recipient, uint256 amount - ) public virtual override whenNotPaused(pauser) returns (bool) { + ) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transferFrom(sender, recipient, amount); } - function setDefender(address _defender) { + function setDefender(address _defender) external { _onlyGovernance(); - defender = _defender + defender = _defender; } } diff --git a/contracts/badger-sett/sett-forks/Sett_B.sol b/contracts/badger-sett/sett-forks/Sett_B.sol index 3e8aa42e..2ff2ae94 100644 --- a/contracts/badger-sett/sett-forks/Sett_B.sol +++ b/contracts/badger-sett/sett-forks/Sett_B.sol @@ -10,16 +10,17 @@ import "../../../deps/@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgra import "../../../deps/@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "../../../deps/@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "../../interfaces/badger/IController.sol"; -import "../../interfaces/erc20/IERC20Detailed.sol"; -import "../badger-remote/DefenderStorageless.sol"; +import "../../../interfaces/badger/IController.sol"; +import "../../../interfaces/erc20/IERC20Detailed.sol"; +import "../../badger-remote/DefenderStorageless.sol"; +import "../../badger-remote/PauseableStorageless.sol"; import "../SettAccessControlDefended.sol"; -import "./SettVersion.sol"; +import "../SettVersion.sol"; /* Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol */ -contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless, SettVersion { +contract Sett_B is SettVersion, ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefended, DefenderStorageless, PauseableStorageless { using SafeERC20Upgradeable for IERC20Upgradeable; using AddressUpgradeable for address; using SafeMathUpgradeable for uint256; @@ -37,8 +38,12 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen string internal constant _defaultNamePrefix = "Badger Sett "; string internal constant _symbolSymbolPrefix = "b"; + // Remote defender. + address public defender; + event FullPricePerShareUpdated(uint256 value, uint256 indexed timestamp, uint256 indexed blockNumber); + // NB: This is a maintenance fork and initialize is not intended to be used. function initialize( address _token, address _controller, @@ -48,7 +53,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen bool _overrideTokenName, string memory _namePrefix, string memory _symbolPrefix - ) public initializer whenNotPaused { + ) public initializer whenNotPausedRemote(defender) { IERC20Detailed namedToken = IERC20Detailed(_token); string memory tokenName = namedToken.name(); string memory tokenSymbol = namedToken.symbol(); @@ -76,9 +81,6 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen min = 9500; emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); - - // Paused on launch - _pause(); } /// ===== Modifiers ===== @@ -121,7 +123,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// @notice Deposit assets into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function deposit(uint256 _amount) public whenNotPaused defend(defender) { + function deposit(uint256 _amount) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -130,7 +132,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// @notice Convenience function: Deposit entire balance of asset into the Sett, and return corresponding shares to the user /// @notice Only callable by EOA accounts that pass the defend() check - function depositAll() external whenNotPaused defend(defender) { + function depositAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -138,7 +140,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen } /// @notice No rebalance implementation for lower fees and faster swaps - function withdraw(uint256 _shares) public whenNotPaused defend(defender) { + function withdraw(uint256 _shares) public whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -146,7 +148,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen } /// @notice Convenience function: Withdraw all shares of the sender - function withdrawAll() external whenNotPaused defend(defender) { + function withdrawAll() external whenNotPausedRemote(defender) defend(defender) { _blockLocked(); _lockForBlock(msg.sender); @@ -157,36 +159,30 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// @notice Set minimum threshold of underlying that must be deposited in strategy /// @notice Can only be changed by governance - function setMin(uint256 _min) external whenNotPaused { + function setMin(uint256 _min) external whenNotPausedRemote(defender) { _onlyGovernance(); min = _min; } /// @notice Change controller address /// @notice Can only be changed by governance - function setController(address _controller) public whenNotPaused { + function setController(address _controller) public whenNotPausedRemote(defender) { _onlyGovernance(); controller = _controller; } /// @notice Change guardian address /// @notice Can only be changed by governance - function setGuardian(address _guardian) external whenNotPaused { + function setGuardian(address _guardian) external whenNotPausedRemote(defender) { _onlyGovernance(); guardian = _guardian; } - function setWithdrawalFee(uint256 _withdrawalFee) external { - _onlyGovernance(); - require(_withdrawalFee <= MAX_FEE, "sett/excessive-withdrawal-fee"); - withdrawalFee = _withdrawalFee; - } - /// ===== Permissioned Actions: Controller ===== /// @notice Used to swap any borrowed reserve over the debt limit to liquidate to 'token' /// @notice Only controller can trigger harvests - function harvest(address reserve, uint256 amount) external whenNotPaused { + function harvest(address reserve, uint256 amount) external whenNotPausedRemote(defender) { _onlyController(); require(reserve != address(token), "token"); IERC20Upgradeable(reserve).safeTransfer(controller, amount); @@ -197,7 +193,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// @notice Transfer the underlying available to be claimed to the controller /// @notice The controller will deposit into the Strategy for yield-generating activities /// @notice Permissionless operation - function earn() public whenNotPaused { + function earn() public whenNotPausedRemote(defender) { _onlyAuthorizedActors(); uint256 _bal = available(); @@ -207,19 +203,19 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// @dev Emit event tracking current full price per share /// @dev Provides a pure on-chain way of approximating APY - function trackFullPricePerShare() external whenNotPaused { + function trackFullPricePerShare() external whenNotPausedRemote(defender) { _onlyAuthorizedActors(); emit FullPricePerShareUpdated(getPricePerFullShare(), now, block.number); } function pause() external { _onlyAuthorizedPausers(); - _pause(); + _pause(defender); } function unpause() external { _onlyGovernance(); - _unpause(); + _unpause(defender); } /// ===== Internal Implementations ===== @@ -268,7 +264,7 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen /// ===== ERC20 Overrides ===== /// @dev Add blockLock to transfers, users cannot transfer tokens in the same block as a deposit or withdrawal. - function transfer(address recipient, uint256 amount) public virtual override whenNotPaused returns (bool) { + function transfer(address recipient, uint256 amount) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transfer(recipient, amount); } @@ -277,8 +273,13 @@ contract Sett_B is ERC20Upgradeable, PausableUpgradeable, SettAccessControlDefen address sender, address recipient, uint256 amount - ) public virtual override whenNotPaused returns (bool) { + ) public virtual override whenNotPausedRemote(defender) returns (bool) { _blockLocked(); return super.transferFrom(sender, recipient, amount); } + + function setDefender(address _defender) external { + _onlyGovernance(); + defender = _defender; + } } diff --git a/helpers/gnosis_safe.py b/helpers/gnosis_safe.py index d394e7c7..ff1b0356 100644 --- a/helpers/gnosis_safe.py +++ b/helpers/gnosis_safe.py @@ -27,7 +27,7 @@ def __init__(self, description, operation=None, callInfo=None): if not operation: self.operation = "" - + if not callInfo: self.callInfo = "" @@ -39,7 +39,7 @@ class MultisigTx: def __init__(self, params, metadata: MultisigTxMetadata): self.params = params self.metadata = metadata - + # def printMetadata(self): # def printParams(self): @@ -84,16 +84,16 @@ def executeTx(self, id=None, print_output=True): if self.testMode: tx = exec_direct(self.contract, tx.params) - if print_output: + if print_output and hasattr(tx, "call_trace"): print(tx.call_trace()) - # try: + # try: # failEvents = tx.events['ExecutionFailure'] # if len(failEvents) > 0: # print(tx.events) # assert False # except EventLookupError: return tx - + def get_first_owner(self): return self.contract.getOwners()[0] diff --git a/interfaces/remote/IRemoteDefender.sol b/interfaces/remote/IRemoteDefender.sol index d288e81f..788f46cb 100644 --- a/interfaces/remote/IRemoteDefender.sol +++ b/interfaces/remote/IRemoteDefender.sol @@ -3,9 +3,19 @@ pragma solidity >=0.6.0; interface IRemoteDefender { - function appproved(address) external view returns (bool); + function approved(address account) external view returns (bool); - function approve(address) external; + function approve(address account) external; - function revoke(address) external; + function revoke(address account) external; + + function approveFor(address account, address target) external; + + function revoke(address account, address target) external; + + function frozen(address account) external view returns (bool); + + function freeze(address account) external; + + function unfreeze(address account) external; } diff --git a/interfaces/remote/IRemotePauser.sol b/interfaces/remote/IRemotePauser.sol index e0d11dfc..db26632b 100644 --- a/interfaces/remote/IRemotePauser.sol +++ b/interfaces/remote/IRemotePauser.sol @@ -8,4 +8,8 @@ interface IRemotePauser { function pause() external; function unpause() external; + + function pauseGlobal() external; + + function unpauseGlobal() external; } diff --git a/scripts/deploy/upgrade.py b/scripts/deploy/upgrade.py index 37c1d6e6..5fded0c2 100644 --- a/scripts/deploy/upgrade.py +++ b/scripts/deploy/upgrade.py @@ -1,6 +1,5 @@ from brownie.network.contract import ProjectContract -from scripts.systems.badger_system import BadgerSystem from helpers.gnosis_safe import GnosisSafe, MultisigTxMetadata @@ -14,7 +13,7 @@ # Upgrades versioned proxy contract's impl/logic contract if not latest version. # Contracts must implement a version method above or they default to version 0.0. def upgrade_versioned_proxy( - badger: BadgerSystem, + badger, # upgradeable proxy contract proxy: ProjectContract, # latest logic contract diff --git a/scripts/systems/upgrade_system.py b/scripts/systems/upgrade_system.py index 41552a6a..a4f30926 100644 --- a/scripts/systems/upgrade_system.py +++ b/scripts/systems/upgrade_system.py @@ -1,9 +1,10 @@ from brownie import ( - ProjectContract, DiggSett, Sett_A, Sett_B, + exceptions, ) +from brownie.network.contract import ProjectContract from typing import Optional, List, Tuple, Any from rich.console import Console @@ -79,24 +80,26 @@ def __init__(self, badger, deployer): } def upgrade_sett_contract(self, contractKey: str, validate: bool = False) -> None: - self._upgrade_with_suffix( + self._upgrade_contract( SETT_SUFFIX, self.badger.getSettArtifact, contractKey=contractKey, + validate=validate, ) def upgrade_strategy_contract(self, contractKey: str, validate: bool = False) -> None: - self._upgrade_with_suffix( + self._upgrade_contract( STRATEGY_SUFFIX, self.badger.getStrategyArtifact, contractKey=contractKey, + validate=validate, ) def upgrade_sett_contracts(self, validate: bool = False) -> None: - self._upgrade_with_suffix(SETT_SUFFIX, self.getSettArtifact) + self._upgrade_contract(SETT_SUFFIX, self.getSettArtifact, validate=validate) def upgrade_strategy_contracts(self, validate: bool = False) -> None: - self._upgrade_with_suffix(STRATEGY_SUFFIX, self.badger.getStrategyArtifact) + self._upgrade_contract(STRATEGY_SUFFIX, self.badger.getStrategyArtifact, validate=validate) def _upgrade_contract( self, @@ -123,8 +126,11 @@ def _upgrade_contract( ) validator.snapshot(contract) - if not validator.validate(): - console.print("[red]=== Failed to validate upgrade non matching storage vars: {}[/red]".format(validator.snapshots)) + if validate: + if not validator.validate(): + console.print("[red]=== Failed to validate upgrade non matching storage vars: {}[/red]".format(validator.snapshots)) + raise Exception("validation failed") + validator.reset() def track_contract_upgradeable(self, key: str, contract: ProjectContract) -> None: self.contracts_upgradeable[key] = contract @@ -156,7 +162,11 @@ def __init__(self, fields: List[str]): def snapshot(self, contract: ProjectContract) -> None: snapshot = () for field in self.fields: - snapshot += getattr(contract, field)() + try: + snapshot += (getattr(contract, field)(),) + except exceptions.VirtualMachineError: + # Return None if the field doesn't exist (revert). + snapshot += (None,) self.snapshots.append(snapshot) def validate(self) -> bool: @@ -166,3 +176,6 @@ def validate(self) -> bool: return False prev = snapshot return True + + def reset(self) -> None: + self.snapshots = [] diff --git a/tests/upgrade/test_upgrade.py b/tests/upgrade/test_upgrade.py new file mode 100644 index 00000000..cd7a17bd --- /dev/null +++ b/tests/upgrade/test_upgrade.py @@ -0,0 +1,20 @@ +from rich.console import Console + +from config.badger_config import badger_config +from scripts.systems.badger_system import connect_badger + +console = Console() + + +def test_upgrade(): + # connect to prod deploy and run simulation + console.print("[grey]connecting to badger system...[/grey]") + badger = connect_badger(badger_config.prod_json) + + console.print("[grey]upgrading sett contracts...[/grey]") + badger.upgrade.upgrade_sett_contracts(validate=True) + + console.print("[grey]upgrading strategy contracts...[/grey]") + badger.upgrade.upgrade_strategy_contracts(validate=True) + + console.print("[green]sett/strategy contracts upgraded[/green]") From 4c1ef96702338e08f5e8c66ee14f8849404220ec Mon Sep 17 00:00:00 2001 From: shake Date: Thu, 4 Mar 2021 01:01:20 -0500 Subject: [PATCH 09/12] fix: upgrades must be made from gov timelock --- scripts/deploy/upgrade.py | 14 +------------- scripts/systems/badger_system.py | 4 ++++ scripts/systems/upgrade_system.py | 9 +++++++-- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/scripts/deploy/upgrade.py b/scripts/deploy/upgrade.py index 5fded0c2..41518685 100644 --- a/scripts/deploy/upgrade.py +++ b/scripts/deploy/upgrade.py @@ -1,7 +1,5 @@ from brownie.network.contract import ProjectContract -from helpers.gnosis_safe import GnosisSafe, MultisigTxMetadata - # try all the methods in priority order VERSION_METHODS = [ @@ -29,17 +27,7 @@ def upgrade_versioned_proxy( ): return - multi = GnosisSafe(badger.devMultisig) - - multi.execute( - MultisigTxMetadata(description="Upgrade versioned proxy contract with new logic version",), - { - "to": badger.devProxyAdmin.address, - "data": badger.devProxyAdmin.upgrade.encode_input( - proxy, logic - ), - }, - ) + badger.devProxyAdmin.upgrade(proxy, logic, {"from": badger.governanceTimelock}) def _get_version(contract: ProjectContract) -> float: diff --git a/scripts/systems/badger_system.py b/scripts/systems/badger_system.py index 13f7ef91..6eeccf25 100644 --- a/scripts/systems/badger_system.py +++ b/scripts/systems/badger_system.py @@ -202,6 +202,7 @@ def connect_badger( badger.connect_honeypot_meme(badger_deploy["honeypotMeme"]) badger.connect_community_pool(badger_deploy["communityPool"]) badger.connect_dao_badger_timelock(badger_deploy["daoBadgerTimelock"]) + badger.connect_governance_timelock(badger_deploy["timelock"]) badger.connect_rewards_manager(badger_deploy["badgerRewardsManager"]) badger.connect_unlock_scheduler(badger_deploy["unlockScheduler"]) @@ -950,6 +951,9 @@ def connect_dao_badger_timelock(self, address): self.daoBadgerTimelock = SimpleTimelock.at(address) self.upgrade.track_contract_upgradeable("daoBadgerTimelock", self.daoBadgerTimelock) + def connect_governance_timelock(self, address): + self.governanceTimelock = GovernanceTimelock.at(address) + def connect_rewards_manager(self, address): self.badgerRewardsManager = BadgerRewardsManager.at(address) self.upgrade.track_contract_upgradeable( diff --git a/scripts/systems/upgrade_system.py b/scripts/systems/upgrade_system.py index a4f30926..5f9f9dd2 100644 --- a/scripts/systems/upgrade_system.py +++ b/scripts/systems/upgrade_system.py @@ -172,8 +172,13 @@ def snapshot(self, contract: ProjectContract) -> None: def validate(self) -> bool: prev = self.snapshots[0] for snapshot in self.snapshots[1:]: - if snapshot != prev: - return False + # Snapshots should be the same length. + for idx in range(0, len(snapshot)): + # If field unimpl in last version, skip comparison. + if prev[idx] is None: + continue + if snapshot[idx] != prev[idx]: + return False prev = snapshot return True From c623cae0eef07c6346853f70ba2ade1d3baad390 Mon Sep 17 00:00:00 2001 From: shake Date: Thu, 4 Mar 2021 01:02:53 -0500 Subject: [PATCH 10/12] chore: reserve 50 --- contracts/badger-remote/RemoteDefenderUpgradeable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/badger-remote/RemoteDefenderUpgradeable.sol b/contracts/badger-remote/RemoteDefenderUpgradeable.sol index 247a7aca..7f1bbc3a 100644 --- a/contracts/badger-remote/RemoteDefenderUpgradeable.sol +++ b/contracts/badger-remote/RemoteDefenderUpgradeable.sol @@ -88,5 +88,5 @@ contract RemoteDefenderUpgradeable is OwnableUpgradeable { } // Reserve storage space for upgrades. - uint256[49] private __gap; + uint256[50] private __gap; } From f9645829c6483f40a69c1a44a073dc850f3a77e5 Mon Sep 17 00:00:00 2001 From: shake Date: Sun, 7 Mar 2021 22:28:02 -0500 Subject: [PATCH 11/12] test: add test for defender and upgrade to v1.3 --- .../test/ThirdPartyContractAccess.sol | 33 ++++++ contracts/badger-sett/Sett.sol | 13 --- contracts/badger-sett/SettVersion.sol | 22 +++- scripts/systems/badger_system.py | 34 +++++- scripts/systems/bridge_minimal.py | 2 +- scripts/systems/bridge_system.py | 6 +- scripts/systems/upgrade_system.py | 2 + .../SushiDiggWbtcLpOptimizerMiniDeploy.py | 2 +- .../sett/fixtures/UniDiggWbtcLpMiniDeploy.py | 2 +- tests/sett/test_sett_defender.py | 108 ++++++++++++++++++ 10 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 contracts/badger-remote/test/ThirdPartyContractAccess.sol create mode 100644 tests/sett/test_sett_defender.py diff --git a/contracts/badger-remote/test/ThirdPartyContractAccess.sol b/contracts/badger-remote/test/ThirdPartyContractAccess.sol new file mode 100644 index 00000000..49021336 --- /dev/null +++ b/contracts/badger-remote/test/ThirdPartyContractAccess.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "interfaces/badger/ISett.sol"; + +/* + ThirdPartyContractAccess is a test contract for simulating third party contract access to our API. + */ +contract ThirdPartyContractAccess { + address public sett; + + constructor(address _sett) public { + sett = _sett; + } + + // Test third party access paths for Sett methods (if authorized). + function depositAll() external { + ISett(sett).depositAll(); + } + + function withdrawAll() external { + ISett(sett).withdrawAll(); + } + + function deposit(uint256 _amount) external { + ISett(sett).deposit(_amount); + } + + function withdraw(uint256 _amount) external { + ISett(sett).withdraw(_amount); + } +} diff --git a/contracts/badger-sett/Sett.sol b/contracts/badger-sett/Sett.sol index 0a53b99a..58b764b4 100644 --- a/contracts/badger-sett/Sett.sol +++ b/contracts/badger-sett/Sett.sol @@ -21,19 +21,6 @@ import "./SettVersion.sol"; All new sett deploys should be based on this contract. There are some maintenance forks under `./sett-forks`. Source: https://github.com/iearn-finance/yearn-protocol/blob/develop/contracts/vaults/yVault.sol - - Changelog: - - V1.1 - * Strategist no longer has special function calling permissions - * Version function added to contract - * All write functions, with the exception of transfer, are pausable - * Keeper or governance can pause - * Only governance can unpause - - V1.2 - * Transfer functions are now pausable along with all other non-permissioned write functions - * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well */ contract Sett is SettVersion, ERC20Upgradeable, SettAccessControlDefended, PausableUpgradeable, DefenderStorageless, PauseableStorageless { using SafeERC20Upgradeable for IERC20Upgradeable; diff --git a/contracts/badger-sett/SettVersion.sol b/contracts/badger-sett/SettVersion.sol index a6e16b87..4072b10e 100644 --- a/contracts/badger-sett/SettVersion.sol +++ b/contracts/badger-sett/SettVersion.sol @@ -2,9 +2,27 @@ pragma solidity ^0.6.11; -// Track underlying version of all Sett forks in one place. +/* + All Sett contracts inherit from this. + + Changelog: + + V1.1 + * Strategist no longer has special function calling permissions + * Version function added to contract + * All write functions, with the exception of transfer, are pausable + * Keeper or governance can pause + * Only governance can unpause + + V1.2 + * Transfer functions are now pausable along with all other non-permissioned write functions + * All permissioned write functions, with the exception of pause() & unpause(), are pausable as well + + V1.3 + * Add global defender w/ pause/freeze capabilities. +*/ contract SettVersion { function version() public view returns (string memory) { - return "1.2"; + return "1.3"; } } diff --git a/scripts/systems/badger_system.py b/scripts/systems/badger_system.py index 6eeccf25..38ffb289 100644 --- a/scripts/systems/badger_system.py +++ b/scripts/systems/badger_system.py @@ -505,12 +505,8 @@ def deploy_team_vesting(self): ) self.upgrade.track_contract_upgradeable("teamVesting", self.teamVesting) - def deploy_logic(self, name, BrownieArtifact, test=False): + def deploy_logic(self, name, BrownieArtifact): deployer = self.deployer - if test: - self.logic[name] = BrownieArtifact.deploy({"from": deployer}) - return - self.logic[name] = BrownieArtifact.deploy( {"from": deployer}, publish_source=self.publish_source ) @@ -853,6 +849,18 @@ def deploy_strategy_harvest_rencrv(self): self.wire_up_sett(sett, strategy, controller) + def deploy_defender(self): + deployer = self.deployer + self.defender = deploy_proxy( + "RemoteDefenderUpgradeable", + RemoteDefenderUpgradeable.abi, + self.logic.RemoteDefenderUpgradeable.address, + self.devProxyAdmin.address, + # no args, owner is deployer + self.logic.RemoteDefenderUpgradeable.initialize.encode_input(), + deployer, + ) + def signal_token_lock(self, id, params): geyser = self.getGeyser(id) self.rewardsEscrow.signalTokenLock( @@ -1077,3 +1085,19 @@ def getStrategyArtifact(self, id): def getStrategyArtifactName(self, id): return self.strategy_artifacts[id]["artifactName"] + + # ===== Configuration ===== + + # configureDefender configures defender globally for all contracts. + def configureDefender(self): + multi = GnosisSafe(self.devMultisig) + for key, sett in self.sett_system.vaults.items(): + multi.execute( + MultisigTxMetadata( + description=f"Configure defender on Sett {sett.address}" + ), + { + "to": sett.address, + "data": sett.setDefender.encode_input(self.defender.address), + }, + ) diff --git a/scripts/systems/bridge_minimal.py b/scripts/systems/bridge_minimal.py index 1c2ccc85..89939ffe 100644 --- a/scripts/systems/bridge_minimal.py +++ b/scripts/systems/bridge_minimal.py @@ -24,7 +24,7 @@ def deploy_bridge_minimal(deployer, devProxyAdmin, test=False, publish_source=Fa bridge.deploy_mocks() registry = bridge.mocks.registry - bridge.deploy_logic("BadgerBridgeAdapter", BadgerBridgeAdapter, test=test) + bridge.deploy_logic("BadgerBridgeAdapter", BadgerBridgeAdapter) bridge.deploy_adapter( registry, swap.router, diff --git a/scripts/systems/bridge_system.py b/scripts/systems/bridge_system.py index 255f5361..933ab3b5 100644 --- a/scripts/systems/bridge_system.py +++ b/scripts/systems/bridge_system.py @@ -125,12 +125,8 @@ def deploy_adapter( deployer, ) - def deploy_logic(self, name, BrownieArtifact, test=False): + def deploy_logic(self, name, BrownieArtifact): deployer = self.deployer - if test: - self.logic[name] = BrownieArtifact.deploy({"from": deployer}) - return - self.logic[name] = BrownieArtifact.deploy( {"from": deployer}, publish_source=self.publish_source ) diff --git a/scripts/systems/upgrade_system.py b/scripts/systems/upgrade_system.py index 5f9f9dd2..bd53fb22 100644 --- a/scripts/systems/upgrade_system.py +++ b/scripts/systems/upgrade_system.py @@ -132,6 +132,8 @@ def _upgrade_contract( raise Exception("validation failed") validator.reset() + console.print(f"[green]upgraded sett {key} @ {contract.address}") + def track_contract_upgradeable(self, key: str, contract: ProjectContract) -> None: self.contracts_upgradeable[key] = contract diff --git a/tests/sett/fixtures/SushiDiggWbtcLpOptimizerMiniDeploy.py b/tests/sett/fixtures/SushiDiggWbtcLpOptimizerMiniDeploy.py index 37053220..b992f343 100644 --- a/tests/sett/fixtures/SushiDiggWbtcLpOptimizerMiniDeploy.py +++ b/tests/sett/fixtures/SushiDiggWbtcLpOptimizerMiniDeploy.py @@ -27,7 +27,7 @@ def fetch_params(self): params.token = self.digg.token params.badgerTree = self.badger.badgerTree - self.badger.deploy_logic("DiggRewardsFaucet", DiggRewardsFaucet, test=True) + self.badger.deploy_logic("DiggRewardsFaucet", DiggRewardsFaucet) self.rewards = self.badger.deploy_digg_rewards_faucet( self.key, self.digg.token ) diff --git a/tests/sett/fixtures/UniDiggWbtcLpMiniDeploy.py b/tests/sett/fixtures/UniDiggWbtcLpMiniDeploy.py index 07a6c8a3..a49b8786 100644 --- a/tests/sett/fixtures/UniDiggWbtcLpMiniDeploy.py +++ b/tests/sett/fixtures/UniDiggWbtcLpMiniDeploy.py @@ -26,7 +26,7 @@ def fetch_params(self): want = params.want params.token = self.digg.token - self.badger.deploy_logic("DiggRewardsFaucet", DiggRewardsFaucet, test=True) + self.badger.deploy_logic("DiggRewardsFaucet", DiggRewardsFaucet) self.rewards = self.badger.deploy_digg_rewards_faucet( self.key, self.digg.token ) diff --git a/tests/sett/test_sett_defender.py b/tests/sett/test_sett_defender.py new file mode 100644 index 00000000..1e3bfa8e --- /dev/null +++ b/tests/sett/test_sett_defender.py @@ -0,0 +1,108 @@ +import random +from rich.console import Console +from typing import ( + Optional, + List, + Any, +) +from brownie import ( + accounts, + network, + exceptions, + RemoteDefenderUpgradeable, + ThirdPartyContractAccess, +) + +from config.badger_config import badger_config +from scripts.systems.badger_system import connect_badger + +console = Console() + +# Schema: (METHOD_NAME, (*ARGS),) +SETT_METHODS_AND_ARGS = [ + ("depositAll", (),), + ("withdrawAll", (),), + ("withdraw", (0,),), + ("deposit", (0,),), +] + + +def test_defender(): + # connect to prod deploy and run simulation + badger = connect_badger(badger_config.prod_json) + badger.upgrade.upgrade_sett_contracts() + + badger.deploy_logic("RemoteDefenderUpgradeable", RemoteDefenderUpgradeable) + badger.deploy_defender() + badger.configureDefender() + + deployer = badger.deployer + defender = badger.defender + + # Test EOA access. + user = accounts[random.randint(10, 40)] + + # Validate the access unrestricted case works first. + for sett in badger.sett_system.vaults.values(): + _testSett(user, sett) + + defender.pauseGlobal({"from": deployer}) + for sett in badger.sett_system.vaults.values(): + _testSett(user, sett, error="Pausable: paused") + defender.unpauseGlobal({"from": deployer}) + + defender.freeze(user, {"from": deployer}) + for sett in badger.sett_system.vaults.values(): + _testSett(user, sett, error="Caller frozen") + defender.unfreeze(user, {"from": deployer}) + + # Validate the access unrestricted case works last. + for sett in badger.sett_system.vaults.values(): + _testSett(user, sett) + + # Test non EOA access. + for sett in badger.sett_system.vaults.values(): + contract = ThirdPartyContractAccess.deploy(sett, {"from": deployer}) + _testSett(user, contract, error="Access denied for caller") + + # Approve third party contract for access. + defender.approve(contract, {"from": deployer}) + _testSett(user, contract) + + # Revoke access. + defender.revoke(contract, {"from": deployer}) + _testSett(user, contract, error="Access denied for caller") + + +def _testSett( + user: network.account.Account, + sett: network.contract.ProjectContract, + error: Optional[str] = None, +) -> None: + for (method, args) in SETT_METHODS_AND_ARGS: + _testContractMethod( + sett, + method, + args + ({"from": user},), + error=error + ) + + +def _testContractMethod( + contract: network.contract.ProjectContract, + method: str, + args: List[Any], + error: Optional[str] = None, +) -> None: + reverted = False + try: + getattr(contract, method)(*args) + except exceptions.VirtualMachineError as e: + if error is None: + __import__('pdb').set_trace() + assert error in e.message + reverted = True + + # Ensure that the method has reverted if expected error is supplied. + if error is not None: + assert reverted From 2b08adda8ff7f863fa9620e88e176109cb3c59e2 Mon Sep 17 00:00:00 2001 From: shake Date: Sun, 7 Mar 2021 23:09:34 -0500 Subject: [PATCH 12/12] test: use reverts api --- tests/sett/test_sett_defender.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/sett/test_sett_defender.py b/tests/sett/test_sett_defender.py index 1e3bfa8e..fa9a35c9 100644 --- a/tests/sett/test_sett_defender.py +++ b/tests/sett/test_sett_defender.py @@ -6,9 +6,9 @@ Any, ) from brownie import ( + reverts, accounts, network, - exceptions, RemoteDefenderUpgradeable, ThirdPartyContractAccess, ) @@ -94,15 +94,9 @@ def _testContractMethod( args: List[Any], error: Optional[str] = None, ) -> None: - reverted = False - try: + if error is None: + getattr(contract, method)(*args) + return + + with reverts(error): getattr(contract, method)(*args) - except exceptions.VirtualMachineError as e: - if error is None: - __import__('pdb').set_trace() - assert error in e.message - reverted = True - - # Ensure that the method has reverted if expected error is supplied. - if error is not None: - assert reverted