Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds new governance pause and migration functions #222

Merged
merged 1 commit into from
Nov 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions contracts/ConvergentCurvePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import "./balancer-core-v2/vault/interfaces/IMinimalSwapInfoPool.sol";
import "./balancer-core-v2/vault/interfaces/IVault.sol";
import "./balancer-core-v2/pools/BalancerPoolToken.sol";

// SECURITY - A governance address can freeze trading and deposits but has no access to user funds
// and cannot stop withdraws.

contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
using LogExpMath for uint256;
using FixedPoint for uint256;
Expand All @@ -30,7 +33,12 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {

// The fees recorded during swaps. These will be 18 point not token decimal encoded
uint128 public feesUnderlying;
uint128 public feesBond;
// This typing is a weird mismatch but we want to be able to hold a bool as well
uint120 public feesBond;
// A bool to indicate if the contract is paused, stored with 'fees bond'
bool public paused;
// A mapping of who can pause
mapping(address => bool) public pausers;
// Stored records of governance tokens
address public immutable governance;
// The percent of each trade's implied yield to collect as LP fee
Expand All @@ -49,6 +57,8 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
// The max percent fee for governance, immutable after compilation
uint256 public constant FEE_BOUND = 3e17;

// State saying if the contract is paused

/// @notice This event allows the frontend to track the fees
/// @param collectedBase the base asset tokens fees collected in this txn
/// @param collectedBond the bond asset tokens fees collected in this txn
Expand All @@ -74,6 +84,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
/// @param _governance The address which gets minted reward lp
/// @param name The balancer pool token name
/// @param symbol The balancer pool token symbol
/// @param _pauser An address that can pause trades and deposits
constructor(
IERC20 _underlying,
IERC20 _bond,
Expand All @@ -84,7 +95,8 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
uint256 _percentFeeGov,
address _governance,
string memory name,
string memory symbol
string memory symbol,
address _pauser
) BalancerPoolToken(name, symbol) {
// Sanity Check
require(_expiration - block.timestamp < _unitSeconds);
Expand All @@ -102,6 +114,9 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
tokens[1] = _underlying;
}

// Set that the _pauser can pause
pausers[_pauser] = true;

// Pass in zero addresses for Asset Managers
// Note - functions below assume this token order
vault.registerTokens(poolId, tokens, new address[](2));
Expand Down Expand Up @@ -151,7 +166,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
SwapRequest memory swapRequest,
uint256 currentBalanceTokenIn,
uint256 currentBalanceTokenOut
) public override returns (uint256) {
) public override notPaused() returns (uint256) {
// Check that the sender is pool, we change state so must make
// this check.
require(msg.sender == address(_vault), "Non Vault caller");
Expand Down Expand Up @@ -235,6 +250,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
)
external
override
notPaused()
returns (
uint256[] memory amountsIn,
uint256[] memory dueProtocolFeeAmounts
Expand Down Expand Up @@ -377,6 +393,30 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
data[bondIndex] = _normalize(data[bondIndex], 18, bondDecimals);
}

// Permission-ed functions

/// @notice checks for a pause on trading and depositing functionality
modifier notPaused() {
require(!paused, "Paused");
_;
}

/// @notice Allows an authorized address or the owner to pause this contract
/// @param pauseStatus true for paused, false for not paused
/// @dev the caller must be authorized
function pause(bool pauseStatus) external {
require(pausers[msg.sender], "Sender not Authorized");
paused = pauseStatus;
}

/// @notice Governance sets someone's pause status
/// @param who The address
/// @param status true for able to pause false for not
function setPauser(address who, bool status) external {
require(msg.sender == governance, "Sender not Owner");
pausers[who] = status;
}

// Math libraries and internal routing

/// @dev Calculates how many tokens should be outputted given an input plus reserve variables
Expand Down Expand Up @@ -449,7 +489,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
amountIn.sub(amountOut)
);
// we record that collected fee from the input bond
feesBond += uint128(impliedYieldFee);
feesBond += uint120(impliedYieldFee);
// and return the updated input quote
return amountIn.add(impliedYieldFee);
}
Expand All @@ -460,7 +500,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
amountOut.sub(amountIn)
);
// we record that fee collected from the bond output
feesBond += uint128(impliedYieldFee);
feesBond += uint120(impliedYieldFee);
// and then return the updated output
return amountOut.sub(impliedYieldFee);
} else {
Expand Down Expand Up @@ -649,7 +689,7 @@ contract ConvergentCurvePool is IMinimalSwapInfoPool, BalancerPoolToken {
);
// Store the remaining fees
feesUnderlying = uint128(remainingUnderlying);
feesBond = uint128(remainingBond);
feesBond = uint120(remainingBond);
// We return the fees which were removed from storage
return (usedFeeUnderlying, usedFeeBond);
}
Expand Down
113 changes: 105 additions & 8 deletions contracts/YVaultAssetProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ pragma solidity ^0.8.0;
import "./interfaces/IERC20.sol";
import "./interfaces/IYearnVault.sol";
import "./WrappedPosition.sol";
import "./libraries/Authorizable.sol";

/// SECURITY - This contract has an owner address which can migrate funds to a new yearn vault [or other contract
/// with compatible interface] as well as pause deposits and withdraws. This means that any deposited funds
/// have the same security as that address.

/// @author Element Finance
/// @title Yearn Vault v1 Asset Proxy
contract YVaultAssetProxy is WrappedPosition {
// the yearn vault and how many decimals it's deposit receipt tokens are
IYearnVault public immutable vault;
contract YVaultAssetProxy is WrappedPosition, Authorizable {
// The addresses of the current yearn vault
IYearnVault public vault;
// 18 decimal fractional form of the multiplier which is applied after
// a vault upgrade. 0 when no upgrade has happened
uint88 public conversionRate;
// Bool packed into the same storage slot as vault and conversion rate
bool public paused;
uint8 public immutable vaultDecimals;

/// @notice Constructs this contract and stores needed data
Expand All @@ -20,14 +30,25 @@ contract YVaultAssetProxy is WrappedPosition {
/// This token should revert in the event of a transfer failure.
/// @param _name The name of the token created
/// @param _symbol The symbol of the token created
/// @param _governance The address which can upgrade the yearn vault
/// @param _pauser address which can pause this contract
constructor(
address vault_,
IERC20 _token,
string memory _name,
string memory _symbol
) WrappedPosition(_token, _name, _symbol) {
string memory _symbol,
address _governance,
address _pauser
) WrappedPosition(_token, _name, _symbol) Authorizable() {
// Authorize the pauser
_authorize(_pauser);
// set the owner
setOwner(_governance);
// Set the vault
vault = IYearnVault(vault_);
// Approve the vault so it can pull tokens from this address
_token.approve(vault_, type(uint256).max);
// Load the decimals and set them as an immutable
uint8 localVaultDecimals = IERC20(vault_).decimals();
vaultDecimals = localVaultDecimals;
require(
Expand All @@ -36,29 +57,55 @@ contract YVaultAssetProxy is WrappedPosition {
);
}

/// @notice Checks that the contract has not been paused
modifier notPaused() {
require(!paused, "Paused");
_;
}

/// @notice Makes the actual deposit into the yearn vault
/// @return Tuple (the shares minted, amount underlying used)
function _deposit() internal override returns (uint256, uint256) {
function _deposit()
internal
override
notPaused()
returns (uint256, uint256)
{
// Get the amount deposited
uint256 amount = token.balanceOf(address(this));

// Deposit and get the shares that were minted to this
uint256 shares = vault.deposit(amount, address(this));

// If we have migrated our shares are no longer 1 - 1 with the vault shares
if (conversionRate != 0) {
// conversionRate is the fraction of yearnSharePrice1/yearnSharePrices2 at time of migration
// and so this multiplication will convert between yearn shares in the new vault and
// those in the old vault
shares = (shares * conversionRate) / 1e18;
}

// Return the amount of shares the user has produced, and the amount used for it.
return (shares, amount);
}

/// @notice Withdraw the number of shares
/// @param _shares The number of shares to withdraw
/// @param _shares The number of wrapped position shares to withdraw
/// @param _destination The address to send the output funds
// @param _underlyingPerShare The possibly precomputed underlying per share
/// @return returns the amount of funds freed by doing a yearn withdraw
function _withdraw(
uint256 _shares,
address _destination,
uint256
) internal override returns (uint256) {
) internal override notPaused() returns (uint256) {
// If the conversion rate is non-zero we have upgraded and so our wrapped shares are
// not one to one with the original shares.
if (conversionRate != 0) {
// Then since conversion rate is yearnSharePrice1/yearnSharePrices2 we divide the
// wrapped position shares by it because they are equivalent to the first yearn vault shares
_shares = (_shares * 1e18) / conversionRate;
}
// Withdraws shares from the vault. Max loss is set at 100% as
// the minimum output value is enforced by the calling
// function in the WrappedPosition contract.
Expand All @@ -77,6 +124,11 @@ contract YVaultAssetProxy is WrappedPosition {
view
returns (uint256)
{
// We may have to convert before using the vault price per share
if (conversionRate != 0) {
// Imitate the _withdraw logic and convert this amount to yearn vault2 shares
_amount = (_amount * 1e18) / conversionRate;
}
return (_amount * _pricePerShare()) / (10**vaultDecimals);
}

Expand All @@ -91,4 +143,49 @@ contract YVaultAssetProxy is WrappedPosition {
token.approve(address(vault), 0);
token.approve(address(vault), type(uint256).max);
}

/// @notice Allows an authorized address or the owner to pause this contract
/// @param pauseStatus true for paused, false for not paused
/// @dev the caller must be authorized
function pause(bool pauseStatus) external onlyAuthorized() {
paused = pauseStatus;
}

/// @notice Function to transition between two yearn vaults
/// @param newVault The address of the new vault
/// @param minOutputShares The min of the new yearn vault's shares the wp will receive
/// @dev WARNING - This function has the capacity to steal all user funds from this
/// contract and so it should be ensured that the owner is a high quorum
/// governance vote through the time lock.
function transition(IYearnVault newVault, uint256 minOutputShares)
external
onlyOwner
{
// Load the current vault's price per share
uint256 currentPricePerShare = _pricePerShare();
// Load the new vault's price per share
uint256 newPricePerShare = newVault.pricePerShare();
// Load the current conversion rate or set it to 1
uint256 newConversionRate = conversionRate == 0 ? 1e18 : conversionRate;
// Calculate the new conversion rate, note by multiplying by the old
// conversion rate here we implicitly support more than 1 upgrade
newConversionRate =
(newConversionRate * newPricePerShare) /
currentPricePerShare;
// We now withdraw from the old yearn vault using max shares
// Note - Vaults should be checked in the future that they still have this behavior
vault.withdraw(type(uint256).max, address(this), 10000);
// Approve the new vault
token.approve(address(newVault), type(uint256).max);
// Then we deposit into the new vault
uint256 currentBalance = token.balanceOf(address(this));
uint256 outputShares = newVault.deposit(currentBalance, address(this));
// We enforce a min output
require(outputShares >= minOutputShares, "Not enough output");
// Change the stored variables
vault = newVault;
// because of the truncation yearn vaults can't have a larger diff than ~ billion
// times larger
conversionRate = uint88(newConversionRate);
}
}
7 changes: 5 additions & 2 deletions contracts/factories/ConvergentPoolFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ contract ConvergentPoolFactory is BasePoolFactory, Authorizable {
/// @param _percentFee The fee percent of each trades implied yield paid to gov.
/// @param _name The name of the balancer v2 lp token for this pool
/// @param _symbol The symbol of the balancer v2 lp token for this pool
/// @param _pauser An address with the power to stop trading and deposits
/// @return The new pool address
function create(
address _underlying,
Expand All @@ -49,7 +50,8 @@ contract ConvergentPoolFactory is BasePoolFactory, Authorizable {
uint256 _unitSeconds,
uint256 _percentFee,
string memory _name,
string memory _symbol
string memory _symbol,
address _pauser
) external returns (address) {
address pool = address(
new ConvergentCurvePool(
Expand All @@ -62,7 +64,8 @@ contract ConvergentPoolFactory is BasePoolFactory, Authorizable {
percentFeeGov,
governance,
_name,
_symbol
_symbol,
_pauser
)
);
// Register the pool with the vault
Expand Down
5 changes: 3 additions & 2 deletions contracts/test/TestConvergentCurvePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ contract TestConvergentCurvePool is ConvergentCurvePool {
_percentFee,
_governance,
name,
symbol
symbol,
_governance
)
{} // solhint-disable-line no-empty-blocks

Expand Down Expand Up @@ -103,7 +104,7 @@ contract TestConvergentCurvePool is ConvergentCurvePool {
}

// Allows tests to specify fees without making trades
function setFees(uint128 amountUnderlying, uint128 amountBond) public {
function setFees(uint128 amountUnderlying, uint120 amountBond) public {
feesUnderlying = amountUnderlying;
feesBond = amountBond;
}
Expand Down
4 changes: 4 additions & 0 deletions contracts/test/TestYVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ contract TestYVault is ERC20PermitWithSupply {
address destination,
uint256
) external returns (uint256) {
// Yearn supports this
if (_shares == type(uint256).max) {
_shares = balanceOf[msg.sender];
}
uint256 _amount = (_shares * pricePerShare()) / (10**decimals);
_burn(msg.sender, _shares);
IERC20(token).transfer(destination, _amount);
Expand Down
Loading