diff --git a/Makefile b/Makefile index 5f9e0a1..04c07c5 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,8 @@ deploy-weETH-zksync :; forge script --zksync scripts/DeployZkSync.s.sol:DeployWe deploy-sUSDe-zksync :; forge script --zksync scripts/DeployZkSync.s.sol:DeploySUSDeZkSync --rpc-url zksync $(common-flags) deploy-USDe-zksync :; forge script --zksync scripts/DeployZkSync.s.sol:DeployUSDeZkSync --rpc-url zksync $(common-flags) +deploy-eurc-base :; forge script scripts/DeployBase.s.sol:DeployEURCBase --rpc-url base $(common-flags) + # Utilities download :; cast etherscan-source --chain ${chain} -d src/etherscan/${chain}_${address} ${address} git-diff : diff --git a/scripts/DeployBase.s.sol b/scripts/DeployBase.s.sol index 7bbb163..65035af 100644 --- a/scripts/DeployBase.s.sol +++ b/scripts/DeployBase.s.sol @@ -4,12 +4,14 @@ pragma solidity ^0.8.0; import {GovV3Helpers} from 'aave-helpers/GovV3Helpers.sol'; import {BaseScript} from 'solidity-utils/contracts/utils/ScriptUtils.sol'; import {AaveV3Base, AaveV3BaseAssets} from 'aave-address-book/AaveV3Base.sol'; - +import {EURPriceCapAdapterStable, IEURPriceCapAdapterStable, IChainlinkAggregator} from '../src/contracts/misc-adapters/EURPriceCapAdapterStable.sol'; import {CLRatePriceCapAdapter, IPriceCapAdapter} from '../src/contracts/CLRatePriceCapAdapter.sol'; library CapAdaptersCodeBase { address public constant weETH_eETH_AGGREGATOR = 0x35e9D7001819Ea3B39Da906aE6b06A62cfe2c181; address public constant ezETH_ETH_AGGREGATOR = 0xC4300B7CF0646F0Fe4C5B2ACFCCC4dCA1346f5d8; + address public constant EURC_PRICE_FEED = 0xDAe398520e2B67cd3f27aeF9Cf14D93D927f8250; + address public constant EUR_PRICE_FEED = 0xc91D87E81faB8f93699ECf7Ee9B44D11e1D53F0F; function weETHAdapterCode() internal pure returns (bytes memory) { return @@ -52,6 +54,23 @@ library CapAdaptersCodeBase { ) ); } + + function EURCAdapterCode() internal pure returns (bytes memory) { + return + abi.encodePacked( + type(EURPriceCapAdapterStable).creationCode, + abi.encode( + IEURPriceCapAdapterStable.CapAdapterStableParamsEUR({ + aclManager: AaveV3Base.ACL_MANAGER, + assetToUsdAggregator: IChainlinkAggregator(EURC_PRICE_FEED), + baseToUsdAggregator: IChainlinkAggregator(EUR_PRICE_FEED), + adapterDescription: 'Capped EURC/USD', + priceCapRatio: int256(1.04 * 1e8), + ratioDecimals: 8 + }) + ) + ); + } } contract DeployWeEthBase is BaseScript { @@ -65,3 +84,9 @@ contract DeployEzEthBase is BaseScript { GovV3Helpers.deployDeterministic(CapAdaptersCodeBase.ezETHAdapterCode()); } } + +contract DeployEURCBase is BaseScript { + function run() external broadcast { + GovV3Helpers.deployDeterministic(CapAdaptersCodeBase.EURCAdapterCode()); + } +} diff --git a/src/contracts/misc-adapters/EURPriceCapAdapterStable.sol b/src/contracts/misc-adapters/EURPriceCapAdapterStable.sol new file mode 100644 index 0000000..d958873 --- /dev/null +++ b/src/contracts/misc-adapters/EURPriceCapAdapterStable.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.19; + +import {IEURPriceCapAdapterStable, ICLSynchronicityPriceAdapter, IACLManager, IChainlinkAggregator} from '../../interfaces/IEURPriceCapAdapterStable.sol'; + +/** + * @title EURPriceCapAdapterStable + * @author BGD Labs + * @notice Price capped adapter to cap the price of the EUR asset using the + * @notice chainlink market feeds ASSET/USD and EUR/USD + */ +contract EURPriceCapAdapterStable is IEURPriceCapAdapterStable { + /// @inheritdoc IEURPriceCapAdapterStable + IChainlinkAggregator public immutable ASSET_TO_USD_AGGREGATOR; + + /// @inheritdoc IEURPriceCapAdapterStable + IChainlinkAggregator public immutable BASE_TO_USD_AGGREGATOR; + + /// @inheritdoc IEURPriceCapAdapterStable + IACLManager public immutable ACL_MANAGER; + + /// @inheritdoc IEURPriceCapAdapterStable + uint8 public immutable RATIO_DECIMALS; + + /// @inheritdoc ICLSynchronicityPriceAdapter + uint8 public decimals; + + /// @inheritdoc ICLSynchronicityPriceAdapter + string public description; + + int256 internal _priceCapRatio; + + /** + * @param capAdapterStableParams parameters to create eur stable cap adapter + */ + constructor(CapAdapterStableParamsEUR memory capAdapterStableParams) { + if (address(capAdapterStableParams.aclManager) == address(0)) { + revert ACLManagerIsZeroAddress(); + } + + ASSET_TO_USD_AGGREGATOR = capAdapterStableParams.assetToUsdAggregator; + BASE_TO_USD_AGGREGATOR = capAdapterStableParams.baseToUsdAggregator; + ACL_MANAGER = capAdapterStableParams.aclManager; + RATIO_DECIMALS = capAdapterStableParams.ratioDecimals; + description = capAdapterStableParams.adapterDescription; + decimals = ASSET_TO_USD_AGGREGATOR.decimals(); + + _setPriceCapRatio(capAdapterStableParams.priceCapRatio); + } + + /// @inheritdoc ICLSynchronicityPriceAdapter + function latestAnswer() external view returns (int256) { + int256 assetPrice = ASSET_TO_USD_AGGREGATOR.latestAnswer(); + int256 basePrice = BASE_TO_USD_AGGREGATOR.latestAnswer(); + int256 maxPrice = (basePrice * _priceCapRatio) / int256(10 ** RATIO_DECIMALS); + + if (assetPrice > maxPrice) { + return maxPrice; + } + + return assetPrice; + } + + /// @inheritdoc IEURPriceCapAdapterStable + function setPriceCapRatio(int256 priceCapRatio) external { + if (!ACL_MANAGER.isRiskAdmin(msg.sender) && !ACL_MANAGER.isPoolAdmin(msg.sender)) { + revert CallerIsNotRiskOrPoolAdmin(); + } + + _setPriceCapRatio(priceCapRatio); + } + + /// @inheritdoc IEURPriceCapAdapterStable + function getPriceCapRatio() external view returns (int256) { + return _priceCapRatio; + } + + /// @inheritdoc IEURPriceCapAdapterStable + function isCapped() public view virtual returns (bool) { + return (ASSET_TO_USD_AGGREGATOR.latestAnswer() > this.latestAnswer()); + } + + /** + * @notice Updates price cap ratio + * @param priceCapRatio the new price cap ratio + */ + function _setPriceCapRatio(int256 priceCapRatio) internal virtual { + int256 assetPrice = ASSET_TO_USD_AGGREGATOR.latestAnswer(); + int256 basePrice = BASE_TO_USD_AGGREGATOR.latestAnswer(); + + if ((basePrice * priceCapRatio) / int256(10 ** RATIO_DECIMALS) < assetPrice) { + revert CapLowerThanActualPrice(); + } + + _priceCapRatio = priceCapRatio; + + emit PriceCapRatioUpdated(priceCapRatio); + } +} diff --git a/src/interfaces/IEURPriceCapAdapterStable.sol b/src/interfaces/IEURPriceCapAdapterStable.sol new file mode 100644 index 0000000..cd49603 --- /dev/null +++ b/src/interfaces/IEURPriceCapAdapterStable.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IACLManager} from 'aave-address-book/AaveV3.sol'; +import {IChainlinkAggregator} from 'cl-synchronicity-price-adapter/interfaces/IChainlinkAggregator.sol'; +import {ICLSynchronicityPriceAdapter} from 'cl-synchronicity-price-adapter/interfaces/ICLSynchronicityPriceAdapter.sol'; + +interface IEURPriceCapAdapterStable is ICLSynchronicityPriceAdapter { + /** + * @notice Parameters to create eur stable cap adapter + * @param capAdapterStableParams parameters to create eur stable cap adapter + */ + struct CapAdapterStableParamsEUR { + IACLManager aclManager; + IChainlinkAggregator assetToUsdAggregator; + IChainlinkAggregator baseToUsdAggregator; + string adapterDescription; + int256 priceCapRatio; + uint8 ratioDecimals; + } + + /** + * @dev Emitted when the price cap ratio gets updated + * @param priceCapRatio the new price cap ratio + **/ + event PriceCapRatioUpdated(int256 priceCapRatio); + + /** + * @notice Price feed for (ASSET / USD) pair + */ + function ASSET_TO_USD_AGGREGATOR() external view returns (IChainlinkAggregator); + + /** + * @notice Price feed for (BASE / USD) pair + */ + function BASE_TO_USD_AGGREGATOR() external view returns (IChainlinkAggregator); + + /** + * @notice Number of decimals of the priceCap ratio + */ + function RATIO_DECIMALS() external view returns (uint8); + + /** + * @notice ACL manager contract + */ + function ACL_MANAGER() external view returns (IACLManager); + + /** + * @notice Updates price cap ratio + * @param priceCapRatio the new price cap ratio + */ + function setPriceCapRatio(int256 priceCapRatio) external; + + /** + * @notice Get price cap ratio value + */ + function getPriceCapRatio() external view returns (int256); + + /** + * @notice Returns if the price is currently capped + */ + function isCapped() external view returns (bool); + + error ACLManagerIsZeroAddress(); + error CallerIsNotRiskOrPoolAdmin(); + error CapLowerThanActualPrice(); +} diff --git a/tests/base/EURCPriceCapAdapter.t.sol b/tests/base/EURCPriceCapAdapter.t.sol new file mode 100644 index 0000000..9a89445 --- /dev/null +++ b/tests/base/EURCPriceCapAdapter.t.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import '../BaseStableTest.sol'; +import {CapAdaptersCodeBase} from '../../scripts/DeployBase.s.sol'; + +contract EURCBasePriceCapAdapterTest is BaseStableTest { + constructor() + BaseStableTest( + CapAdaptersCodeBase.EURCAdapterCode(), + 10, + ForkParams({network: 'base', blockNumber: 26853575}) + ) + {} +}