Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1872: Fix bug with transactions being reverted because of wrong permit/approval spender #22

Merged
merged 5 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: pnpm lint

- name: Test
run: pnpm test
run: pnpm test:coverage

- name: Build
run: pnpm build
1 change: 1 addition & 0 deletions lib/api/puffer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class PufferClient {
) {
const viemChain = VIEM_CHAINS[chain];

/* istanbul ignore next */
this.walletClient =
walletClient ??
createWalletClient({
Expand Down
7 changes: 5 additions & 2 deletions lib/contracts/handlers/erc20-permit-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
WalletClient,
isHash,
isHex,
padHex,
serializeSignature,
} from 'viem';

Expand All @@ -33,6 +34,7 @@ describe('ERC20PermitHandler', () => {
contractTestingUtils.mockCall('nonces', [10n]);
contractTestingUtils.mockCall('name', ['Ethereum Staking Mock']);

const mockSpender = padHex('0x', { size: 20 });
// Mocking the wallet client since couldn't find a way to mock
// through eth-testing.
const mockSignature = serializeSignature({
Expand All @@ -47,7 +49,7 @@ describe('ERC20PermitHandler', () => {

const signature = await handler
.withToken(Token.stETH)
.getPermitSignature(mockAccount, 1n);
.getPermitSignature(mockAccount, mockSpender, 1n);

const { r, s, v, yParity, deadline } = signature;
expect(isHex(r)).toBeTruthy();
Expand All @@ -59,8 +61,9 @@ describe('ERC20PermitHandler', () => {

it('should approve the usage of token for a spender', async () => {
contractTestingUtils.mockTransaction('approve');
const mockSpender = padHex('0x', { size: 20 });

const txHash = await handler.approve(mockAccount, 1n);
const txHash = await handler.approve(mockAccount, mockSpender, 1n);
expect(isHash(txHash)).toBeTruthy();
});
});
27 changes: 18 additions & 9 deletions lib/contracts/handlers/erc20-permit-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import { Chain, VIEM_CHAINS, ViemChain } from '../../chains/constants';
import { ERC20PERMIT_ABI } from '../abis/tokens-abis';
import { TOKENS_ADDRESSES, Token } from '../tokens';
import { CHAIN_ADDRESSES } from '../addresses';
import { getTimestampInSeconds } from '../../utils/time';

/**
Expand Down Expand Up @@ -68,14 +67,19 @@ export class ERC20PermitHandler {
* Process and get permit signature for the given token to perform
* transactions through the `PufferDepositor` contract.
*
* @param walletAddress Wallet address making the transaction.
* @param ownerAddress Address of the token awner.
* @param spenderAddress Address of the spender who needs the permit.
* @param value Value of the transaction.
* @returns Permit signature in the form `{ r, s, v?, yParity }`.
*/
public async getPermitSignature(walletAddress: Address, value: bigint) {
public async getPermitSignature(
ownerAddress: Address,
spenderAddress: Address,
value: bigint,
) {
const contract = this.getContract();

const permitNonces = await contract.read.nonces([walletAddress]);
const permitNonces = await contract.read.nonces([ownerAddress]);
const name = await contract.read.name();
const domain = <const>{
name,
Expand All @@ -96,13 +100,13 @@ export class ERC20PermitHandler {
const deadline = BigInt(getTimestampInSeconds() + 60 * 60 * 2);

const signature = await this.walletClient.signTypedData({
account: walletAddress,
account: ownerAddress,
domain,
types,
primaryType: 'Permit',
message: {
owner: walletAddress,
spender: CHAIN_ADDRESSES[this.chain].PufferDepositor as Address,
owner: ownerAddress,
spender: spenderAddress,
value,
nonce: permitNonces,
deadline,
Expand All @@ -124,13 +128,18 @@ export class ERC20PermitHandler {
/**
* Approve transaction for the spender to spend the owner's tokens.
*
* @param ownerAddress Address of the caller of the transaction.
* @param spenderAddress Address of the spender.
* @param value Value to approve for the spender.
* @returns Hash of the transaction.
*/
public approve(spenderAddress: Address, value: bigint) {
public approve(
ownerAddress: Address,
spenderAddress: Address,
value: bigint,
) {
return this.getContract().write.approve([spenderAddress, value], {
account: spenderAddress,
account: ownerAddress,
chain: this.viemChain,
});
}
Expand Down
13 changes: 9 additions & 4 deletions lib/contracts/handlers/puf-locker-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export class PufLockerHandler {
/**
* Get the minimum and maximum lock periods allowed for deposits.
*
* @returns The minimum and maximum lock period. (`[minLock,
* maxLock]`)
* @returns The minimum and maximum lock period in seconds.
* (`[minLock, maxLock]`)
*/
public getLockPeriods() {
return this.getContract().read.getLockPeriods();
Expand All @@ -106,7 +106,7 @@ export class PufLockerHandler {
* @param pufToken PufToken to deposit.
* @param walletAddress Wallet address of the depositor.
* @param value Amount of the deposit.
* @param lockPeriod The period for the deposit.
* @param lockPeriod The period for the deposit in seconds.
* @returns The transaction hash of the deposit.
*/
public async deposit(
Expand All @@ -117,7 +117,12 @@ export class PufLockerHandler {
) {
const { r, s, v, yParity, deadline } = await this.erc20PermitHandler
.withToken(pufToken)
.getPermitSignature(walletAddress, value);
.getPermitSignature(
walletAddress,
CHAIN_ADDRESSES[this.chain].PufLocker as Address,
value,
);
/* istanbul ignore next */
const permitData = {
r,
s,
Expand Down
5 changes: 3 additions & 2 deletions lib/contracts/handlers/puf-token-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isHash } from 'viem';
import { isHash, padHex } from 'viem';
import {
setupTestWalletClient,
setupTestPublicClient,
Expand Down Expand Up @@ -95,8 +95,9 @@ describe('PufTokenHandler', () => {

it('should request for approval', async () => {
contractTestingUtils.mockTransaction('approve');
const txHash = await handler.approve(mockAccount, 10n);
const mockSpender = padHex('0x', { size: 20 });

const txHash = await handler.approve(mockAccount, mockSpender, 10n);
expect(isHash(txHash)).toBe(true);
});

Expand Down
9 changes: 7 additions & 2 deletions lib/contracts/handlers/puf-token-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,18 @@ export class PufTokenHandler {
/**
* Approve transaction for the spender to spend the owner's tokens.
*
* @param ownerAddress Address of the caller of the transaction.
* @param spenderAddress Address of the spender.
* @param value Value to approve for the spender.
* @returns Hash of the transaction.
*/
public approve(spenderAddress: Address, value: bigint) {
public approve(
ownerAddress: Address,
spenderAddress: Address,
value: bigint,
) {
return this.getContract().write.approve([spenderAddress, value], {
account: spenderAddress,
account: ownerAddress,
chain: this.viemChain,
});
}
Expand Down
14 changes: 12 additions & 2 deletions lib/contracts/handlers/puffer-depositor-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ export class PufferDepositorHandler {
public async depositStETH(walletAddress: Address, value: bigint) {
const { r, s, v, yParity, deadline } = await this.erc20PermitHandler
.withToken(Token.stETH)
.getPermitSignature(walletAddress, value);
.getPermitSignature(
walletAddress,
CHAIN_ADDRESSES[this.chain].PufferDepositor as Address,
value,
);
/* istanbul ignore next */
const permitData = {
r,
s,
Expand Down Expand Up @@ -108,7 +113,12 @@ export class PufferDepositorHandler {
public async depositWstETH(walletAddress: Address, value: bigint) {
const { r, s, v, yParity, deadline } = await this.erc20PermitHandler
.withToken(Token.wstETH)
.getPermitSignature(walletAddress, value);
.getPermitSignature(
walletAddress,
CHAIN_ADDRESSES[this.chain].PufferDepositor as Address,
value,
);
/* istanbul ignore next */
const permitData = {
r,
s,
Expand Down
27 changes: 12 additions & 15 deletions lib/contracts/handlers/puffer-l2-depositor-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,18 @@ describe('PufferL2DepositorHandler', () => {
);
});

// TODO: This test is not working because the suggested parameters
// from the PufferL2Depositor contract are not exactly correct. See
// https://github.com/PufferFinance/puffer-contracts/blob/d3e318f3c45d744bfa2dbadfa1abe998fa49d4b5/mainnet-contracts/src/interface/IPufferL2Depositor.sol#L51-L57
// it('should deposit pre-approved token', async () => {
// contractTestingUtils.mockTransaction('deposit');

// const { transact, estimate } = handler.depositAfterApproval(
// Token.stETH,
// mockAccount,
// 10n,
// );

// expect(typeof (await estimate())).toBe('bigint');
// expect(isHash(await transact())).toBe(true);
// });
it('should deposit pre-approved token', async () => {
contractTestingUtils.mockTransaction('deposit');

const { transact, estimate } = handler.depositAfterApproval(
Token.stETH,
mockAccount,
10n,
);

expect(typeof (await estimate())).toBe('bigint');
expect(isHash(await transact())).toBe(true);
});

it('should deposit token after requesting permit', async () => {
contractTestingUtils.mockTransaction('deposit');
Expand Down
26 changes: 22 additions & 4 deletions lib/contracts/handlers/puffer-l2-depositor-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WalletClient, PublicClient, getContract, Address } from 'viem';
import { WalletClient, PublicClient, getContract, Address, padHex } from 'viem';
import { Chain, VIEM_CHAINS, ViemChain } from '../../chains/constants';
import { PUFFER_L2_DEPOSITOR_ABIS } from '../abis/puffer-depositor-abis';
import { CHAIN_ADDRESSES } from '../addresses';
Expand Down Expand Up @@ -77,7 +77,14 @@ export class PufferL2DepositorHandler {
TOKENS_ADDRESSES[token][this.chain],
walletAddress,
// Only `amount` is needed if `token.approve()` is already called.
{ amount: 0n } as any,
// So using mock values for other properties.
{
r: padHex('0x', { size: 32 }),
s: padHex('0x', { size: 32 }),
v: 0,
deadline: 0n,
amount: value,
},
value,
];

Expand All @@ -99,9 +106,14 @@ export class PufferL2DepositorHandler {
* the transaction but returns two methods namely `transact` and
* `estimate`.
*
* Note that not all token contracts support permit signatures (e.g.
* USDC). If a token's contract doesn't support permit signatures, use
* `Token.approve()` and call `depositAfterApproval()` instead.
*
* @param token Token to deposit.
* @param walletAddress Wallet address to take the token from.
* @param value Value in wei of the token to deposit.
* @param referralCode Referral code for the deposit.
* @returns `transact: () => Promise<Address>` - Used to make the
* transaction.
*
Expand All @@ -112,10 +124,16 @@ export class PufferL2DepositorHandler {
token: NonPufToken,
walletAddress: Address,
value: bigint,
referralCode: bigint = 0n,
) {
const { r, s, v, yParity, deadline } = await this.erc20PermitHandler
.withToken(token)
.getPermitSignature(walletAddress, value);
.getPermitSignature(
walletAddress,
CHAIN_ADDRESSES[this.chain].PufferL2Depositor as Address,
value,
);
/* istanbul ignore next */
const permitData = {
r,
s,
Expand All @@ -128,7 +146,7 @@ export class PufferL2DepositorHandler {
TOKENS_ADDRESSES[token][this.chain],
walletAddress,
permitData,
value,
referralCode,
];

const transact = () =>
Expand Down
7 changes: 4 additions & 3 deletions lib/contracts/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export enum Token {
USDC = 'USDC',
DAI = 'DAI',
ETH = 'ETH',
wETH = 'wETH',
WETH = 'wETH',
stETH = 'stETH',
wstETH = 'wstETH',
pufETH = 'pufETH',
Expand All @@ -22,7 +22,7 @@ export enum Token {

export type NonPufToken = Extract<
Token,
'USDC' | 'DAI' | 'ETH' | 'wETH' | 'stETH' | 'wstETH' | 'pufETH'
'USDC' | 'DAI' | 'ETH' | 'WETH' | 'stETH' | 'wstETH' | 'pufETH'
>;

export type PufToken = Extract<
Expand All @@ -49,7 +49,8 @@ export const TOKENS_ADDRESSES: {
[Chain.Holesky]: '0x4478905505ddfb7eA1c8A9f46eAEC3695cE542ac',
},
[Token.ETH]: {},
[Token.wETH]: {
// Does not support permit signatures (ERC20Permit).
[Token.WETH]: {
[Chain.Mainnet]: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
[Chain.Holesky]: '0x94373a4919b3240d86ea41593d5eba789fef3848',
},
Expand Down
1 change: 1 addition & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './api/puffer-client';
export * from './api/puffer-client-helpers';
export * from './chains/constants';
export * from './contracts/tokens';
export * from './contracts/addresses';
Loading