Skip to content

Commit

Permalink
Merge pull request #91 from base-org/wilson/writeContractDeposit
Browse files Browse the repository at this point in the history
writeContractDeposit
  • Loading branch information
zencephalon authored Oct 10, 2023
2 parents b7d1842 + 1cedfab commit b86493f
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 24 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-goats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"op-viem": patch
---

Add writeContractDeposit
1 change: 1 addition & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export {
type SimulateWithdrawETHParameters,
type SimulateWithdrawETHReturnType,
} from './public/L2/simulateWithdrawETH.js'
export { writeContractDeposit, type WriteContractDepositParameters } from './wallet/L1/writeContractDeposit.js'
export { writeDepositERC20, type WriteDepositERC20Parameters } from './wallet/L1/writeDepositERC20.js'
export { writeDepositETH, type WriteDepositETHParameters } from './wallet/L1/writeDepositETH.js'
export {
Expand Down
119 changes: 119 additions & 0 deletions src/actions/wallet/L1/writeContractDeposit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { optimismPortalABI } from '@eth-optimism/contracts-ts'
import { decodeEventLog, encodeFunctionData } from 'viem'
import { mine } from 'viem/actions'
import { expect, test } from 'vitest'
import { erc721ABI } from 'wagmi'
import { accounts } from '../../../_test/constants.js'
import { publicClient, testClient, walletClient } from '../../../_test/utils.js'
import { base } from '../../../chains/base.js'
import { parseOpaqueData } from '../../../utils/getArgsFromTransactionDepositedOpaqueData.js'
import { writeContractDeposit } from './writeContractDeposit.js'

test('default', async () => {
const functionName = 'approve'
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n]
const l2GasLimit = 100000n
const hash = await writeContractDeposit(walletClient, {
abi: erc721ABI,
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b',
functionName,
args,
account: accounts[0].address,
l2GasLimit,
l2Chain: base,
})
await mine(testClient, { blocks: 1 })

const txReceipt = await publicClient.getTransactionReceipt({ hash })
expect(txReceipt.status).toEqual('success')
const depositEvents = []
for (const l of txReceipt.logs) {
try {
const event = decodeEventLog({
abi: optimismPortalABI,
data: l.data,
topics: l.topics,
})
if (event.eventName === 'TransactionDeposited') {
depositEvents.push({ event, logIndex: l.logIndex })
}
} catch {}
}
const parsedOpaqueData = parseOpaqueData(depositEvents[0].event.args.opaqueData)
expect(BigInt(parsedOpaqueData.mint)).toEqual(0n)
expect(BigInt(parsedOpaqueData.value)).toEqual(0n)
expect(BigInt(parsedOpaqueData.gas)).toEqual(l2GasLimit)
expect(parsedOpaqueData?.data).toEqual(encodeFunctionData({
abi: erc721ABI,
args,
functionName,
}))
})

test('throws error if strict = true and account is smart contract wallet', async () => {
const scw_address = '0x2De5Aad1bD26ec3fc4F64E95068c02D84Df072C6'
await testClient.impersonateAccount({
address: scw_address,
})
const functionName = 'approve'
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n]
const l2GasLimit = 100000n
expect(() =>
writeContractDeposit(walletClient, {
abi: erc721ABI,
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b',
functionName,
args,
account: scw_address,
l2GasLimit,
l2Chain: base,
})
).rejects.toThrowError(
'Calling depositTransaction from a smart contract can have unexpected results, see https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#address-aliasing. Set `strict` to false to disable this check.',
)
})

test('allows smart contract wallet if strict = false', async () => {
const scw_address = '0x2De5Aad1bD26ec3fc4F64E95068c02D84Df072C6'
await testClient.impersonateAccount({
address: scw_address,
})
await testClient.setBalance({
address: scw_address,
value: 10n ** 22n,
})
const functionName = 'approve'
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n]
const l2GasLimit = 100000n
expect(
await writeContractDeposit(walletClient, {
abi: erc721ABI,
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b',
functionName,
args,
account: scw_address,
l2GasLimit,
l2Chain: base,
strict: false,
}),
).toBeDefined()
})

test('throws error if no account passed', async () => {
const functionName = 'approve'
const args: [`0x${string}`, bigint] = ['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', 2048n]
const l2GasLimit = 100000n
expect(() =>
// @ts-expect-error
writeContractDeposit(walletClient, {
abi: erc721ABI,
address: '0x6171f829e107f70b58d67594c6b62a7d3eb7f23b',
functionName,
args,
l2GasLimit,
l2Chain: base,
})
).rejects.toThrowError(
'No account found',
)
})
103 changes: 103 additions & 0 deletions src/actions/wallet/L1/writeContractDeposit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
type Abi,
type Account,
type Address,
type Chain,
encodeFunctionData,
type EncodeFunctionDataParameters,
type Transport,
type WalletClient,
type WriteContractParameters,
type WriteContractReturnType,
} from 'viem'
import { getBytecode } from 'viem/actions'
import { parseAccount } from 'viem/utils'
import { OpStackL1Contract } from '../../../index.js'
import type { GetL2Chain, L1ActionBaseType, ResolveChain } from '../../../types/l1Actions.js'
import { writeDepositTransaction, type WriteDepositTransactionParameters } from './writeDepositTransaction.js'

export type WriteContractDepositParameters<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
TChain extends Chain | undefined = Chain,
TAccount extends Account | undefined = Account | undefined,
TChainOverride extends Chain | undefined = Chain | undefined,
> =
& { account: TAccount | Address; l2GasLimit: bigint; l2MsgValue?: bigint; strict?: boolean }
& L1ActionBaseType<GetL2Chain<ResolveChain<TChain, TChainOverride>>, typeof OpStackL1Contract.OptimismPortal>
& Omit<
WriteContractParameters<
TAbi,
TFunctionName,
TChain,
TAccount,
TChainOverride
>, // NOTE(Wilson):
// In the future we could possibly allow value to be passed, creating an L2 mint
// as writeDepositTransaction does but I want to avoid for now as it complicates
// simulating the L2 transaction that results from this call, as we have no to mock/simulate the L2 mint.
'value' | 'account'
>

/**
* A L1 -> L2 version of Viem's writeContract. Can be used to create an arbitrary L2 transaction from L1.
* NOTE: If caller is a smart contract wallet, msg.sender on the L2 transaction will be an alias of the L1 address.
* Must set `strict` = false to allow calling from smart contract wallet.
*
* @param parameters - {@link WriteContractDepositParameters}
* @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType}
*/
export async function writeContractDeposit<
TChain extends Chain | undefined,
TAccount extends Account | undefined,
const TAbi extends Abi | readonly unknown[],
TFunctionName extends string,
TChainOverride extends Chain | undefined,
>(
client: WalletClient<Transport, TChain, TAccount>,
{
abi,
account: account_ = client.account,
address,
args,
functionName,
l2GasLimit,
l2MsgValue = 0n,
l2Chain,
optimismPortalAddress,
strict = true,
...request
}: WriteContractDepositParameters<
TAbi,
TFunctionName,
TChain,
TAccount,
TChainOverride
>,
): Promise<WriteContractReturnType> {
const calldata = encodeFunctionData({
abi,
args,
functionName,
} as unknown as EncodeFunctionDataParameters<TAbi, TFunctionName>)
if (!account_) {
throw new Error('No account found')
}
const account = parseAccount(account_)

if (strict) {
const code = await getBytecode(client, { address: account.address })
if (code) {
throw new Error(
'Calling depositTransaction from a smart contract can have unexpected results, see https://github.com/ethereum-optimism/optimism/blob/develop/specs/deposits.md#address-aliasing. Set `strict` to false to disable this check.',
)
}
}
return writeDepositTransaction(client, {
optimismPortalAddress,
l2Chain,
args: { gasLimit: l2GasLimit, to: address, data: calldata, value: l2MsgValue },
account,
...request,
} as unknown as WriteDepositTransactionParameters<TChain, TAccount, TChainOverride>)
}
17 changes: 16 additions & 1 deletion src/decorators/walletL1OpStackActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Account, Chain, Transport, WriteContractReturnType } from 'viem'
import type { Abi, Account, Chain, Transport, WriteContractReturnType } from 'viem'
import type { WalletClient } from 'viem'
import { writeContractDeposit, type WriteContractDepositParameters } from '../actions/wallet/L1/writeContractDeposit.js'
import { writeDepositERC20, type WriteDepositERC20Parameters } from '../actions/wallet/L1/writeDepositERC20.js'
import { writeDepositETH, type WriteDepositETHParameters } from '../actions/wallet/L1/writeDepositETH.js'
import {
Expand Down Expand Up @@ -56,6 +57,19 @@ export type WalletL1OpStackActions<
TChainOverride
>,
) => Promise<WriteContractReturnType>
writeContractDeposit: <
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends string = string,
TChainOverride extends Chain | undefined = Chain | undefined,
>(
args: WriteContractDepositParameters<
TAbi,
TFunctionName,
TChain,
TAccount,
TChainOverride
>,
) => Promise<WriteContractReturnType>
}

export function walletL1OpStackActions<
Expand All @@ -71,5 +85,6 @@ export function walletL1OpStackActions<
writeDepositERC20: (args) => writeDepositERC20(client, args),
writeProveWithdrawalTransaction: (args) => writeProveWithdrawalTransaction(client, args),
writeFinalizeWithdrawalTransaction: (args) => writeFinalizeWithdrawalTranasction(client, args),
writeContractDeposit: (args) => writeContractDeposit(client, args),
}
}
48 changes: 48 additions & 0 deletions src/utils/getArgsFromTransactionDepositedOpaqueData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { type Hex, size, slice } from 'viem'
import type { TransactionDepositedEvent } from '../index.js'

export type ParsedTransactionDepositedOpaqueData = {
// TODO(Wilson): consider using mint in writeDepositContract is it is more
// clear that this is the value that will be minted on L2
// this type could then maybe be called DepositTransactionArgs, though it would have this
// additional mint field, when compared to the contract
mint: Hex
value: Hex
gas: Hex
isCreation: boolean
data: Hex
}

/**
* @description Returns the TransactionDeposited event and log index, if found,
* from the transaction receipt
*
* @param {opaqueData} opaqueData from the TransactionDepositedEvent event args
* @returns {ParsedTransactionDepositedOpaqueData} The data parsed into five fields
*/
export function parseOpaqueData(
opaqueData: TransactionDepositedEvent['args']['opaqueData'],
): ParsedTransactionDepositedOpaqueData {
let offset = 0
const mint = slice(opaqueData, offset, offset + 32)
offset += 32
const value = slice(opaqueData, offset, offset + 32)
offset += 32
const gas = slice(opaqueData, offset, offset + 8)
offset += 8
const isCreation = BigInt(opaqueData[offset]) === 1n
offset += 1
const data =
// NOTE(Wilson): this is to deal with kind of odd behvior in slice
// https://github.com/wagmi-dev/viem/blob/main/src/utils/data/slice.ts#L34
offset > size(opaqueData) - 1
? '0x'
: slice(opaqueData, offset, opaqueData.length)
return {
mint,
value,
gas,
isCreation,
data,
}
}
30 changes: 8 additions & 22 deletions src/utils/getDepositTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type Hex, size, slice } from 'viem'
import { type Hex } from 'viem'
import {
type DepositTransaction,
SourceHashDomain,
type TransactionDepositedEvent,
} from '../types/depositTransaction.js'
import { parseOpaqueData } from './getArgsFromTransactionDepositedOpaqueData.js'
import { getSourceHash } from './getSourceHash.js'

export type GetDepositTransactionParams =
Expand All @@ -30,33 +31,18 @@ export function getDepositTransaction({
sourceHash = sourceHash ?? getSourceHash({ domain, logIndex, l1BlockHash })
/// code from https://github.com/ethereum-optimism/optimism/blob/develop/packages/core-utils/src/optimism/deposit-transaction.ts#L198
/// with adaptions for viem
const opaqueData = event.args.opaqueData
let offset = 0
const mint = slice(opaqueData, offset, offset + 32)
offset += 32
const value = slice(opaqueData, offset, offset + 32)
offset += 32
const gas = slice(opaqueData, offset, offset + 8)
offset += 8
const isCreation = BigInt(opaqueData[offset]) === 1n
offset += 1
const to = isCreation === true ? '0x' : event.args.to
const data =
// NOTE(Wilson): this is to deal with kind of odd behvior in slice
// https://github.com/wagmi-dev/viem/blob/main/src/utils/data/slice.ts#L34
offset > size(opaqueData) - 1
? '0x'
: slice(opaqueData, offset, opaqueData.length)
const parsedOpaqueData = parseOpaqueData(event.args.opaqueData)
const isSystemTransaction = false
const to = parsedOpaqueData.isCreation === true ? '0x' : event.args.to

return {
sourceHash,
from: event.args.from,
to,
mint: mint,
value,
gas,
mint: parsedOpaqueData.mint,
value: parsedOpaqueData.value,
gas: parsedOpaqueData.gas,
isSystemTransaction,
data,
data: parsedOpaqueData.data,
}
}
2 changes: 1 addition & 1 deletion src/utils/getTransactionDepositedEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function getTransactionDepositedEvents({
})
if (event.eventName === 'TransactionDeposited') {
if (!l.logIndex) {
throw new Error('Found TransactionDeposited by logIndex undefined')
throw new Error('Found TransactionDeposited but logIndex undefined')
}
depositEvents.push({ event, logIndex: l.logIndex })
}
Expand Down

0 comments on commit b86493f

Please sign in to comment.