Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/great-spies-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Fixed Celo maxFeePerGas calculation for fee currency transactions.
80 changes: 73 additions & 7 deletions src/celo/fees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,94 @@ describe('celo/fees', () => {
expect(requestMock).not.toHaveBeenCalled()
})

test('calls the client when feeCurrency is provided', async () => {
test('calls the client when feeCurrency is provided celo L1', async () => {
const requestMock = vi.spyOn(client, 'request')

const baseFee = 15057755162n
const priorityFee = 602286n

expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function')

// The check to determine if the chain is L1 or L2 is done by checking to
// see if there is code at the proxy admin address (by calling
// eth_getCode), if there is code then the chain is considered to be L2. A
// response of '0x' as used here is what is returned when there is no code
// at the address.

// @ts-ignore
requestMock.mockImplementation((request) => {
if (request.method === 'eth_gasPrice') return '11619349802'
if (request.method === 'eth_maxPriorityFeePerGas') return '2323869960'
if (request.method === 'eth_gasPrice') return baseFee.toString()
if (request.method === 'eth_maxPriorityFeePerGas')
return priorityFee.toString()
if (request.method === 'eth_getCode') return '0x'
return
})

expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function')

const fees = await celoestimateFeesPerGasFn({
client,
multiply: (value: bigint) => (value * 150n) / 100n,
request: {
feeCurrency: '0xfee',
},
} as any)

// For celo L1 the fees maxFeePerGas is calculated as `baseFee + maxPriorityFeePerGas`, the multiply method is not used.
expect(fees).toMatchInlineSnapshot(`
{
"maxFeePerGas": 11619349802n,
"maxPriorityFeePerGas": 2323869960n,
"maxFeePerGas": ${baseFee + priorityFee}n,
"maxPriorityFeePerGas": ${priorityFee}n,
}
`)
expect(requestMock).toHaveBeenCalledWith({
method: 'eth_maxPriorityFeePerGas',
params: ['0xfee'],
})
expect(requestMock).toHaveBeenCalledWith({
method: 'eth_gasPrice',
params: ['0xfee'],
})
})

test('calls the client when feeCurrency is provided celo L2', async () => {
const requestMock = vi.spyOn(client, 'request')

const baseFee = 15057755162n
const priorityFee = 602286n

expect(celo.fees.estimateFeesPerGas).toBeTypeOf('function')

// The check to determine if the chain is L1 or L2 is done by checking to
// see if there is code at the proxy admin address (by calling
// eth_getCode), if there is code then the chain is considered to be L2. A
// response longer than '0x' as used here is what is returned when there is
// code at the address.

// @ts-ignore
requestMock.mockImplementation((request) => {
if (request.method === 'eth_gasPrice')
return (baseFee + priorityFee).toString()
if (request.method === 'eth_maxPriorityFeePerGas')
return priorityFee.toString()
if (request.method === 'eth_getCode') return '0x00400400404040404040404'
return
})

const multiply = (value: bigint) => (value * 150n) / 100n
const feesCeloL1 = await celoestimateFeesPerGasFn({
client,
multiply: multiply,
request: {
feeCurrency: '0xfee',
},
} as any)

// For Celo L2 the fees maxFeePerGas is calculated as the following where
// multiply is the method passed to celoestimateFeesPerGasFn:
// `multiply(baseFeePerGas - maxPriorityFeePerGas) + maxPriorityFeePerGas`.
expect(feesCeloL1).toMatchInlineSnapshot(`
{
"maxFeePerGas": ${multiply(baseFee) + priorityFee}n,
"maxPriorityFeePerGas": ${priorityFee}n,
}
`)
expect(requestMock).toHaveBeenCalledWith({
Expand Down
35 changes: 33 additions & 2 deletions src/celo/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ import type {
} from '../index.js'
import type { formatters } from './formatters.js'

type RequestGetCodeParams = {
Method: 'eth_getCode'
Parameters: [Address, 'latest']
ReturnType: Hex
}
/*
* This checks if we're in L2 context, it's a port of the technique used in
* https://github.com/celo-org/celo-monorepo/blob/da9b4955c1fdc8631980dc4adf9b05e0524fc228/packages/protocol/contracts-0.8/common/IsL2Check.sol#L17
*/
const isCel2 = async (client: Client) => {
const proxyAdminAddress = '0x4200000000000000000000000000000000000018'
const code = await client.request<RequestGetCodeParams>({
method: 'eth_getCode',
params: [proxyAdminAddress, 'latest'],
})
if (typeof code === 'string') {
return code !== '0x' && code.length > 2
}
return false
}

export const fees: ChainFees<typeof formatters> = {
/*
* Estimates the fees per gas for a transaction.
Expand All @@ -22,16 +43,26 @@ export const fees: ChainFees<typeof formatters> = {
) => {
if (!params.request?.feeCurrency) return null

const [maxFeePerGas, maxPriorityFeePerGas] = await Promise.all([
const [gasPrice, maxPriorityFeePerGas] = await Promise.all([
estimateFeePerGasInFeeCurrency(params.client, params.request.feeCurrency),
estimateMaxPriorityFeePerGasInFeeCurrency(
params.client,
params.request.feeCurrency,
),
])

let maxFeePerGas: bigint
if (await isCel2(params.client)) {
// eth_gasPrice for cel2 returns baseFeePerGas + maxPriorityFeePerGas
maxFeePerGas =
params.multiply(gasPrice - maxPriorityFeePerGas) + maxPriorityFeePerGas
} else {
// eth_gasPrice for Celo L1 returns (baseFeePerGas * multiplier), where the multiplier is 2 by default.
maxFeePerGas = gasPrice + maxPriorityFeePerGas
}

return {
maxFeePerGas,
maxFeePerGas: maxFeePerGas,
maxPriorityFeePerGas,
}
},
Expand Down
6 changes: 3 additions & 3 deletions src/celo/sendTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ describe('sendTransaction()', () => {
}

if (request.method === 'eth_maxPriorityFeePerGas') {
return 1n
return 602286n
}

if (
request.method === 'eth_gasPrice' &&
(request.params as string[])[0] === feeCurrencyAddress
) {
return 2n
return 15057755162n
}

if (request.method === 'eth_estimateGas') {
Expand Down Expand Up @@ -99,7 +99,7 @@ describe('sendTransaction()', () => {
expect(transportRequestMock).toHaveBeenLastCalledWith({
method: 'eth_sendRawTransaction',
params: [
'0x7bf87782a4ec8001020194f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c0940000000000000000000000000000000000000fee80a0a3163f9ff91200f4c8000f0217d85d16c329c2f38d48a7b4b70119989e475e57a0555fd5b2a6eac95426e33cd07ca5fec121ad46194611a013001f76bbc4b33136',
'0x7bf87f82a4ec80830930ae8503818c4cc80194f39fd6e51aad88f6f4ce6ab8827279cfffb922660180c0940000000000000000000000000000000000000fee01a07dbc07e3ed73e889b085f29f2b4a1e794d578307199b5a677071a204328837a0a02da0ba218d0ab8164010f3c0efa7f84f4df6d879a242dc5110248e7014df75dc',
],
})
})
Expand Down