diff --git a/packages/foundry/contracts/interfaces/ILadleGov.sol b/packages/foundry/contracts/interfaces/ILadleGov.sol index 1ad3ce69..b5bbabd5 100644 --- a/packages/foundry/contracts/interfaces/ILadleGov.sol +++ b/packages/foundry/contracts/interfaces/ILadleGov.sol @@ -6,6 +6,10 @@ import "./IJoin.sol"; interface ILadleGov { function joins(bytes6) external view returns (IJoin); + function addToken(address, bool) external; + + function addIntegration(address, bool) external; + function addJoin(bytes6, address) external; function addPool(bytes6, address) external; diff --git a/packages/foundry/contracts/utils/ContangoWand.sol b/packages/foundry/contracts/utils/ContangoWand.sol new file mode 100644 index 00000000..00522ec0 --- /dev/null +++ b/packages/foundry/contracts/utils/ContangoWand.sol @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.8.13; + +import "../interfaces/ICauldronGov.sol"; +import "../interfaces/ICauldron.sol"; +import "../interfaces/ILadleGov.sol"; +import "../interfaces/ILadle.sol"; +import "../interfaces/IJoin.sol"; +import "../oracles/yieldspace/YieldSpaceMultiOracle.sol"; +import "../oracles/composite/CompositeMultiOracle.sol"; +import "@yield-protocol/yieldspace-tv/src/interfaces/IPool.sol"; +import "@yield-protocol/utils-v2/src/access/AccessControl.sol"; + +/// @title A contract that allows configuring the cauldron and ladle within bounds +contract ContangoWand is AccessControl { + ICauldronGov public immutable cauldron; + ICauldron public immutable masterCauldron; + ILadleGov public immutable ladle; + ILadle public immutable masterLadle; + YieldSpaceMultiOracle public immutable yieldSpaceOracle; + CompositeMultiOracle public immutable compositeOracle; + + mapping (bytes6 => mapping(bytes6 => uint32)) public ratio; + mapping (bytes6 => mapping(bytes6 => DataTypes.Debt)) public debt; + + DataTypes.Debt public defaultDebtLimits; + uint32 public defaultRatio; + + constructor( + ICauldronGov cauldron_, + ICauldron masterCauldron_, + ILadleGov ladle_, + ILadle masterLadle_, + YieldSpaceMultiOracle yieldSpaceOracle_, + CompositeMultiOracle compositeOracle_ + ) { + cauldron = cauldron_; + masterCauldron = masterCauldron_; + ladle = ladle_; + masterLadle = masterLadle_; + yieldSpaceOracle = yieldSpaceOracle_; + compositeOracle = compositeOracle_; + } + + /// ----------------- Cauldron Governance ----------------- + + /// @notice Copy the spotOracle and ratio from the master cauldron + function copySpotOracle(bytes6 baseId, bytes6 ilkId) external auth { + DataTypes.SpotOracle memory spotOracle_ = masterCauldron.spotOracles(baseId, ilkId); + cauldron.setSpotOracle(baseId, ilkId, spotOracle_.oracle, spotOracle_.ratio); + } + + /// @notice Copy the lending oracle from the master cauldron + function copyLendingOracle(bytes6 baseId) external auth { + IOracle lendingOracle_ = masterCauldron.lendingOracles(baseId); + cauldron.setLendingOracle(baseId, lendingOracle_); + } + + /// @notice Copy the debt limits from the master cauldron + function copyDebtLimits(bytes6 baseId, bytes6 ilkId) external auth { + DataTypes.Debt memory debt_ = masterCauldron.debt(baseId, ilkId); + cauldron.setDebtLimits(baseId, ilkId, debt_.max, debt_.min, debt_.dec); + } + + /// @notice Add a new asset in the Cauldron, as long as it is an asset or fyToken known to the Yield Cauldron + function addAsset(bytes6 assetId) external auth { + address asset_ = masterCauldron.assets(assetId); + require( + asset_ != address(0) || + address(masterCauldron.series(assetId).fyToken) != address(0), + "Asset not known to the Yield Cauldron"); + cauldron.addAsset(assetId, asset_); + } + + /// @notice Add a new series, if it exists in the Yield Cauldron + function addSeries(bytes6 seriesId) external auth { + DataTypes.Series memory series_ = masterCauldron.series(seriesId); + require(address(series_.fyToken) != address(0), "Series not known to the Yield Cauldron"); + cauldron.addSeries(seriesId, series_.baseId, series_.fyToken); + } + + /// @notice Add ilks to series + function addIlks(bytes6 seriesId, bytes6[] calldata ilkIds) external auth { + cauldron.addIlks(seriesId, ilkIds); + } + + /// @notice Bound ratio for a given asset pair + function boundRatio(bytes6 baseId, bytes6 ilkId, uint32 ratio_) external auth { + ratio[baseId][ilkId] = ratio_; + } + + /// @notice Set the default ratio + function setDefaultRatio(uint32 ratio_) external auth { + defaultRatio = ratio_; + } + + /// @notice Set the ratio for a given asset pair in the Cauldron, within bounds. Set the spot oracle always to the composite oracle. + function setRatio(bytes6 baseId, bytes6 ilkId, uint32 ratio_) external auth { + // If the ilkId is a series and boundaries are not set, set ratio to the default + uint32 bound_ = ratio[baseId][ilkId]; + if (bound_ == 0 && cauldron.series(ilkId).fyToken != IFYToken(address(0))) { + ratio[baseId][ilkId] = bound_ = defaultRatio; + } + require(ratio_ >= bound_, "Ratio out of bounds"); + cauldron.setSpotOracle(baseId, ilkId, compositeOracle, ratio_); + } + + function _getDebtDecimals(bytes6 baseId, bytes6 ilkId) internal view returns (uint8 dec) { + // If the debt is already set in the cauldron, we use the decimals from there + // Otherwise, we use the decimals of the base + DataTypes.Debt memory cauldronDebt_ = ICauldron(address(cauldron)).debt(baseId, ilkId); + if (cauldronDebt_.sum != 0) { + dec = cauldronDebt_.dec; + } else { + dec = IERC20Metadata(cauldron.assets(baseId)).decimals(); + } + } + + /// @notice Bound debt limits for a given asset pair + function boundDebtLimits(bytes6 baseId, bytes6 ilkId, uint96 max, uint24 min) external auth { + debt[baseId][ilkId] = DataTypes.Debt({ + max: max, + min: min, + dec: _getDebtDecimals(baseId, ilkId), + sum: 0 + }); + } + + /// @notice Set the default debt limits + function setDefaultDebtLimits(uint96 max, uint24 min) external auth { + defaultDebtLimits = DataTypes.Debt({ + max: max, + min: min, + dec: 0, + sum: 0 + }); + } + + /// @notice Set the debt limits for a given asset pair in the Cauldron, within bounds + function setDebtLimits(bytes6 baseId, bytes6 ilkId, uint96 max, uint24 min) external auth { + // If the ilkId is a series and boundaries are not set, set them to default values + DataTypes.Debt memory bounds_ = debt[baseId][ilkId]; + + if (bounds_.max == 0 && bounds_.min == 0) { + bounds_ = defaultDebtLimits; + bounds_.dec = _getDebtDecimals(baseId, ilkId); + debt[baseId][ilkId] = bounds_; + } + require(max <= bounds_.max, "Max debt out of bounds"); + require(min >= bounds_.min, "Min debt out of bounds"); + + cauldron.setDebtLimits(baseId, ilkId, max, min, bounds_.dec); + } + + /// ----------------- Oracle Governance ----------------- + + /// @notice Set a pool as a source in the YieldSpace oracle, as long as: + /// - It is a pool known to the Yield Ladle + /// - The baseId matches the pool's baseId + /// - The quoteId matches the pool's seriesId + function setYieldSpaceOracleSource(bytes6 seriesId) external auth { + IPool pool_ = IPool(masterLadle.pools(seriesId)); + require(address(pool_) != address(0), "Pool not known to the Yield Ladle"); + DataTypes.Series memory series_ = masterCauldron.series(seriesId); + require(address(series_.fyToken) != address(0), "Series not known to the Yield Cauldron"); + require(address(series_.fyToken) == address(pool_.fyToken()), "fyToken mismatch"); // Sanity check + + yieldSpaceOracle.setSource(series_.baseId, seriesId, pool_); + } + + /// @notice Set the YieldSpace oracle as the source for a given asset pair in the Composite oracle, provided the source is set in the YieldSpace oracle + function setCompositeOracleSource(bytes6 baseId, bytes6 ilkId) external auth { + (IPool pool_, ) = yieldSpaceOracle.sources(baseId, ilkId); + require(address(pool_) != address(0), "YieldSpace oracle not set"); + compositeOracle.setSource(baseId, ilkId, yieldSpaceOracle); + } + + /// @notice Set a path in the Composite oracle, as long as the path is not overwriting anything + function setCompositeOraclePath(bytes6 baseId, bytes6 quoteId, bytes6[] calldata path) external auth { + require(compositeOracle.paths(baseId, quoteId, 0) == bytes6(0), "Path already set"); // We check that the first element in the path is empty + compositeOracle.setPath(baseId, quoteId, path); + } + + /// ----------------- Ladle Governance ----------------- + + /// @notice Propagate a pool to the Ladle from the Yield Ladle + function addPool(bytes6 seriesId) external auth { + address pool_ = masterLadle.pools(seriesId); + require(pool_ != address(0), "Pool not known to the Yield Ladle"); + ladle.addPool(seriesId, pool_); + } + + /// @notice Propagate an integration to the Ladle from the Yield Ladle + function addIntegration(address integration) external auth { + ladle.addIntegration(integration, masterLadle.integrations(integration)); + } + + /// @notice Propagate a token to the Ladle from the Yield Ladle + function addToken(address token) external auth { + ladle.addToken(token, masterLadle.tokens(token)); + } + + /// @notice Add join to the Ladle. + /// @dev These will often be used to hold fyToken, so it doesn't seem possible to put boundaries. However, it seems low risk. Famous last words. + function addJoin(bytes6 assetId, address join) external auth { + ladle.addJoin(assetId, join); + } +}