From 54952381678cd3944024bffc31da2811224d150a Mon Sep 17 00:00:00 2001 From: Nick Guest Date: Mon, 16 Sep 2024 15:47:50 +0200 Subject: [PATCH 1/6] Add PufferWithdrawalManager and PufferWithdrawalsManager abis --- .../abis/mainnet/PufferWithdrawalManager.ts | 661 ++++++++++++++++++ .../abis/puffer-withdrawal-manager-abis.ts | 7 + lib/contracts/addresses.ts | 1 + .../handlers/puffer-withdrawal-manager.ts | 128 ++++ 4 files changed, 797 insertions(+) create mode 100644 lib/contracts/abis/mainnet/PufferWithdrawalManager.ts create mode 100644 lib/contracts/abis/puffer-withdrawal-manager-abis.ts create mode 100644 lib/contracts/handlers/puffer-withdrawal-manager.ts diff --git a/lib/contracts/abis/mainnet/PufferWithdrawalManager.ts b/lib/contracts/abis/mainnet/PufferWithdrawalManager.ts new file mode 100644 index 0000000..4a7c3e6 --- /dev/null +++ b/lib/contracts/abis/mainnet/PufferWithdrawalManager.ts @@ -0,0 +1,661 @@ +export const PufferWithdrawalManager = [ + { + type: 'constructor', + inputs: [ + { + name: 'batchSize', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'pufferVault', + type: 'address', + internalType: 'contract PufferVaultV3', + }, + { + name: 'weth', + type: 'address', + internalType: 'contract IWETH', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'receive', + stateMutability: 'payable', + }, + { + type: 'function', + name: 'BATCH_SIZE', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'MIN_WITHDRAWAL_AMOUNT', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'PUFFER_VAULT', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'contract PufferVaultV3', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'UPGRADE_INTERFACE_VERSION', + inputs: [], + outputs: [ + { + name: '', + type: 'string', + internalType: 'string', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'WETH', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'contract IWETH', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'authority', + inputs: [], + outputs: [ + { + name: '', + type: 'address', + internalType: 'address', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'changeMaxWithdrawalAmount', + inputs: [ + { + name: 'newMaxWithdrawalAmount', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'completeQueuedWithdrawal', + inputs: [ + { + name: 'withdrawalIdx', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'finalizeWithdrawals', + inputs: [ + { + name: 'withdrawalBatchIndex', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'getFinalizedWithdrawalBatch', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getMaxWithdrawalAmount', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getWithdrawal', + inputs: [ + { + name: 'withdrawalIdx', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [ + { + name: '', + type: 'tuple', + internalType: 'struct PufferWithdrawalManagerStorage.Withdrawal', + components: [ + { + name: 'pufETHAmount', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'pufETHToETHExchangeRate', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'recipient', + type: 'address', + internalType: 'address', + }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'getWithdrawalsLength', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'initialize', + inputs: [ + { + name: 'accessManager', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'isConsumingScheduledOp', + inputs: [], + outputs: [ + { + name: '', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'proxiableUUID', + inputs: [], + outputs: [ + { + name: '', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'requestWithdrawal', + inputs: [ + { + name: 'pufETHAmount', + type: 'uint128', + internalType: 'uint128', + }, + { + name: 'recipient', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'requestWithdrawalWithPermit', + inputs: [ + { + name: 'permitData', + type: 'tuple', + internalType: 'struct Permit', + components: [ + { + name: 'deadline', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'v', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'r', + type: 'bytes32', + internalType: 'bytes32', + }, + { + name: 's', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + }, + { + name: 'recipient', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'setAuthority', + inputs: [ + { + name: 'newAuthority', + type: 'address', + internalType: 'address', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'upgradeToAndCall', + inputs: [ + { + name: 'newImplementation', + type: 'address', + internalType: 'address', + }, + { + name: 'data', + type: 'bytes', + internalType: 'bytes', + }, + ], + outputs: [], + stateMutability: 'payable', + }, + { + type: 'event', + name: 'AuthorityUpdated', + inputs: [ + { + name: 'authority', + type: 'address', + indexed: false, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'BatchFinalized', + inputs: [ + { + name: 'batchIdx', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { + name: 'expectedETHAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'actualEthAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'pufETHBurnAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Initialized', + inputs: [ + { + name: 'version', + type: 'uint64', + indexed: false, + internalType: 'uint64', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'MaxWithdrawalAmountChanged', + inputs: [ + { + name: 'oldMaxWithdrawalAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'newMaxWithdrawalAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Upgraded', + inputs: [ + { + name: 'implementation', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'WithdrawalCompleted', + inputs: [ + { + name: 'withdrawalIdx', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { + name: 'ethPayoutAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'payoutExchangeRate', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'WithdrawalRequested', + inputs: [ + { + name: 'withdrawalIdx', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { + name: 'batchIdx', + type: 'uint256', + indexed: true, + internalType: 'uint256', + }, + { + name: 'pufETHAmount', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + { + name: 'recipient', + type: 'address', + indexed: true, + internalType: 'address', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AccessManagedInvalidAuthority', + inputs: [ + { + name: 'authority', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'AccessManagedRequiredDelay', + inputs: [ + { + name: 'caller', + type: 'address', + internalType: 'address', + }, + { + name: 'delay', + type: 'uint32', + internalType: 'uint32', + }, + ], + }, + { + type: 'error', + name: 'AccessManagedUnauthorized', + inputs: [ + { + name: 'caller', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'AddressEmptyCode', + inputs: [ + { + name: 'target', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'BatchAlreadyFinalized', + inputs: [ + { + name: 'batchIndex', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { + type: 'error', + name: 'BatchSizeCannotChange', + inputs: [], + }, + { + type: 'error', + name: 'BatchesAreNotFull', + inputs: [], + }, + { + type: 'error', + name: 'ERC1967InvalidImplementation', + inputs: [ + { + name: 'implementation', + type: 'address', + internalType: 'address', + }, + ], + }, + { + type: 'error', + name: 'ERC1967NonPayable', + inputs: [], + }, + { + type: 'error', + name: 'FailedInnerCall', + inputs: [], + }, + { + type: 'error', + name: 'InvalidInitialization', + inputs: [], + }, + { + type: 'error', + name: 'MultipleWithdrawalsAreForbidden', + inputs: [], + }, + { + type: 'error', + name: 'NotFinalized', + inputs: [], + }, + { + type: 'error', + name: 'NotInitializing', + inputs: [], + }, + { + type: 'error', + name: 'SafeCastOverflowedUintDowncast', + inputs: [ + { + name: 'bits', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'value', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { + type: 'error', + name: 'UUPSUnauthorizedCallContext', + inputs: [], + }, + { + type: 'error', + name: 'UUPSUnsupportedProxiableUUID', + inputs: [ + { + name: 'slot', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + }, + { + type: 'error', + name: 'WithdrawalAlreadyCompleted', + inputs: [], + }, + { + type: 'error', + name: 'WithdrawalAmountTooHigh', + inputs: [], + }, + { + type: 'error', + name: 'WithdrawalAmountTooLow', + inputs: [], + }, +]; diff --git a/lib/contracts/abis/puffer-withdrawal-manager-abis.ts b/lib/contracts/abis/puffer-withdrawal-manager-abis.ts new file mode 100644 index 0000000..efdc273 --- /dev/null +++ b/lib/contracts/abis/puffer-withdrawal-manager-abis.ts @@ -0,0 +1,7 @@ +import { Chain } from '../../chains/constants'; +import { PufferWithdrawalManager } from './mainnet/PufferWithdrawalManager'; + +export const PUFFER_WITHDRAWAL_MANAGER_ABIS = { + [Chain.Mainnet]: { PufferWithdrawalManager }, + [Chain.Holesky]: { PufferWithdrawalManager }, +}; diff --git a/lib/contracts/addresses.ts b/lib/contracts/addresses.ts index 5a8d7ae..5719116 100644 --- a/lib/contracts/addresses.ts +++ b/lib/contracts/addresses.ts @@ -8,6 +8,7 @@ export const CONTRACT_ADDRESSES = { PufferDepositor: '0x4aa799c5dfc01ee7d790e3bf1a7c2257ce1dceff', PufferL2Depositor: '0x3436E0B85cd929929F5802e792CFE282166E0259', PufLocker: '0x48e8dE138C246c14248C94d2D616a2F9eb4590D2', + PufferWithdrawalManager: '0x0000000000000000000000000000000000000000', }, [Chain.Holesky]: { PufferVault: '0x9196830bB4c05504E0A8475A0aD566AceEB6BeC9', diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts new file mode 100644 index 0000000..82e7556 --- /dev/null +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -0,0 +1,128 @@ +import { + Address, + getContract, + GetContractReturnType, + PublicClient, + WalletClient, +} from 'viem'; +import { CONTRACT_ADDRESSES } from '../addresses'; +import { Chain, VIEM_CHAINS, ViemChain } from '../../chains/constants'; +import { ERC20PermitHandler } from './erc20-permit-handler'; +import { PUFFER_WITHDRAWAL_MANAGER_ABIS } from '../abis/puffer-withdrawal-manager-abis'; +import { Token } from '../tokens'; + +/** + * Handler for the `PufferWithdrawalsManager` contract exposing methods to + * interact with the contract. + */ +export class PufferWithdrawalHandler { + private viemChain: ViemChain; + private erc20PermitHandler: ERC20PermitHandler; + + /** + * Create the handler for the `PufferWithdrawalsManager` 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( + private chain: Chain, + private walletClient: WalletClient, + private publicClient: PublicClient, + ) { + this.viemChain = VIEM_CHAINS[chain]; + this.erc20PermitHandler = new ERC20PermitHandler( + chain, + walletClient, + publicClient, + ); + } + + /** + * Get the contract. + * + * @returns The viem contract. + */ + private getContract() { + const address = CONTRACT_ADDRESSES[this.chain] + .PufferWithdrawalManager as Address; + const abi = + PUFFER_WITHDRAWAL_MANAGER_ABIS[this.chain].PufferWithdrawalManager; + const client = { public: this.publicClient, wallet: this.walletClient }; + + return getContract({ address, abi, client }) as GetContractReturnType< + typeof abi, + typeof client, + Address + >; + } + + /** + * Request a withdrawal of the given amount to the given address. + * @param walletAddress The account address to request the withdrawal for. + * @param amount The pufETH amount to request the withdrawal for. + * @returns The transaction hash of the withdrawal. + */ + public async requestWithdrawal(walletAddress: Address, amount: bigint) { + return await this.getContract().write.requestWithdrawal([ + amount, + walletAddress, + ]); + } + + /** + * Request a withdrawal of the given amount to the given address. + * @param walletAddress The account address to request the withdrawal for. + * @param amount The pufETHamount to request the withdrawal for. + * @returns The transaction hash of the withdrawal. + */ + public async requestWithdrawalWithPermit( + walletAddress: Address, + amount: bigint, + ) { + const { r, s, v, yParity, deadline } = await this.erc20PermitHandler + .withToken(Token.pufETH) + .getPermitSignature( + walletAddress, + CONTRACT_ADDRESSES[this.chain].PufferWithdrawalManager as Address, + amount, + ); + /* istanbul ignore next */ + const permitData = { + r, + s, + v: Number(v ?? yParity), + deadline, + amount, + }; + // TOOD add permit + return await this.getContract().write.requestWithdrawalWithPermit([ + permitData, + walletAddress, + ]); + } + + /** + * Complete a withdrawal from the queue. + * @param withdrawalIdx The index of the withdrawal to complete. + * @returns The transaction hash of the withdrawal. + */ + public async completeQueueWithdrawal(withdrawalIdx: bigint) { + return await this.getContract().write.completeQueuedWithdrawal([ + withdrawalIdx, + ]); + } + + /** + * Get the withdrawal at the given index. + * @param withdrawalIdx The index of the withdrawal to get. + * @returns The withdrawal at the given index. + */ + public async getWithDrawal(withdrawalIdx: bigint) { + return await this.getContract().read.getWithdrawal([withdrawalIdx]); + } +} From 99f70eda2eafff294cd54de72d34ddb7311cc05b Mon Sep 17 00:00:00 2001 From: Nick Guest Date: Tue, 17 Sep 2024 11:49:37 +0200 Subject: [PATCH 2/6] Add tests for PufferWithdrawalHandler --- .../puffer-withdrawal-manager.test.ts | 137 ++++++++++++++++++ .../handlers/puffer-withdrawal-manager.ts | 45 ++++-- 2 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 lib/contracts/handlers/puffer-withdrawal-manager.test.ts diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts new file mode 100644 index 0000000..7f44e56 --- /dev/null +++ b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts @@ -0,0 +1,137 @@ +import { PufferWithdrawalHandler } from './puffer-withdrawal-manager'; +import { + setupTestPublicClient, + setupTestWalletClient, +} from '../../../test/setup-test-clients'; +import { Chain } from '../../chains/constants'; +import { Address, getContract } from 'viem'; +import { Token } from '../tokens'; +import { ERC20PermitHandler } from './erc20-permit-handler'; +import { PUFFER_WITHDRAWAL_MANAGER_ABIS } from '../abis/puffer-withdrawal-manager-abis'; + +jest.mock('viem', () => ({ + ...jest.requireActual('viem'), + getContract: jest.fn(), +})); +jest.mock('./erc20-permit-handler'); +const mockErc20PermitHandler = jest.mocked(ERC20PermitHandler); + +describe('PufferWithdrawalHandler', () => { + let handler: PufferWithdrawalHandler; + const mockChain = Chain.Mainnet; + const mockAddress = '0x123' as Address; + const walletClient = setupTestWalletClient(); + const publicClient = setupTestPublicClient(); + + beforeEach(() => { + handler = new PufferWithdrawalHandler( + mockChain, + walletClient, + publicClient, + ); + }); + + it('should return the contract', () => { + const mockAbi = PUFFER_WITHDRAWAL_MANAGER_ABIS[1].PufferWithdrawalManager; + handler.getContract(); + + expect(getContract).toHaveBeenCalledWith({ + address: '0x0000000000000000000000000000000000000000', + abi: mockAbi, + client: { public: publicClient, wallet: walletClient }, + }); + }); + + it('should request a withdrawal', async () => { + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + write: { + requestWithdrawal: jest.fn().mockResolvedValue('0x567' as Address), + }, + }); + + const amount = BigInt(1000); + + await handler.requestWithdrawal(mockAddress, amount); + + expect( + handler['getContract']().write.requestWithdrawal, + ).toHaveBeenCalledWith( + expect.arrayContaining([amount, mockAddress]), + expect.any(Object), + ); + }); + + it('should request a withdrawal with permit', async () => { + const amount = BigInt(1000); + const mockPermit = { + r: '0x', + s: '0x', + v: 27, + deadline: BigInt(1000), + amount, + }; + + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + write: { + requestWithdrawalWithPermit: jest + .fn() + .mockResolvedValue('0x567' as Address), + }, + }); + + const withToken = jest + .spyOn(mockErc20PermitHandler.prototype, 'withToken') + .mockImplementation(() => ({ + getPermitSignature: jest.fn().mockResolvedValue(mockPermit), + })); + + await handler.requestWithdrawalWithPermit(mockAddress, amount); + + expect(withToken).toHaveBeenCalledWith(Token.pufETH); + + expect( + handler['getContract']().write.requestWithdrawalWithPermit, + ).toHaveBeenCalledWith( + expect.arrayContaining([mockPermit, mockAddress]), + expect.any(Object), + ); + }); + + it('should complete a queued withdrawal', async () => { + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + write: { + completeQueuedWithdrawal: jest + .fn() + .mockResolvedValue('0x567' as Address), + }, + }); + + const withdrawalIdx = BigInt(5); + await handler.completeQueueWithdrawal(mockAddress, withdrawalIdx); + + expect( + handler['getContract']().write.completeQueuedWithdrawal, + ).toHaveBeenCalledWith( + expect.arrayContaining([withdrawalIdx]), + expect.any(Object), + ); + }); + + it('should get a withdrawal by its index', async () => { + const withdrawalIdx = BigInt(1); + const mockWithdrawal = { amount: '10', recipient: mockAddress }; + + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + read: { + getWithdrawal: jest.fn().mockResolvedValue(mockWithdrawal), + }, + }); + + const result = await handler.getWithDrawal(withdrawalIdx); + expect(result).toEqual(mockWithdrawal); + + expect(handler['getContract']().read.getWithdrawal).toHaveBeenCalledWith([ + withdrawalIdx, + ]); + }); +}); diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts index 82e7556..de1efcd 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -47,7 +47,7 @@ export class PufferWithdrawalHandler { * * @returns The viem contract. */ - private getContract() { + public getContract() { const address = CONTRACT_ADDRESSES[this.chain] .PufferWithdrawalManager as Address; const abi = @@ -62,16 +62,19 @@ export class PufferWithdrawalHandler { } /** - * Request a withdrawal of the given amount to the given address. + * Request a withdrawal of the given amount to the given address, with a permit. * @param walletAddress The account address to request the withdrawal for. * @param amount The pufETH amount to request the withdrawal for. * @returns The transaction hash of the withdrawal. */ public async requestWithdrawal(walletAddress: Address, amount: bigint) { - return await this.getContract().write.requestWithdrawal([ - amount, - walletAddress, - ]); + return await this.getContract().write.requestWithdrawal( + [amount, walletAddress], + { + account: walletAddress, + chain: this.viemChain, + }, + ); } /** @@ -91,7 +94,7 @@ export class PufferWithdrawalHandler { CONTRACT_ADDRESSES[this.chain].PufferWithdrawalManager as Address, amount, ); - /* istanbul ignore next */ + const permitData = { r, s, @@ -99,11 +102,14 @@ export class PufferWithdrawalHandler { deadline, amount, }; - // TOOD add permit - return await this.getContract().write.requestWithdrawalWithPermit([ - permitData, - walletAddress, - ]); + + return await this.getContract().write.requestWithdrawalWithPermit( + [permitData, walletAddress], + { + account: walletAddress, + chain: this.viemChain, + }, + ); } /** @@ -111,10 +117,17 @@ export class PufferWithdrawalHandler { * @param withdrawalIdx The index of the withdrawal to complete. * @returns The transaction hash of the withdrawal. */ - public async completeQueueWithdrawal(withdrawalIdx: bigint) { - return await this.getContract().write.completeQueuedWithdrawal([ - withdrawalIdx, - ]); + public async completeQueueWithdrawal( + walletAddress: Address, + withdrawalIdx: bigint, + ) { + return await this.getContract().write.completeQueuedWithdrawal( + [withdrawalIdx], + { + account: walletAddress, + chain: this.viemChain, + }, + ); } /** From 1de25a14826668d497af5e80be58f0db458345e5 Mon Sep 17 00:00:00 2001 From: 9inpachi <9inpachi@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:36:48 +0000 Subject: [PATCH 3/6] [Github Actions] Release v1.5.3 --- lib/contracts/handlers/puffer-withdrawal-manager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts index de1efcd..1bf46de 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -94,7 +94,6 @@ export class PufferWithdrawalHandler { CONTRACT_ADDRESSES[this.chain].PufferWithdrawalManager as Address, amount, ); - const permitData = { r, s, From 476c862231754213f843c9c4222a1fdcf7f22272 Mon Sep 17 00:00:00 2001 From: Nick Guest Date: Wed, 18 Sep 2024 15:17:59 +0200 Subject: [PATCH 4/6] 2512 return transact and estimate gas from withdrawal fns --- .../puffer-withdrawal-manager.test.ts | 77 ++++++++++++++++++- .../handlers/puffer-withdrawal-manager.ts | 50 ++++++++---- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts index 7f44e56..2b6e53b 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts @@ -51,7 +51,9 @@ describe('PufferWithdrawalHandler', () => { const amount = BigInt(1000); - await handler.requestWithdrawal(mockAddress, amount); + const { transact } = await handler.requestWithdrawal(mockAddress, amount); + + await transact(); expect( handler['getContract']().write.requestWithdrawal, @@ -61,6 +63,29 @@ describe('PufferWithdrawalHandler', () => { ); }); + it('should estimate gas for requestWithdrawal', async () => { + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + estimateGas: { + requestWithdrawal: jest.fn().mockResolvedValue(BigInt(100000)), + }, + }); + + const amount = BigInt(1000); + + const { estimate } = await handler.requestWithdrawal(mockAddress, amount); + + const gasEstimate = await estimate(); + + expect( + handler['getContract']().estimateGas.requestWithdrawal, + ).toHaveBeenCalledWith( + expect.arrayContaining([amount, mockAddress]), + expect.any(Object), + ); + + expect(gasEstimate).toEqual(BigInt(100000)); + }); + it('should request a withdrawal with permit', async () => { const amount = BigInt(1000); const mockPermit = { @@ -85,7 +110,12 @@ describe('PufferWithdrawalHandler', () => { getPermitSignature: jest.fn().mockResolvedValue(mockPermit), })); - await handler.requestWithdrawalWithPermit(mockAddress, amount); + const { transact } = await handler.requestWithdrawalWithPermit( + mockAddress, + amount, + ); + + await transact(); expect(withToken).toHaveBeenCalledWith(Token.pufETH); @@ -97,6 +127,49 @@ describe('PufferWithdrawalHandler', () => { ); }); + it('should estimate gas for a withdrawal with permit', async () => { + const amount = BigInt(1000); + const mockPermit = { + r: '0x', + s: '0x', + v: 27, + deadline: BigInt(1000), + amount, + }; + + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + estimateGas: { + requestWithdrawalWithPermit: jest + .fn() + .mockResolvedValue(BigInt(100000)), + }, + }); + + const withToken = jest + .spyOn(mockErc20PermitHandler.prototype, 'withToken') + .mockImplementation(() => ({ + getPermitSignature: jest.fn().mockResolvedValue(mockPermit), + })); + + const { estimate } = await handler.requestWithdrawalWithPermit( + mockAddress, + amount, + ); + + const gasEstimate = await estimate(); + + expect(withToken).toHaveBeenCalledWith(Token.pufETH); + + expect( + handler['getContract']().estimateGas.requestWithdrawalWithPermit, + ).toHaveBeenCalledWith( + expect.arrayContaining([mockPermit, mockAddress]), + expect.any(Object), + ); + + expect(gasEstimate).toEqual(BigInt(100000)); + }); + it('should complete a queued withdrawal', async () => { jest.spyOn(handler as any, 'getContract').mockReturnValue({ write: { diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts index 1bf46de..b2b16e9 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -63,22 +63,33 @@ export class PufferWithdrawalHandler { /** * Request a withdrawal of the given amount to the given address, with a permit. + * * @param walletAddress The account address to request the withdrawal for. * @param amount The pufETH amount to request the withdrawal for. * @returns The transaction hash of the withdrawal. */ public async requestWithdrawal(walletAddress: Address, amount: bigint) { - return await this.getContract().write.requestWithdrawal( - [amount, walletAddress], - { - account: walletAddress, - chain: this.viemChain, - }, - ); + const transact = async () => + await this.getContract().write.requestWithdrawal( + [amount, walletAddress], + { + account: walletAddress, + chain: this.viemChain, + }, + ); + + const estimate = async () => + await this.getContract().estimateGas.requestWithdrawal( + [amount, walletAddress], + { account: walletAddress }, + ); + + return { transact, estimate }; } /** * Request a withdrawal of the given amount to the given address. + * * @param walletAddress The account address to request the withdrawal for. * @param amount The pufETHamount to request the withdrawal for. * @returns The transaction hash of the withdrawal. @@ -102,17 +113,27 @@ export class PufferWithdrawalHandler { amount, }; - return await this.getContract().write.requestWithdrawalWithPermit( - [permitData, walletAddress], - { - account: walletAddress, - chain: this.viemChain, - }, - ); + const transact = async () => + await this.getContract().write.requestWithdrawalWithPermit( + [permitData, walletAddress], + { + account: walletAddress, + chain: this.viemChain, + }, + ); + + const estimate = async () => + await this.getContract().estimateGas.requestWithdrawalWithPermit( + [permitData, walletAddress], + { account: walletAddress }, + ); + + return { transact, estimate }; } /** * Complete a withdrawal from the queue. + * * @param withdrawalIdx The index of the withdrawal to complete. * @returns The transaction hash of the withdrawal. */ @@ -131,6 +152,7 @@ export class PufferWithdrawalHandler { /** * Get the withdrawal at the given index. + * * @param withdrawalIdx The index of the withdrawal to get. * @returns The withdrawal at the given index. */ From cbae81a61adbeda59dbb1c4a216cb129eb31f67b Mon Sep 17 00:00:00 2001 From: Nick Guest Date: Wed, 18 Sep 2024 15:29:02 +0200 Subject: [PATCH 5/6] 2512 update mocking of permitsignature in tests --- .../puffer-withdrawal-manager.test.ts | 32 +++++++++---------- .../handlers/puffer-withdrawal-manager.ts | 2 ++ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts index 2b6e53b..a592665 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts @@ -5,16 +5,12 @@ import { } from '../../../test/setup-test-clients'; import { Chain } from '../../chains/constants'; import { Address, getContract } from 'viem'; -import { Token } from '../tokens'; -import { ERC20PermitHandler } from './erc20-permit-handler'; import { PUFFER_WITHDRAWAL_MANAGER_ABIS } from '../abis/puffer-withdrawal-manager-abis'; jest.mock('viem', () => ({ ...jest.requireActual('viem'), getContract: jest.fn(), })); -jest.mock('./erc20-permit-handler'); -const mockErc20PermitHandler = jest.mocked(ERC20PermitHandler); describe('PufferWithdrawalHandler', () => { let handler: PufferWithdrawalHandler; @@ -104,11 +100,9 @@ describe('PufferWithdrawalHandler', () => { }, }); - const withToken = jest - .spyOn(mockErc20PermitHandler.prototype, 'withToken') - .mockImplementation(() => ({ - getPermitSignature: jest.fn().mockResolvedValue(mockPermit), - })); + const getPermitSignature = jest + .spyOn((handler as any).erc20PermitHandler, 'getPermitSignature') + .mockReturnValue(Promise.resolve(mockPermit)); const { transact } = await handler.requestWithdrawalWithPermit( mockAddress, @@ -117,7 +111,11 @@ describe('PufferWithdrawalHandler', () => { await transact(); - expect(withToken).toHaveBeenCalledWith(Token.pufETH); + expect(getPermitSignature).toHaveBeenCalledWith( + mockAddress, + '0x0000000000000000000000000000000000000000', + BigInt(1000), + ); expect( handler['getContract']().write.requestWithdrawalWithPermit, @@ -145,11 +143,9 @@ describe('PufferWithdrawalHandler', () => { }, }); - const withToken = jest - .spyOn(mockErc20PermitHandler.prototype, 'withToken') - .mockImplementation(() => ({ - getPermitSignature: jest.fn().mockResolvedValue(mockPermit), - })); + const getPermitSignature = jest + .spyOn((handler as any).erc20PermitHandler, 'getPermitSignature') + .mockReturnValue(Promise.resolve(mockPermit)); const { estimate } = await handler.requestWithdrawalWithPermit( mockAddress, @@ -158,7 +154,11 @@ describe('PufferWithdrawalHandler', () => { const gasEstimate = await estimate(); - expect(withToken).toHaveBeenCalledWith(Token.pufETH); + expect(getPermitSignature).toHaveBeenCalledWith( + mockAddress, + '0x0000000000000000000000000000000000000000', + BigInt(1000), + ); expect( handler['getContract']().estimateGas.requestWithdrawalWithPermit, diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts index b2b16e9..e9efc2f 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -105,6 +105,8 @@ export class PufferWithdrawalHandler { CONTRACT_ADDRESSES[this.chain].PufferWithdrawalManager as Address, amount, ); + + /* istanbul ignore next */ const permitData = { r, s, From 33560ba7c8c1152b572a844c6cef981b32b5c7f4 Mon Sep 17 00:00:00 2001 From: Nick Guest Date: Thu, 19 Sep 2024 10:06:41 +0200 Subject: [PATCH 6/6] 2512 add estimate/transact to withdrawal manager completeQueuedWithdrawal --- .../puffer-withdrawal-manager.test.ts | 30 ++++++++++++++++++- .../handlers/puffer-withdrawal-manager.ts | 16 ++++++---- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts index a592665..b614136 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.test.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.test.ts @@ -180,7 +180,12 @@ describe('PufferWithdrawalHandler', () => { }); const withdrawalIdx = BigInt(5); - await handler.completeQueueWithdrawal(mockAddress, withdrawalIdx); + const { transact } = await handler.completeQueueWithdrawal( + mockAddress, + withdrawalIdx, + ); + + await transact(); expect( handler['getContract']().write.completeQueuedWithdrawal, @@ -190,6 +195,29 @@ describe('PufferWithdrawalHandler', () => { ); }); + it('should estimate gas for a queued withdrawal', async () => { + jest.spyOn(handler as any, 'getContract').mockReturnValue({ + estimateGas: { + completeQueuedWithdrawal: jest.fn().mockResolvedValue(BigInt(100000)), + }, + }); + + const withdrawalIdx = BigInt(5); + const { estimate } = await handler.completeQueueWithdrawal( + mockAddress, + withdrawalIdx, + ); + + await estimate(); + + expect( + handler['getContract']().estimateGas.completeQueuedWithdrawal, + ).toHaveBeenCalledWith( + expect.arrayContaining([withdrawalIdx]), + expect.any(Object), + ); + }); + it('should get a withdrawal by its index', async () => { const withdrawalIdx = BigInt(1); const mockWithdrawal = { amount: '10', recipient: mockAddress }; diff --git a/lib/contracts/handlers/puffer-withdrawal-manager.ts b/lib/contracts/handlers/puffer-withdrawal-manager.ts index e9efc2f..b08458b 100644 --- a/lib/contracts/handlers/puffer-withdrawal-manager.ts +++ b/lib/contracts/handlers/puffer-withdrawal-manager.ts @@ -143,13 +143,19 @@ export class PufferWithdrawalHandler { walletAddress: Address, withdrawalIdx: bigint, ) { - return await this.getContract().write.completeQueuedWithdrawal( - [withdrawalIdx], - { + const transact = async () => + await this.getContract().write.completeQueuedWithdrawal([withdrawalIdx], { account: walletAddress, chain: this.viemChain, - }, - ); + }); + + const estimate = async () => + await this.getContract().estimateGas.completeQueuedWithdrawal( + [withdrawalIdx], + { account: walletAddress }, + ); + + return { transact, estimate }; } /**