Skip to content
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
151 changes: 150 additions & 1 deletion src/lib/core/__tests__/resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ethers } from 'ethers'
import { ValueResolver } from '../resolver'
import { ExecutionContext } from '../context'
import { BasicArithmeticValue, Network, ReadBalanceValue, ComputeCreate2Value, ConstructorEncodeValue, AbiEncodeValue, AbiPackValue, CallValue, ContractExistsValue } from '../../types'
import { BasicArithmeticValue, Network, ReadBalanceValue, ComputeCreate2Value, ConstructorEncodeValue, AbiEncodeValue, AbiPackValue, CallValue, ContractExistsValue, ComputeCreateValue } from '../../types'
import { ContractRepository } from '../../contracts/repository'

describe('ValueResolver', () => {
Expand Down Expand Up @@ -494,6 +494,155 @@ describe('ValueResolver', () => {
})
})

describe('compute-create', () => {
it('should compute CREATE address with hardcoded test case 1', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0x0000000000000000000000000000000000000000',
nonce: '0',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0xBd770416a3345F91E4B34576cb804a576fa48EB1')
})

it('should compute CREATE address with hardcoded test case 2', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0xC6064FfBaDB0687Da29721C8EC02ACa71e735a3e',
nonce: '1',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0x6d2E686984620c01Af3cd125F9E1A2E23a972FFc')
})

it('should compute CREATE address with hardcoded test case 3', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0xC6064FfBaDB0687Da29721C8EC02ACa71e735a3e',
nonce: '2',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0xBA6CfaFc33eD8229D2Af9a5a7BC22e8834cE0873')
})

it('should compute CREATE address with hardcoded test case ERC-2470', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0xBb6e024b9cFFACB947A71991E386681B1Cd1477D',
nonce: '0',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0xce0042B868300000d44A59004Da54A005ffdcf9f')
})

it('should compute CREATE address with hardcoded test case Universal Deployer', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0x9c5a87452d4FAC0cbd53BDCA580b20A45526B3AB',
nonce: '0',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0x1B926fBB24A9F78DCDd3272f2d86F5D0660E59c0')
})

it('should resolve values from context before computing CREATE address', async () => {
context.setOutput('myDeployer', '0x0000000000000000000000000000000000000000')
context.setOutput('myNonce', '0')

const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '{{myDeployer}}',
nonce: '{{myNonce}}',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0xBd770416a3345F91E4B34576cb804a576fa48EB1')
})

it('should throw error for invalid deployer address', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: 'invalid-address',
nonce: '0',
},
}

await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid deployer address: invalid-address')
})

it('should throw error for invalid nonce', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0x0000000000000000000000000000000000000000',
nonce: 'invalid-nonce',
},
}

await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid nonce: invalid-nonce')
})

it('should throw error for null deployer address', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: null as any,
nonce: '0',
},
}

await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid deployer address: null')
})

it('should throw error for undefined nonce', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0x0000000000000000000000000000000000000000',
nonce: undefined as any,
},
}

await expect(resolver.resolve(value, context)).rejects.toThrow('Invalid nonce: undefined')
})

it('should handle checksummed addresses', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0xdEADBEeF00000000000000000000000000000000',
nonce: '0',
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0xf2048C36a5536FeA3Bc71d49ed59f2c65C546EEA')
})

it('should handle number nonce', async () => {
const value: ComputeCreateValue = {
type: 'compute-create',
arguments: {
deployerAddress: '0x0000000000000000000000000000000000000000',
nonce: 69420,
},
}
const result = await resolver.resolve(value, context)
expect(result).toBe('0x7Bd7F19787DA009bD75b849c92Db10CE11916487')
})
})

describe('constructor-encode', () => {
it('should encode creation code with no constructor arguments', async () => {
const value = {
Expand Down
37 changes: 29 additions & 8 deletions src/lib/core/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import {
AbiEncodeValue,
AbiPackValue,
ConstructorEncodeValue,
ComputeCreateValue,
ComputeCreate2Value,
ReadBalanceValue,
BasicArithmeticValue,
CallValue,
ContractExistsCondition,
ContractExistsValue,
JobCompletedValue,
ReadJsonValue,
} from '../types'
import { ExecutionContext } from './context'
import { isAddress, isBigNumberish, isBytesLike } from '../utils/assertion'

/**
* A scope for resolving local variables, such as template arguments.
Expand Down Expand Up @@ -154,6 +155,8 @@ export class ValueResolver {
return this.resolveAbiPack(resolvedArgs as AbiPackValue['arguments'])
case 'constructor-encode':
return this.resolveConstructorEncode(resolvedArgs as ConstructorEncodeValue['arguments'])
case 'compute-create':
return this.resolveComputeCreate(resolvedArgs as ComputeCreateValue['arguments'])
case 'compute-create2':
return this.resolveComputeCreate2(resolvedArgs as ComputeCreate2Value['arguments'])
case 'read-balance':
Expand Down Expand Up @@ -260,7 +263,7 @@ export class ValueResolver {
}

// Validate that creation code is valid bytecode
if (!ethers.isBytesLike(creationCode)) {
if (!isBytesLike(creationCode)) {
throw new Error(`Invalid creation code: ${creationCode}`)
}

Expand All @@ -280,18 +283,36 @@ export class ValueResolver {
return '0x' + cleanCreationCode + cleanEncodedArgs
}

private resolveComputeCreate(args: ComputeCreateValue['arguments']): string {
const { deployerAddress, nonce } = args
// Check if the deployer address is a valid address
if (!isAddress(deployerAddress)) {
throw new Error(`Invalid deployer address: ${deployerAddress}`)
}
// Check if the nonce is a valid value
if (!isBigNumberish(nonce)) {
throw new Error(`Invalid nonce: ${nonce}`)
}
const bnNonce = ethers.toBigInt(nonce)
// Create the create address
return ethers.getCreateAddress({
from: deployerAddress,
nonce: bnNonce,
})
}

private resolveComputeCreate2(args: ComputeCreate2Value['arguments']): string {
const { deployerAddress, salt, initCode } = args
// Check if the deployer address is a valid address
if (!ethers.isAddress(deployerAddress)) {
if (!isAddress(deployerAddress)) {
throw new Error(`Invalid deployer address: ${deployerAddress}`)
}
// Check if the salt is a valid bytes value
if (!ethers.isBytesLike(salt)) {
if (!isBytesLike(salt)) {
throw new Error(`Invalid salt: ${salt}`)
}
// Check if the init code is a valid bytes value
if (!ethers.isBytesLike(initCode)) {
if (!isBytesLike(initCode)) {
throw new Error(`Invalid init code: ${initCode}`)
}
// Hash the init code using Keccak256
Expand All @@ -304,7 +325,7 @@ export class ValueResolver {
// Check if the address is a valid address
const addressValue = args.address as any

if (!ethers.isAddress(addressValue)) {
if (!isAddress(addressValue)) {
throw new Error(`Invalid address: ${addressValue}`)
}

Expand Down Expand Up @@ -349,7 +370,7 @@ export class ValueResolver {
}

// Validate that the target address is a valid Ethereum address
if (!ethers.isAddress(to)) {
if (!isAddress(to)) {
throw new Error(`call: invalid target address: ${to}`)
}

Expand Down Expand Up @@ -407,7 +428,7 @@ export class ValueResolver {
private async resolveContractExists(args: ContractExistsValue['arguments'], context: ExecutionContext): Promise<boolean> {
const { address } = args

if (!ethers.isAddress(address)) {
if (!isAddress(address)) {
throw new Error(`contract-exists: invalid address: ${address}`)
}

Expand Down
11 changes: 10 additions & 1 deletion src/lib/types/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ export interface ConstructorEncodeValue {
};
}

export interface ComputeCreateValue {
type: 'compute-create';
arguments: {
deployerAddress: AddressValue;
nonce: Uint256Value;
};
}

export interface ComputeCreate2Value {
type: 'compute-create2';
arguments: {
Expand Down Expand Up @@ -94,6 +102,7 @@ export type ValueResolver =
| AbiEncodeValue
| AbiPackValue
| ConstructorEncodeValue
| ComputeCreateValue
| ComputeCreate2Value
| ReadBalanceValue
| BasicArithmeticValue
Expand All @@ -113,4 +122,4 @@ export type Value<T = string | number | boolean> = T | Reference | ValueResolver
export type BytesValue = Value<string>;
export type AddressValue = Value<string>;
export type Uint256Value = Value<string | number>;
export type BooleanValue = Value<boolean>;
export type BooleanValue = Value<boolean>;
24 changes: 24 additions & 0 deletions src/lib/utils/assertion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ethers } from "ethers";

export function isAddress(value: unknown): value is string {
return ethers.isAddress(value)
}

export function isBytesLike(value: unknown): value is string {
return ethers.isBytesLike(value)
}

export function isBigNumberish(value: unknown): value is string | number | bigint {
try {
switch (typeof(value)) {
case "bigint":
case "number":
case "string":
ethers.toBigInt(value)
return true
}
} catch (error) {
// Fail out
}
return false
}