diff --git a/lib/contracts/abis/mainnet/InstitutionalVault.ts b/lib/contracts/abis/mainnet/InstitutionalVault.ts index 4b9c3cc..53d76e0 100644 --- a/lib/contracts/abis/mainnet/InstitutionalVault.ts +++ b/lib/contracts/abis/mainnet/InstitutionalVault.ts @@ -34,6 +34,13 @@ export const InstitutionalVault = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'NON_RESTORAKING_WITHDRAWAL_CREDENTIALS_FACTORY', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'UPGRADE_INTERFACE_VERSION', @@ -176,6 +183,20 @@ export const InstitutionalVault = [ stateMutability: 'view', type: 'function', }, + { + inputs: [], + name: 'getEigenPodWithdrawalCredentials', + outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getNoRestakingWithdrawalCredentials', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, { inputs: [], name: 'getNonRestakedValidatorETH', @@ -210,9 +231,9 @@ export const InstitutionalVault = [ }, { inputs: [], - name: 'isConsumingScheduledOp', - outputs: [{ internalType: 'bytes4', name: '', type: 'bytes4' }], - stateMutability: 'view', + name: 'initializerV2', + outputs: [], + stateMutability: 'nonpayable', type: 'function', }, { @@ -288,13 +309,6 @@ export const InstitutionalVault = [ stateMutability: 'view', type: 'function', }, - { - inputs: [], - name: 'proxiableUUID', - outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], - stateMutability: 'view', - type: 'function', - }, { inputs: [{ internalType: 'uint256', name: 'shareAmount', type: 'uint256' }], name: 'queueWithdrawals', @@ -315,11 +329,19 @@ export const InstitutionalVault = [ }, { inputs: [ - { internalType: 'address', name: 'newAuthority', type: 'address' }, + { + components: [ + { internalType: 'bytes', name: 'pubkey', type: 'bytes' }, + { internalType: 'uint64', name: 'amountGwei', type: 'uint64' }, + ], + internalType: 'struct IEigenPodTypes.WithdrawalRequest[]', + name: 'requests', + type: 'tuple[]', + }, ], - name: 'setAuthority', + name: 'requestWithdrawal', outputs: [], - stateMutability: 'nonpayable', + stateMutability: 'payable', type: 'function', }, { @@ -435,7 +457,13 @@ export const InstitutionalVault = [ stateMutability: 'nonpayable', type: 'function', }, - { stateMutability: 'payable', type: 'receive' }, + { + inputs: [], + name: 'withdrawNonRestakedETH', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { internalType: 'bytes[]', name: 'srcPubkeys', type: 'bytes[]' }, diff --git a/lib/contracts/abis/mainnet/NonRestakingWithdrawalCredentials.ts b/lib/contracts/abis/mainnet/NonRestakingWithdrawalCredentials.ts new file mode 100644 index 0000000..7b5927b --- /dev/null +++ b/lib/contracts/abis/mainnet/NonRestakingWithdrawalCredentials.ts @@ -0,0 +1,71 @@ +export const NonRestakingWithdrawalCredentials = [ + { + inputs: [], + name: 'authority', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getConsolidationRequestFee', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getWithdrawalRequestFee', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes', name: 'srcPubkey', type: 'bytes' }, + { internalType: 'bytes', name: 'targetPubkey', type: 'bytes' }, + ], + internalType: 'struct IEigenPodTypes.ConsolidationRequest[]', + name: 'requests', + type: 'tuple[]', + }, + ], + name: 'requestConsolidation', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { + components: [ + { internalType: 'bytes', name: 'pubkey', type: 'bytes' }, + { internalType: 'uint64', name: 'amountGwei', type: 'uint64' }, + ], + internalType: 'struct IEigenPodTypes.WithdrawalRequest[]', + name: 'requests', + type: 'tuple[]', + }, + ], + name: 'requestWithdrawal', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'vault', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'withdrawETH', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +]; diff --git a/lib/contracts/handlers/__tests__/institutional-vault-handler.test.ts b/lib/contracts/handlers/__tests__/institutional-vault-handler.test.ts index c5a1169..8d42fa6 100644 --- a/lib/contracts/handlers/__tests__/institutional-vault-handler.test.ts +++ b/lib/contracts/handlers/__tests__/institutional-vault-handler.test.ts @@ -73,6 +73,26 @@ describe('InstitutionalVaultHandler', () => { expect(result.toLowerCase()).toBe(address); }); + it('should get the non-restaking withdrawal credentials factory', async () => { + const address = generateAddress(); + contractTestingUtils.mockCall( + 'NON_RESTORAKING_WITHDRAWAL_CREDENTIALS_FACTORY', + [address], + ); + + const result = + await handler.getNonRestakingWithdrawalCredentialsFactory(); + expect(result.toLowerCase()).toBe(address); + }); + + it('should get the WETH contract address', async () => { + const address = generateAddress(); + contractTestingUtils.mockCall('WETH', [address]); + + const result = await handler.getWeth(); + expect(result.toLowerCase()).toBe(address); + }); + it('should get the eigen pod', async () => { const address = generateAddress(); contractTestingUtils.mockCall('getEigenPod', [address]); @@ -81,6 +101,27 @@ describe('InstitutionalVaultHandler', () => { expect(result.toLowerCase()).toBe(address); }); + it('should get the eigen pod withdrawal credentials', async () => { + const credentials = + '0x010000000000000000000000abcdef1234567890abcdef1234567890abcdef12'; + contractTestingUtils.mockCall('getEigenPodWithdrawalCredentials', [ + credentials, + ]); + + const result = await handler.getEigenPodWithdrawalCredentials(); + expect(result.toLowerCase()).toBe(credentials); + }); + + it('should get the no-restaking withdrawal credentials', async () => { + const address = generateAddress(); + contractTestingUtils.mockCall('getNoRestakingWithdrawalCredentials', [ + address, + ]); + + const result = await handler.getNoRestakingWithdrawalCredentials(); + expect(result.toLowerCase()).toBe(address); + }); + it('should get the allowance', async () => { const owner = generateAddress(); const spender = generateAddress(); @@ -336,6 +377,21 @@ describe('InstitutionalVaultHandler', () => { expect(isHash(txHash)).toBeTruthy(); }); + it('should request withdrawal from the EigenPod', async () => { + const requests = [ + { + pubkey: + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as `0x${string}`, + amountGwei: BigInt(32_000_000_000), + }, + ]; + const value = 1_000_000_000n; // 1 gwei + contractTestingUtils.mockTransaction('requestWithdrawal'); + + const txHash = await handler.requestWithdrawal(requests, value); + expect(isHash(txHash)).toBeTruthy(); + }); + it('should redeem shares to the receiver', async () => { const shares = 10n; const receiver = generateAddress(); @@ -442,5 +498,12 @@ describe('InstitutionalVaultHandler', () => { ); expect(isHash(txHash)).toBeTruthy(); }); + + it('should withdraw non-restaked ETH', async () => { + contractTestingUtils.mockTransaction('withdrawNonRestakedETH'); + + const txHash = await handler.withdrawNonRestakedETH(); + expect(isHash(txHash)).toBeTruthy(); + }); }); }); diff --git a/lib/contracts/handlers/__tests__/non-restaking-withdrawal-credentials-handler.test.ts b/lib/contracts/handlers/__tests__/non-restaking-withdrawal-credentials-handler.test.ts new file mode 100644 index 0000000..dcc7df7 --- /dev/null +++ b/lib/contracts/handlers/__tests__/non-restaking-withdrawal-credentials-handler.test.ts @@ -0,0 +1,121 @@ +import { WalletClient, PublicClient, isHash } from 'viem'; +import { + setupTestWalletClient, + setupTestPublicClient, +} from '../../../../test/setup-test-clients'; +import { mockAccount, testingUtils } from '../../../../test/setup-tests'; +import { NonRestakingWithdrawalCredentials } from '../../abis/mainnet/NonRestakingWithdrawalCredentials'; +import { NonRestakingWithdrawalCredentialsHandler } from '../non-restaking-withdrawal-credentials-handler'; +import { generateAddress } from '../../../../test/mocks/address'; +import { InvalidContractAddressError } from '../../../errors/validation-errors'; +import { Chain } from '../../../chains/constants'; + +describe('NonRestakingWithdrawalCredentialsHandler', () => { + const contractTestingUtils = testingUtils.generateContractUtils( + NonRestakingWithdrawalCredentials, + ); + let handler: NonRestakingWithdrawalCredentialsHandler; + let walletClient: WalletClient; + let publicClient: PublicClient; + + beforeEach(() => { + walletClient = setupTestWalletClient(Chain.Holesky, undefined, mockAccount); + publicClient = setupTestPublicClient(); + + handler = new NonRestakingWithdrawalCredentialsHandler( + Chain.Holesky, + walletClient, + publicClient, + ); + }); + + it('should set and get the address used by the handler', () => { + const address = generateAddress(); + handler.withAddress(address); + + expect(handler.getAddress()).toBe(address); + }); + + it('should throw an error if the address is not set and a method is called', () => { + const address = generateAddress(); + contractTestingUtils.mockCall('authority', [address]); + + expect(() => handler.authority()).toThrow(InvalidContractAddressError); + }); + + describe('with address set', () => { + beforeEach(() => { + handler.withAddress(generateAddress()); + }); + + it('should get the authority', async () => { + const address = generateAddress(); + contractTestingUtils.mockCall('authority', [address]); + + const result = await handler.authority(); + expect(result.toLowerCase()).toBe(address); + }); + + it('should get the consolidation request fee', async () => { + const fee = 1000000000n; // 1 gwei + contractTestingUtils.mockCall('getConsolidationRequestFee', [fee]); + + const result = await handler.getConsolidationRequestFee(); + expect(result).toBe(fee); + }); + + it('should get the withdrawal request fee', async () => { + const fee = 1000000000n; // 1 gwei + contractTestingUtils.mockCall('getWithdrawalRequestFee', [fee]); + + const result = await handler.getWithdrawalRequestFee(); + expect(result).toBe(fee); + }); + + it('should get the vault address', async () => { + const address = generateAddress(); + contractTestingUtils.mockCall('vault', [address]); + + const result = await handler.vault(); + expect(result.toLowerCase()).toBe(address); + }); + + it('should request consolidation', async () => { + const requests = [ + { + srcPubkey: + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as `0x${string}`, + targetPubkey: + '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678' as `0x${string}`, + }, + ]; + const value = 1000000000n; // 1 gwei + contractTestingUtils.mockTransaction('requestConsolidation'); + + const txHash = await handler.requestConsolidation(requests, value); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should request withdrawal', async () => { + const requests = [ + { + pubkey: + '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' as `0x${string}`, + amountGwei: BigInt(32_000_000_000), + }, + ]; + const value = 1000000000n; // 1 gwei + contractTestingUtils.mockTransaction('requestWithdrawal'); + + const txHash = await handler.requestWithdrawal(requests, value); + expect(isHash(txHash)).toBeTruthy(); + }); + + it('should withdraw ETH', async () => { + contractTestingUtils.mockTransaction('withdrawETH'); + + const txHash = await handler.withdrawETH(); + expect(isHash(txHash)).toBeTruthy(); + }); + }); +}); diff --git a/lib/contracts/handlers/institutional-vault-handler.ts b/lib/contracts/handlers/institutional-vault-handler.ts index 8b5100c..82642ec 100644 --- a/lib/contracts/handlers/institutional-vault-handler.ts +++ b/lib/contracts/handlers/institutional-vault-handler.ts @@ -21,6 +21,11 @@ export type QueuedWithdrawal = { scaledShares: bigint[]; }; +export type WithdrawalRequest = { + pubkey: Hex; + amountGwei: bigint; +}; + /** * Handler for the `InstitutionalVault` contract exposing methods to * interact with the contract. @@ -103,6 +108,24 @@ export class InstitutionalVaultHandler { return this.getContract().read.BEACON_DEPOSIT_CONTRACT(); } + /** + * Get the non-restaking withdrawal credentials factory address. + * + * @returns The non-restaking withdrawal credentials factory address. + */ + public getNonRestakingWithdrawalCredentialsFactory() { + return this.getContract().read.NON_RESTORAKING_WITHDRAWAL_CREDENTIALS_FACTORY(); + } + + /** + * Get the WETH contract address. + * + * @returns The WETH contract address. + */ + public getWeth() { + return this.getContract().read.WETH(); + } + /** * Get the eigen delegation manager contract address. * @@ -130,6 +153,24 @@ export class InstitutionalVaultHandler { return this.getContract().read.getEigenPod(); } + /** + * Get the eigen pod withdrawal credentials. + * + * @returns The eigen pod withdrawal credentials as bytes. + */ + public getEigenPodWithdrawalCredentials() { + return this.getContract().read.getEigenPodWithdrawalCredentials(); + } + + /** + * Get the no-restaking withdrawal credentials address. + * + * @returns The no-restaking withdrawal credentials address. + */ + public getNoRestakingWithdrawalCredentials() { + return this.getContract().read.getNoRestakingWithdrawalCredentials(); + } + /** * Get the allowance for the spender to spend the owner's tokens. * @@ -398,6 +439,20 @@ export class InstitutionalVaultHandler { }); } + /** + * Request withdrawal from the EigenPod. + * + * @param requests The withdrawal requests containing pubkey and amountGwei. + * @param value The amount of ETH to send with the transaction. + */ + public requestWithdrawal(requests: WithdrawalRequest[], value: bigint) { + return this.getContract().write.requestWithdrawal([requests], { + account: this.walletClient.account!, + chain: this.viemChain, + value, + }); + } + /** * Redeem the shares to the receiver. * @@ -580,4 +635,14 @@ export class InstitutionalVaultHandler { }, ); } + + /** + * Withdraw non-restaked ETH from the vault. + */ + public withdrawNonRestakedETH() { + return this.getContract().write.withdrawNonRestakedETH({ + account: this.walletClient.account!, + chain: this.viemChain, + }); + } } diff --git a/lib/contracts/handlers/non-restaking-withdrawal-credentials-handler.ts b/lib/contracts/handlers/non-restaking-withdrawal-credentials-handler.ts new file mode 100644 index 0000000..c5467c0 --- /dev/null +++ b/lib/contracts/handlers/non-restaking-withdrawal-credentials-handler.ts @@ -0,0 +1,170 @@ +import { + Chain as ViemChain, + WalletClient, + PublicClient, + getContract, + GetContractReturnType, + Address, + Hex, +} from 'viem'; +import { NonRestakingWithdrawalCredentials } from '../abis/mainnet/NonRestakingWithdrawalCredentials'; +import { Chain, VIEM_CHAINS } from '../../chains/constants'; +import { InvalidContractAddressError } from '../../errors/validation-errors'; + +export type ConsolidationRequest = { + srcPubkey: Hex; + targetPubkey: Hex; +}; + +export type WithdrawalRequest = { + pubkey: Hex; + amountGwei: bigint; +}; + +/** + * Handler for the `NonRestakingWithdrawalCredentials` contract exposing methods to + * interact with the contract. + */ +export class NonRestakingWithdrawalCredentialsHandler { + private viemChain: ViemChain; + private address?: Address; + + /** + * Create the handler for the `NonRestakingWithdrawalCredentials` contract exposing + * methods to interact with the contract. + * + * @param chain Chain to use for the client. + * @param walletClient The wallet client to use for wallet + * interactions. + * @param publicClient The public client to use for public + * interactions. + */ + constructor( + chain: Chain, + private walletClient: WalletClient, + private publicClient: PublicClient, + ) { + this.viemChain = VIEM_CHAINS[chain]; + } + + /** + * Set the address of the contract for this handler. + * + * @param address The address of the contract. + * @returns The handler. + */ + public withAddress(address: Address) { + this.address = address; + + return this; + } + + /** + * Get the address of the contract for this handler. + * + * @returns The address of the contract. + */ + public getAddress() { + return this.address; + } + + /** + * Get the contract. This is a method because the typings are complex + * and lost when trying to make it a member. + * + * @returns The viem contract. + */ + public getContract() { + if (!this.address) { + throw new InvalidContractAddressError( + `No address specified for the contract`, + { + fixMessage: `Set the contract address in the handler by using the setAddress method`, + }, + ); + } + + const abi = NonRestakingWithdrawalCredentials; + const client = { public: this.publicClient, wallet: this.walletClient }; + + return getContract({ + address: this.address, + abi, + client, + }) as GetContractReturnType; + } + + /** + * Get the authority of the contract. + * + * @returns The authority address. + */ + public authority() { + return this.getContract().read.authority(); + } + + /** + * Get the consolidation request fee. + * + * @returns The consolidation request fee. + */ + public getConsolidationRequestFee() { + return this.getContract().read.getConsolidationRequestFee(); + } + + /** + * Get the withdrawal request fee. + * + * @returns The withdrawal request fee. + */ + public getWithdrawalRequestFee() { + return this.getContract().read.getWithdrawalRequestFee(); + } + + /** + * Get the vault address. + * + * @returns The vault address. + */ + public vault() { + return this.getContract().read.vault(); + } + + /** + * Request consolidation of validators. + * + * @param requests The consolidation requests containing srcPubkey and targetPubkey. + * @param value The amount of ETH to send with the transaction for fees. + */ + public requestConsolidation(requests: ConsolidationRequest[], value: bigint) { + return this.getContract().write.requestConsolidation([requests], { + account: this.walletClient.account!, + chain: this.viemChain, + value, + }); + } + + /** + * Request withdrawal from validators. + * + * @param requests The withdrawal requests containing pubkey and amountGwei. + * @param value The amount of ETH to send with the transaction for fees. + */ + public requestWithdrawal(requests: WithdrawalRequest[], value: bigint) { + return this.getContract().write.requestWithdrawal([requests], { + account: this.walletClient.account!, + chain: this.viemChain, + value, + }); + } + + /** + * Withdraw ETH from the contract. + */ + public withdrawETH() { + return this.getContract().write.withdrawETH({ + account: this.walletClient.account!, + chain: this.viemChain, + }); + } +}