diff --git a/.github/workflows/check-anvil-state.yml b/.github/workflows/check-anvil-state.yml new file mode 100644 index 000000000..663946be1 --- /dev/null +++ b/.github/workflows/check-anvil-state.yml @@ -0,0 +1,58 @@ +name: Check anvil state +on: + push: + branches: + - dev + pull_request: + merge_group: + +jobs: + generate-anvil-state: + name: generate anvil state + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Generate anvil state + run: make deploy-contracts-to-anvil-and-save-state + + check-anvil-state: + name: Check anvil dump is up-to-date + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # This step is needed to know if the contracts were changed. + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + contracts: + - 'contracts/lib/**' + - 'contracts/script/**' + - 'contracts/src/**' + anvil_state: + - 'contracts/anvil/contracts-deployed-anvil-state.json' + + # This step runs only if some contract changed. + # It checks whether the anvil state has changed. + # If there are no changes, then the anvil state is outdated + # and therefore this step will fail. + - name: Check the anvil state is up-to-date + if: steps.filter.outputs.contracts == 'true' + run: | + ANVIL_STATE_UPDATED=${{ steps.filter.outputs.anvil_state }} + if [[ "$ANVIL_STATE_UPDATED" == "false" ]]; then + echo "The anvil state is outdated"; + exit 1 + fi diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml new file mode 100644 index 000000000..ba932e2f8 --- /dev/null +++ b/.github/workflows/contracts.yml @@ -0,0 +1,40 @@ +name: Contracts CI + +on: + push: + branches: [main] + pull_request: + branches: [ '**' ] + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./contracts + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: stable + + - name: Show Forge version + run: forge --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build diff --git a/chainio/clients/avsregistry/reader.go b/chainio/clients/avsregistry/reader.go index 382cb27ba..c714364a3 100644 --- a/chainio/clients/avsregistry/reader.go +++ b/chainio/clients/avsregistry/reader.go @@ -30,6 +30,8 @@ type Config struct { OperatorStateRetrieverAddress common.Address } +// The ChainReader provides methods to call the +// AVS registry contract's view functions. type ChainReader struct { logger logging.Logger blsApkRegistryAddr common.Address @@ -40,6 +42,7 @@ type ChainReader struct { ethClient eth.HttpBackend } +// Creates a new instance of the ChainReader. func NewChainReader( registryCoordinatorAddr common.Address, blsApkRegistryAddr common.Address, @@ -84,6 +87,7 @@ func NewReaderFromConfig( ), nil } +// Returns the total quorum count read from the RegistryCoordinator func (r *ChainReader) GetQuorumCount(opts *bind.CallOpts) (uint8, error) { if r.registryCoordinator == nil { return 0, errors.New("RegistryCoordinator contract not provided") @@ -91,6 +95,8 @@ func (r *ChainReader) GetQuorumCount(opts *bind.CallOpts) (uint8, error) { return r.registryCoordinator.QuorumCount(opts) } +// Returns, for each quorum in `quorumNumbers`, a vector of the operators registered for +// that quorum at the current block, containing each operator's `operatorId` and `stake`. func (r *ChainReader) GetOperatorsStakeInQuorumsAtCurrentBlock( opts *bind.CallOpts, quorumNumbers types.QuorumNums, @@ -130,6 +136,8 @@ func (r *ChainReader) GetOperatorsStakeInQuorumsAtBlock( return operatorStakes, nil } +// Returns, for each quorum in `quorumNumbers`, a vector of the addresses of the +// operators registered for that quorum at the current block. func (r *ChainReader) GetOperatorAddrsInQuorumsAtCurrentBlock( opts *bind.CallOpts, quorumNumbers types.QuorumNums, @@ -137,7 +145,6 @@ func (r *ChainReader) GetOperatorAddrsInQuorumsAtCurrentBlock( if r.operatorStateRetriever == nil { return nil, errors.New("OperatorStateRetriever contract not provided") } - if opts.Context == nil { opts.Context = context.Background() } @@ -169,6 +176,10 @@ func (r *ChainReader) GetOperatorAddrsInQuorumsAtCurrentBlock( } +// Returns a tuple containing +// - An array with the quorum IDs in which the given operator is registered at the given block +// - An array that contains, for each quorum, an array with the address, id and stake +// of each operator registered in that quorum. func (r *ChainReader) GetOperatorsStakeInQuorumsOfOperatorAtBlock( opts *bind.CallOpts, operatorId types.OperatorId, @@ -261,6 +272,8 @@ func (r *ChainReader) GetOperatorStakeInQuorumsOfOperatorAtCurrentBlock( return quorumStakes, nil } +// Returns a struct containing the indices of the quorum members that signed, +// and the ones that didn't. func (r *ChainReader) GetCheckSignaturesIndices( opts *bind.CallOpts, referenceBlockNumber uint32, @@ -293,6 +306,7 @@ func (r *ChainReader) GetCheckSignaturesIndices( return checkSignatureIndices, nil } +// Given an operator address, returns its ID. func (r *ChainReader) GetOperatorId( opts *bind.CallOpts, operatorAddress common.Address, @@ -311,6 +325,7 @@ func (r *ChainReader) GetOperatorId( return operatorId, nil } +// Given an operator ID, returns its address. func (r *ChainReader) GetOperatorFromId( opts *bind.CallOpts, operatorId types.OperatorId, @@ -329,6 +344,8 @@ func (r *ChainReader) GetOperatorFromId( return operatorAddress, nil } +// Returns an array of booleans, where the boolean at index i represents +// whether the operator is registered for the quorum i. func (r *ChainReader) QueryRegistrationDetail( opts *bind.CallOpts, operatorAddress common.Address, @@ -358,6 +375,7 @@ func (r *ChainReader) QueryRegistrationDetail( return quorums, nil } +// Returns true if the operator is registered, false otherwise. func (r *ChainReader) IsOperatorRegistered( opts *bind.CallOpts, operatorAddress common.Address, @@ -376,6 +394,9 @@ func (r *ChainReader) IsOperatorRegistered( return registeredWithAvs, nil } +// Queries existing operators for a particular block range. +// Returns two arrays. The first one contains the addresses +// of the operators, and the second contains their corresponding public keys. func (r *ChainReader) QueryExistingRegisteredOperatorPubKeys( ctx context.Context, startBlock *big.Int, @@ -475,6 +496,9 @@ func (r *ChainReader) QueryExistingRegisteredOperatorPubKeys( return operatorAddresses, operatorPubkeys, nil } +// Queries existing operator sockets for a particular block range. +// Returns a mapping containing operator IDs as keys and their +// corresponding sockets as values. func (r *ChainReader) QueryExistingRegisteredOperatorSockets( ctx context.Context, startBlock *big.Int, diff --git a/chainio/clients/avsregistry/subscriber_test.go b/chainio/clients/avsregistry/subscriber_test.go new file mode 100644 index 000000000..0cdf2aaaf --- /dev/null +++ b/chainio/clients/avsregistry/subscriber_test.go @@ -0,0 +1,76 @@ +package avsregistry_test + +import ( + "context" + "testing" + "time" + + "github.com/Layr-Labs/eigensdk-go/crypto/bls" + "github.com/Layr-Labs/eigensdk-go/testutils" + "github.com/Layr-Labs/eigensdk-go/testutils/testclients" + "github.com/Layr-Labs/eigensdk-go/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSubscriberAvsRegistry(t *testing.T) { + client, _ := testclients.BuildTestClients(t) + chainSubscriber := client.AvsRegistryChainSubscriber + chainWriter := client.AvsRegistryChainWriter + + t.Run("subscribe to new pubkey registrations", func(t *testing.T) { + pubKeyRegistrationsC, event, err := chainSubscriber.SubscribeToNewPubkeyRegistrations() + require.NoError(t, err) + defer event.Unsubscribe() + + // Emit a NewPubkeyRegistration event creating a new operator + keypair, err := bls.NewKeyPairFromString("0x01") + require.NoError(t, err) + + ecdsaPrivateKey, err := crypto.HexToECDSA(testutils.ANVIL_FIRST_PRIVATE_KEY) + require.NoError(t, err) + + quorumNumbers := types.QuorumNums{0} + + receipt, err := chainWriter.RegisterOperator( + context.Background(), + ecdsaPrivateKey, + keypair, + quorumNumbers, + "", + true, + ) + require.NoError(t, err) + require.NotNil(t, receipt) + + select { + case newPubkeyRegistration := <-pubKeyRegistrationsC: + expectedOperator := crypto.PubkeyToAddress(ecdsaPrivateKey.PublicKey) + assert.Equal(t, expectedOperator, newPubkeyRegistration.Operator) + case <-time.After(10 * time.Second): + // Throw an error if the event is not received within 10 seconds, making the test fail + t.Fatal("Timed out waiting for NewPubkeyRegistration event") + } + }) + + t.Run("subscribe to operator socket updates", func(t *testing.T) { + socketC, event, err := chainSubscriber.SubscribeToOperatorSocketUpdates() + require.NoError(t, err) + defer event.Unsubscribe() + + // Emit a SocketUpdate event + socketUpdate := "socket-update" + receipt, err := chainWriter.UpdateSocket(context.Background(), types.Socket(socketUpdate), true) + require.NoError(t, err) + require.NotNil(t, receipt) + + select { + case operatorSocketUpdate := <-socketC: + assert.Equal(t, socketUpdate, operatorSocketUpdate.Socket) + case <-time.After(10 * time.Second): + // Throw an error if the event is not received within 10 seconds, making the test fail + t.Fatal("Timed out waiting for OperatorSocketUpdate event") + } + }) +} diff --git a/chainio/clients/avsregistry/writer.go b/chainio/clients/avsregistry/writer.go index 99e5ec1ac..aa22d12d9 100644 --- a/chainio/clients/avsregistry/writer.go +++ b/chainio/clients/avsregistry/writer.go @@ -35,6 +35,8 @@ type eLReader interface { ) ([32]byte, error) } +// The ChainWriter provides methods to call the +// AVS registry contract's state-changing functions. type ChainWriter struct { serviceManagerAddr gethcommon.Address registryCoordinator *regcoord.ContractRegistryCoordinator @@ -47,6 +49,7 @@ type ChainWriter struct { txMgr txmgr.TxManager } +// Returns a new instance of ChainWriter. func NewChainWriter( serviceManagerAddr gethcommon.Address, registryCoordinator *regcoord.ContractRegistryCoordinator, @@ -380,6 +383,8 @@ func (w *ChainWriter) UpdateStakesOfEntireOperatorSetForQuorums( } +// Updates the stakes of a the given `operators` for all the quorums. +// On success, returns the receipt of the transaction. func (w *ChainWriter) UpdateStakesOfOperatorSubsetForAllQuorums( ctx context.Context, operators []gethcommon.Address, @@ -408,6 +413,8 @@ func (w *ChainWriter) UpdateStakesOfOperatorSubsetForAllQuorums( return receipt, nil } +// Deregisters the caller from the quorums given by `quorumNumbers`. +// On success, returns the receipt of the transaction. func (w *ChainWriter) DeregisterOperator( ctx context.Context, quorumNumbers types.QuorumNums, @@ -435,6 +442,8 @@ func (w *ChainWriter) DeregisterOperator( return receipt, nil } +// Deregisters an operator from the given operator sets. +// On success, returns the receipt of the transaction. func (w *ChainWriter) DeregisterOperatorOperatorSets( ctx context.Context, operatorSetIds types.OperatorSetIds, @@ -465,6 +474,8 @@ func (w *ChainWriter) DeregisterOperatorOperatorSets( return receipt, nil } +// Updates the socket of the sender (if it is a registered operator). +// On success, returns the receipt of the transaction. func (w *ChainWriter) UpdateSocket( ctx context.Context, socket types.Socket, diff --git a/chainio/clients/elcontracts/writer.go b/chainio/clients/elcontracts/writer.go index b3986696b..6e9f52367 100644 --- a/chainio/clients/elcontracts/writer.go +++ b/chainio/clients/elcontracts/writer.go @@ -35,6 +35,8 @@ type Reader interface { ) (*strategy.ContractIStrategy, erc20.ContractIERC20Methods, gethcommon.Address, error) } +// The ChainWriter provides methods to call the +// EigenLayer core contract's state-changing functions. type ChainWriter struct { delegationManager *delegationmanager.ContractDelegationManager strategyManager *strategymanager.ContractStrategyManager @@ -49,6 +51,7 @@ type ChainWriter struct { txMgr txmgr.TxManager } +// Returns a new instance of ChainWriter. func NewChainWriter( delegationManager *delegationmanager.ContractDelegationManager, strategyManager *strategymanager.ContractStrategyManager, @@ -80,6 +83,7 @@ func NewChainWriter( } } +// Returns a new instance of ChainWriter from a given config. func NewWriterFromConfig( cfg Config, ethClient eth.HttpBackend, @@ -122,6 +126,8 @@ func NewWriterFromConfig( ), nil } +// Registers the caller as an operator in EigenLayer through the +// DelegationManager contract. func (w *ChainWriter) RegisterAsOperator( ctx context.Context, operator types.Operator, @@ -159,6 +165,9 @@ func (w *ChainWriter) RegisterAsOperator( return receipt, nil } +// Updates an operator's stored `delegationApprover` with +// the given `operator.DelegationApproverAddress` by calling +// the `modifyOperatorDetails` function in the DelegationManager contract. func (w *ChainWriter) UpdateOperatorDetails( ctx context.Context, operator types.Operator, @@ -202,6 +211,7 @@ func (w *ChainWriter) UpdateOperatorDetails( return receipt, nil } +// Updates the metadata URI for the given operator. func (w *ChainWriter) UpdateMetadataURI( ctx context.Context, operatorAddress gethcommon.Address, @@ -238,6 +248,8 @@ func (w *ChainWriter) UpdateMetadataURI( return receipt, nil } +// Deposits `amount` of the `strategyAddr` underlying token +// into the strategy given by `strategyAddr`. func (w *ChainWriter) DepositERC20IntoStrategy( ctx context.Context, strategyAddr gethcommon.Address, @@ -290,6 +302,9 @@ func (w *ChainWriter) DepositERC20IntoStrategy( return receipt, nil } +// Sets `claimer` as the claimer for the earner (in this case the +// earner is the caller). That means that `claimer` can call `processClaim` +// on behalf of the earner. func (w *ChainWriter) SetClaimerFor( ctx context.Context, claimer gethcommon.Address, @@ -320,6 +335,8 @@ func (w *ChainWriter) SetClaimerFor( return receipt, nil } +// Processes the given `claim` for rewards. +// The rewards are transferred to the given `recipientAddress`. func (w *ChainWriter) ProcessClaim( ctx context.Context, claim rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim, @@ -351,6 +368,10 @@ func (w *ChainWriter) ProcessClaim( return receipt, nil } +// Sets the split for a specific operator for a specific AVS. +// The caller must be a registered operator. +// The split has to be between 0 and 10000 bips (inclusive). +// The split will be activated after activation delay. func (w *ChainWriter) SetOperatorAVSSplit( ctx context.Context, operator gethcommon.Address, @@ -383,6 +404,10 @@ func (w *ChainWriter) SetOperatorAVSSplit( return receipt, nil } +// Sets the split for a specific operator for Programmatic Incentives. +// The caller must be a registered operator. +// The split has to be between 0 and 10000 bips (inclusive). +// The split will be activated after activation delay. func (w *ChainWriter) SetOperatorPISplit( ctx context.Context, operator gethcommon.Address, @@ -414,6 +439,8 @@ func (w *ChainWriter) SetOperatorPISplit( return receipt, nil } +// Processes the claims given by `claims`. +// The rewards are transferred to the given `recipientAddress`. func (w *ChainWriter) ProcessClaims( ctx context.Context, claims []rewardscoordinator.IRewardsCoordinatorTypesRewardsMerkleClaim, @@ -455,6 +482,9 @@ func (w *ChainWriter) ProcessClaims( return receipt, nil } +// Deregisters an operator from each of the operator sets given by +// `operatorSetIds` for the given AVS, by calling the function +// `deregisterFromOperatorSets` in the AllocationManager. func (w *ChainWriter) ForceDeregisterFromOperatorSets( ctx context.Context, operator gethcommon.Address, @@ -496,6 +526,8 @@ func (w *ChainWriter) ForceDeregisterFromOperatorSets( return receipt, nil } +// Modifies the proportions of slashable stake allocated to an operator set +// from a list of strategies, for the given `operatorAddress`. func (w *ChainWriter) ModifyAllocations( ctx context.Context, operatorAddress gethcommon.Address, @@ -528,6 +560,10 @@ func (w *ChainWriter) ModifyAllocations( return receipt, nil } +// Sets the allocation delay for an operator. +// The allocation delay is the number of blocks between the operator +// allocating a magnitude to an operator set, and the magnitude becoming +// slashable. func (w *ChainWriter) SetAllocationDelay( ctx context.Context, operatorAddress gethcommon.Address, @@ -559,6 +595,9 @@ func (w *ChainWriter) SetAllocationDelay( return receipt, nil } +// Deregister an operator from one or more of the AVS's operator sets. +// If the operator has any slashable stake allocated to the AVS, +// it remains slashable until the deallocation delay has passed. func (w *ChainWriter) DeregisterFromOperatorSets( ctx context.Context, operator gethcommon.Address, @@ -596,6 +635,9 @@ func (w *ChainWriter) DeregisterFromOperatorSets( return receipt, nil } +// Register an operator for one or more operator sets for an AVS. +// If the operator has any stake allocated to these operator sets, +// it immediately becomes slashable. func (w *ChainWriter) RegisterForOperatorSets( ctx context.Context, registryCoordinatorAddr gethcommon.Address, @@ -660,6 +702,8 @@ func (w *ChainWriter) RegisterForOperatorSets( return receipt, nil } +// Removes permission of an appointee for a specific function +// (given by request.selector) on a target contract, given an account address. func (w *ChainWriter) RemovePermission( ctx context.Context, request RemovePermissionRequest, @@ -688,6 +732,7 @@ func (w *ChainWriter) RemovePermission( return receipt, nil } +// Builds a transaction for the PermissionController's removeAppointee function. func (w *ChainWriter) NewRemovePermissionTx( txOpts *bind.TransactOpts, request RemovePermissionRequest, @@ -712,6 +757,7 @@ func (w *ChainWriter) NewRemovePermissionTx( return tx, nil } +// Builds a transaction for the PermissionController's setAppointee function. func (w *ChainWriter) NewSetPermissionTx( txOpts *bind.TransactOpts, request SetPermissionRequest, @@ -736,6 +782,10 @@ func (w *ChainWriter) NewSetPermissionTx( return tx, nil } +// Set an appointee for a given account. +// Only the admin of the account can set an appointee. +// The appointee will be able to call the function given +// by `request.Selector` on the contract given by `request.Target`. func (w *ChainWriter) SetPermission( ctx context.Context, request SetPermissionRequest, @@ -765,6 +815,7 @@ func (w *ChainWriter) SetPermission( return receipt, nil } +// Builds a transaction for the PermissionController's acceptAdmin function. func (w *ChainWriter) NewAcceptAdminTx( txOpts *bind.TransactOpts, request AcceptAdminRequest, @@ -783,6 +834,8 @@ func (w *ChainWriter) NewAcceptAdminTx( return tx, nil } +// Accept a pending admin for the account given by `request.AccountAddress`. +// The sender of the transaction must be the pending admin. func (w *ChainWriter) AcceptAdmin( ctx context.Context, request AcceptAdminRequest, @@ -812,6 +865,7 @@ func (w *ChainWriter) AcceptAdmin( return receipt, nil } +// Builds a transaction for the PermissionController's addPendingAdmin function. func (w *ChainWriter) NewAddPendingAdminTx( txOpts *bind.TransactOpts, request AddPendingAdminRequest, @@ -830,6 +884,9 @@ func (w *ChainWriter) NewAddPendingAdminTx( return tx, nil } +// Set a pending admin. Multiple admins can be set for an account. +// The caller must be an admin. If the account does not have an admin, +// the caller must be the account. func (w *ChainWriter) AddPendingAdmin(ctx context.Context, request AddPendingAdminRequest) (*gethtypes.Receipt, error) { txOpts, err := w.txMgr.GetNoSendTxOpts() if err != nil { @@ -855,6 +912,7 @@ func (w *ChainWriter) AddPendingAdmin(ctx context.Context, request AddPendingAdm return receipt, nil } +// Builds a transaction for the PermissionController's removeAdmin function. func (w *ChainWriter) NewRemoveAdminTx( txOpts *bind.TransactOpts, request RemoveAdminRequest, @@ -873,6 +931,8 @@ func (w *ChainWriter) NewRemoveAdminTx( return tx, nil } +// Removes the admin given by `request.AdminAddress` from the account given +// by `request.AccountAddress`. The sender of the transaction must be an admin. func (w *ChainWriter) RemoveAdmin( ctx context.Context, request RemoveAdminRequest, @@ -902,6 +962,7 @@ func (w *ChainWriter) RemoveAdmin( return receipt, nil } +// Builds a transaction for the PermissionController's removePendingAdmin function. func (w *ChainWriter) NewRemovePendingAdminTx( txOpts *bind.TransactOpts, request RemovePendingAdminRequest, @@ -920,6 +981,8 @@ func (w *ChainWriter) NewRemovePendingAdminTx( return tx, nil } +// Remove pending admin given by `request.AdminAddress` from the account given +// by `request.AccountAddress`. Only the admin of the account can remove a pending admin. func (w *ChainWriter) RemovePendingAdmin( ctx context.Context, request RemovePendingAdminRequest, @@ -950,6 +1013,7 @@ func (w *ChainWriter) RemovePendingAdmin( return receipt, nil } +// Returns the pubkey registration params for the operator given by `operatorAddress`. func getPubkeyRegistrationParams( ethClient bind.ContractBackend, registryCoordinatorAddr, operatorAddress gethcommon.Address, @@ -982,6 +1046,7 @@ func getPubkeyRegistrationParams( return &pubkeyRegParams, nil } +// Returns the ABI encoding of the given registration params. func abiEncodeRegistrationParams( socket string, pubkeyRegistrationParams regcoord.IBLSApkRegistryPubkeyRegistrationParams,