diff --git a/solidity/contracts/avs/ECDSAServiceManagerBase.sol b/solidity/contracts/avs/ECDSAServiceManagerBase.sol index 31c22a3cab..a48d245332 100644 --- a/solidity/contracts/avs/ECDSAServiceManagerBase.sol +++ b/solidity/contracts/avs/ECDSAServiceManagerBase.sol @@ -15,19 +15,19 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {IRemoteChallenger} from "../interfaces/avs/IRemoteChallenger.sol"; +import {IAVSDirectory} from "../interfaces/avs/IAVSDirectory.sol"; +import {ISignatureUtils} from "../interfaces/avs/ISignatureUtils.sol"; +import {ISlasher} from "../interfaces/avs/ISlasher.sol"; +import {IECDSAStakeRegistry} from "../interfaces/avs/IECDSAStakeRegistry.sol"; +import {IServiceManager} from "../interfaces/avs/IServiceManager.sol"; -import {ISignatureUtils} from "@eigenlayer/interfaces/ISignatureUtils.sol"; -import {IAVSDirectory} from "@eigenlayer/interfaces/IAVSDirectory.sol"; -import {ISlasher} from "@eigenlayer/interfaces/ISlasher.sol"; -import {ECDSAStakeRegistry} from "@eigenlayer-middleware/unaudited/ECDSAStakeRegistry.sol"; -import {IServiceManager} from "@eigenlayer-middleware/interfaces/IServiceManager.sol"; import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable { // ============ Constants ============ // Stake registry responsible for maintaining operator stakes - ECDSAStakeRegistry internal immutable stakeRegistry; + IECDSAStakeRegistry internal immutable stakeRegistry; // Eigenlayer's AVS directory for interactions between AVS and operators IAVSDirectory internal immutable elAvsDirectory; @@ -76,7 +76,7 @@ contract ECDSAServiceManagerBase is IServiceManager, OwnableUpgradeable { constructor( IAVSDirectory _avsDirectory, - ECDSAStakeRegistry _stakeRegistry, + IECDSAStakeRegistry _stakeRegistry, ISlasher _slasher ) { elAvsDirectory = _avsDirectory; diff --git a/solidity/contracts/avs/HyperlaneServiceManager.sol b/solidity/contracts/avs/HyperlaneServiceManager.sol index 6f2e6a0110..f316736105 100644 --- a/solidity/contracts/avs/HyperlaneServiceManager.sol +++ b/solidity/contracts/avs/HyperlaneServiceManager.sol @@ -17,13 +17,13 @@ import "forge-std/console.sol"; // ============ Internal Imports ============ import {Enrollment, EnrollmentStatus, EnumerableMapEnrollment} from "../libs/EnumerableMapEnrollment.sol"; +import {IAVSDirectory} from "../interfaces/avs/IAVSDirectory.sol"; import {IRemoteChallenger} from "../interfaces/avs/IRemoteChallenger.sol"; import {ECDSAServiceManagerBase} from "./ECDSAServiceManagerBase.sol"; // ============ External Imports ============ -import {IAVSDirectory} from "@eigenlayer/interfaces/IAVSDirectory.sol"; -import {ISlasher} from "@eigenlayer/interfaces/ISlasher.sol"; -import {ECDSAStakeRegistry} from "@eigenlayer-middleware/unaudited/ECDSAStakeRegistry.sol"; +import {ISlasher} from "../interfaces/avs/ISlasher.sol"; +import {IECDSAStakeRegistry} from "../interfaces/avs/IECDSAStakeRegistry.sol"; contract HyperlaneServiceManager is ECDSAServiceManagerBase { // ============ Libraries ============ @@ -90,7 +90,7 @@ contract HyperlaneServiceManager is ECDSAServiceManagerBase { constructor( IAVSDirectory _avsDirectory, - ECDSAStakeRegistry _stakeRegistry, + IECDSAStakeRegistry _stakeRegistry, ISlasher _slasher ) ECDSAServiceManagerBase(_avsDirectory, _stakeRegistry, _slasher) {} diff --git a/solidity/contracts/interfaces/avs/IAVSDirectory.sol b/solidity/contracts/interfaces/avs/IAVSDirectory.sol new file mode 100644 index 0000000000..a41860db33 --- /dev/null +++ b/solidity/contracts/interfaces/avs/IAVSDirectory.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import "./ISignatureUtils.sol"; + +interface IAVSDirectory is ISignatureUtils { + /// @notice Enum representing the status of an operator's registration with an AVS + enum OperatorAVSRegistrationStatus { + UNREGISTERED, // Operator not registered to AVS + REGISTERED // Operator registered to AVS + } + + /** + * @notice Called by an avs to register an operator with the avs. + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + /** + * @notice Called by an avs to deregister an operator with the avs. + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external; + + /** + * @notice Called by an AVS to emit an `AVSMetadataURIUpdated` event indicating the information has updated. + * @param metadataURI The URI for metadata associated with an AVS + * @dev Note that the `metadataURI` is *never stored * and is only emitted in the `AVSMetadataURIUpdated` event + */ + function updateAVSMetadataURI(string calldata metadataURI) external; +} diff --git a/solidity/contracts/interfaces/avs/IECDSAStakeRegistry.sol b/solidity/contracts/interfaces/avs/IECDSAStakeRegistry.sol new file mode 100644 index 0000000000..45bee96847 --- /dev/null +++ b/solidity/contracts/interfaces/avs/IECDSAStakeRegistry.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {IStrategy} from "./IStrategy.sol"; + +struct StrategyParams { + IStrategy strategy; // The strategy contract reference + uint96 multiplier; // The multiplier applied to the strategy +} + +struct Quorum { + StrategyParams[] strategies; // An array of strategy parameters to define the quorum +} + +interface IECDSAStakeRegistry { + function quorum() external view returns (Quorum memory); +} diff --git a/solidity/contracts/interfaces/avs/IServiceManager.sol b/solidity/contracts/interfaces/avs/IServiceManager.sol new file mode 100644 index 0000000000..cf19f0eb02 --- /dev/null +++ b/solidity/contracts/interfaces/avs/IServiceManager.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import {ISignatureUtils} from "./ISignatureUtils.sol"; + +/** + * @title Minimal interface for a ServiceManager-type contract that forms the single point for an AVS to push updates to EigenLayer + * @author Layr Labs, Inc. + */ +interface IServiceManager { + /** + * @notice Updates the metadata URI for the AVS + * @param _metadataURI is the metadata URI for the AVS + */ + function updateAVSMetadataURI(string memory _metadataURI) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator registration with the AVS + * @param operator The address of the operator to register. + * @param operatorSignature The signature, salt, and expiry of the operator's signature. + */ + function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature + ) external; + + /** + * @notice Forwards a call to EigenLayer's DelegationManager contract to confirm operator deregistration from the AVS + * @param operator The address of the operator to deregister. + */ + function deregisterOperatorFromAVS(address operator) external; + + /** + * @notice Returns the list of strategies that the operator has potentially restaked on the AVS + * @param operator The address of the operator to get restaked strategies for + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on whether the operator has shares for a strategy in a quorum or uniqueness + * of each element in the returned array. The off-chain service should do that validation separately + */ + function getOperatorRestakedStrategies( + address operator + ) external view returns (address[] memory); + + /** + * @notice Returns the list of strategies that the AVS supports for restaking + * @dev This function is intended to be called off-chain + * @dev No guarantee is made on uniqueness of each element in the returned array. + * The off-chain service should do that validation separately + */ + function getRestakeableStrategies() + external + view + returns (address[] memory); + + /// @notice Returns the EigenLayer AVSDirectory contract. + function avsDirectory() external view returns (address); +} diff --git a/solidity/contracts/interfaces/avs/ISignatureUtils.sol b/solidity/contracts/interfaces/avs/ISignatureUtils.sol new file mode 100644 index 0000000000..158b325d17 --- /dev/null +++ b/solidity/contracts/interfaces/avs/ISignatureUtils.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title The interface for common signature utilities. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + */ +interface ISignatureUtils { + // @notice Struct that bundles together a signature and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } + + // @notice Struct that bundles together a signature, a salt for uniqueness, and an expiration time for the signature. Used primarily for stack management. + struct SignatureWithSaltAndExpiry { + // the signature itself, formatted as a single bytes object + bytes signature; + // the salt used to generate the signature + bytes32 salt; + // the expiration timestamp (UTC) of the signature + uint256 expiry; + } +} diff --git a/solidity/contracts/interfaces/avs/ISlasher.sol b/solidity/contracts/interfaces/avs/ISlasher.sol new file mode 100644 index 0000000000..ccff2786fe --- /dev/null +++ b/solidity/contracts/interfaces/avs/ISlasher.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +/** + * @title Interface for the primary 'slashing' contract for EigenLayer. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice See the `Slasher` contract itself for implementation details. + */ +interface ISlasher { + /** + * @notice Used for 'slashing' a certain operator. + * @param toBeFrozen The operator to be frozen. + * @dev Technically the operator is 'frozen' (hence the name of this function), and then subject to slashing pending a decision by a human-in-the-loop. + * @dev The operator must have previously given the caller (which should be a contract) the ability to slash them, through a call to `optIntoSlashing`. + */ + function freezeOperator(address toBeFrozen) external; +} diff --git a/solidity/contracts/interfaces/avs/IStrategy.sol b/solidity/contracts/interfaces/avs/IStrategy.sol new file mode 100644 index 0000000000..3bcc517735 --- /dev/null +++ b/solidity/contracts/interfaces/avs/IStrategy.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity >=0.5.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title Minimal interface for an `Strategy` contract. + * @author Layr Labs, Inc. + * @notice Terms of Service: https://docs.eigenlayer.xyz/overview/terms-of-service + * @notice Custom `Strategy` implementations may expand extensively on this interface. + */ +interface IStrategy { + /** + * @notice Used to deposit tokens into this Strategy + * @param token is the ERC20 token being deposited + * @param amount is the amount of token being deposited + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * `depositIntoStrategy` function, and individual share balances are recorded in the strategyManager as well. + * @return newShares is the number of new shares issued at the current exchange ratio. + */ + function deposit(IERC20 token, uint256 amount) external returns (uint256); + + /** + * @notice Used to withdraw tokens from this Strategy, to the `recipient`'s address + * @param recipient is the address to receive the withdrawn funds + * @param token is the ERC20 token being transferred out + * @param amountShares is the amount of shares being withdrawn + * @dev This function is only callable by the strategyManager contract. It is invoked inside of the strategyManager's + * other functions, and individual share balances are recorded in the strategyManager as well. + */ + function withdraw( + address recipient, + IERC20 token, + uint256 amountShares + ) external; + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlying( + uint256 amountShares + ) external returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToSharesView`, this function **may** make state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of underlying tokens corresponding to the input `amountShares` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToShares( + uint256 amountUnderlying + ) external returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlyingView`, this function **may** make state modifications + */ + function userUnderlying(address user) external returns (uint256); + + /** + * @notice convenience function for fetching the current total shares of `user` in this strategy, by + * querying the `strategyManager` contract + */ + function shares(address user) external view returns (uint256); + + /** + * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy. + * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications + * @param amountShares is the amount of shares to calculate its conversion into the underlying token + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function sharesToUnderlyingView( + uint256 amountShares + ) external view returns (uint256); + + /** + * @notice Used to convert an amount of underlying tokens to the equivalent amount of shares in this strategy. + * @notice In contrast to `underlyingToShares`, this function guarantees no state modifications + * @param amountUnderlying is the amount of `underlyingToken` to calculate its conversion into strategy shares + * @return The amount of shares corresponding to the input `amountUnderlying` + * @dev Implementation for these functions in particular may vary significantly for different strategies + */ + function underlyingToSharesView( + uint256 amountUnderlying + ) external view returns (uint256); + + /** + * @notice convenience function for fetching the current underlying value of all of the `user`'s shares in + * this strategy. In contrast to `userUnderlying`, this function guarantees no state modifications + */ + function userUnderlyingView(address user) external view returns (uint256); + + /// @notice The underlying token for shares in this Strategy + function underlyingToken() external view returns (IERC20); + + /// @notice The total number of extant shares in this Strategy + function totalShares() external view returns (uint256); + + /// @notice Returns either a brief string explaining the strategy's goal & purpose, or a link to metadata that explains in more detail. + function explanation() external view returns (string memory); +} diff --git a/solidity/test/avs/HyperlaneServiceManager.t.sol b/solidity/test/avs/HyperlaneServiceManager.t.sol index 31e5e02be5..6af4d24943 100644 --- a/solidity/test/avs/HyperlaneServiceManager.t.sol +++ b/solidity/test/avs/HyperlaneServiceManager.t.sol @@ -2,8 +2,7 @@ pragma solidity >=0.8.0; import {DelegationManager} from "@eigenlayer/core/DelegationManager.sol"; -import {ISignatureUtils} from "@eigenlayer/interfaces/ISignatureUtils.sol"; -import {IAVSDirectory} from "@eigenlayer/interfaces/IAVSDirectory.sol"; + import {IDelegationManager} from "@eigenlayer/interfaces/IDelegationManager.sol"; import {IStrategy} from "@eigenlayer/interfaces/IStrategy.sol"; import {ISlasher} from "@eigenlayer/interfaces/ISlasher.sol"; @@ -12,8 +11,10 @@ import {MockAVSDeployer} from "eigenlayer-middleware/test/utils/MockAVSDeployer. import {Quorum, StrategyParams} from "@eigenlayer-middleware/unaudited/ECDSAStakeRegistryStorage.sol"; import {ECDSAStakeRegistry} from "@eigenlayer-middleware/unaudited/ECDSAStakeRegistry.sol"; +import {ISignatureUtils} from "../../contracts/interfaces/avs/ISignatureUtils.sol"; import {Enrollment, EnrollmentStatus} from "../../contracts/libs/EnumerableMapEnrollment.sol"; import {IRemoteChallenger} from "../../contracts/interfaces/avs/IRemoteChallenger.sol"; +import {IAVSDirectory} from "../../contracts/interfaces/avs/IAVSDirectory.sol"; import {HyperlaneServiceManager} from "../../contracts/avs/HyperlaneServiceManager.sol"; import {TestRemoteChallenger} from "../../contracts/test/TestRemoteChallenger.sol";