Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fcf62cb
Better separation of concerns and abstractions
apedrob Jul 6, 2026
b8cbf72
CCT: Add version dispatch + Transfer Ownership
apedrob Jul 7, 2026
5c6491a
Merge branch 'feature/DAPP-10175-cct-sdk-set-pool' into feature/DAPP-…
apedrob Jul 7, 2026
0d34c01
Merge branch 'feature/DAPP-10175-cct-sdk-set-pool' into feature/DAPP-…
apedrob Jul 9, 2026
4cf2f1a
Adjust with base
apedrob Jul 9, 2026
e06660b
Merge branch 'feature/DAPP-10175-cct-sdk-set-pool' into feature/DAPP-…
apedrob Jul 9, 2026
5f621db
Merge branch 'feature/DAPP-10175-cct-sdk-set-pool' into feature/DAPP-…
apedrob Jul 13, 2026
87974f8
Address generic error types and doc examples
apedrob Jul 13, 2026
bbc9191
Fix linting
apedrob Jul 13, 2026
e2f4337
feat(cct-sdk): Add deploy evm token
apedrob Jul 14, 2026
ca537a7
Address PR comments
apedrob Jul 15, 2026
8e50efc
Address PR comments by fixing chain-specific types
apedrob Jul 16, 2026
d5badf9
feat(cct-sdk): Source chainlink/contracts-ccip artifacts (#303)
apedrob Jul 20, 2026
6530538
feat(cct-sdk): Add CrossChainToken deployment + token versioning (#305)
apedrob Jul 20, 2026
0549852
Address preMint specs
apedrob Jul 20, 2026
b886b8e
feat(cct-sdk): Add deploy evm token pool + type/version resolution
apedrob Jul 22, 2026
7bce7b5
add remarks ts doc comment
apedrob Jul 22, 2026
59e7e16
Merge branch 'cct-sdk' into feature/DAPP-10545-deploy-evm-pool
apedrob Jul 22, 2026
fbc053d
Address PR comments
apedrob Jul 23, 2026
fa96c23
add lockfile to fix ci issue
apedrob Jul 23, 2026
e5fafc0
Add comments
apedrob Jul 27, 2026
a0172b0
feat(cct-sdk): Add EVM deployLockbox operation (DAPP-10738)
apedrob Jul 27, 2026
2e44675
feat(cct-sdk): Add EVM authorizeLockboxCallers operation (DAPP-10788)
apedrob Jul 27, 2026
1bee3c2
refactor(cct-sdk): add validateNonZeroAddress + guard single-tx submit
apedrob Jul 28, 2026
56a58f2
feat(cct-sdk): EVM deploy verification + unify deploy ops
apedrob Jul 28, 2026
224bceb
Address PR comments
apedrob Jul 31, 2026
1e2d8eb
Merge branch 'cct-sdk' into feature/DAPP-10572-build-deploy-verification
apedrob Jul 31, 2026
b9ae03e
Review pass
apedrob Jul 31, 2026
397ead2
Merge branch 'cct-sdk' into feature/DAPP-10572-build-deploy-verification
apedrob Jul 31, 2026
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
35 changes: 20 additions & 15 deletions ccip-sdk/src/cct/evm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
/**
* Builds an unsigned `CrossChainToken` (v2.0.0) deployment tx (for multisig / offline
* signing). The deployed address is only known once mined, so it is NOT returned here —
* use {@link deployToken} to deploy and receive `{ hash, contractAddress }`.
* use {@link deployToken} to deploy and receive `{ hash, contractAddress, verification }`.
* @remarks Same post-deploy roles caveat as {@link deployToken} — the pool needs
* `grantMintAndBurnRoles` before it can bridge.
* @throws {@link CCTParamsInvalidError} if any param is invalid
Expand All @@ -155,7 +155,8 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {

/**
* Deploys a `CrossChainToken` (v2.0.0), signing + submitting with `opts.wallet`; resolves
* to the tx hash and the newly deployed token address.
* to the tx hash, the newly deployed token address, and a `verification`
* ({@link ExplorerVerificationInput}) for verifying the source on a block explorer.
* @remarks Mint/burn are role-gated (`MINTER_ROLE`/`BURNER_ROLE`); the token grants neither
* to any pool at deploy. `preMint` mints initial supply to `preMintRecipient`, but before a
* pool can bridge, `burnMintRoleAdmin` must `grantMintAndBurnRoles(pool)`.
Expand All @@ -164,7 +165,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
* @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address
* @example
* ```typescript
* const { hash, contractAddress } = await cct.deployToken({
* const { hash, contractAddress, verification } = await cct.deployToken({
* name: 'My Token',
* symbol: 'MTK',
* decimals: 18,
Expand All @@ -183,7 +184,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
* the pool contract — a `DeployableTokenPoolType` (`BurnMintTokenPool`, `BurnFromMintTokenPool`,
* `BurnWithFromMintTokenPool`, or `LockReleaseTokenPool`; all v2.0.0). The deployed address is
* only known once mined, so it is NOT returned here — use {@link deployTokenPool} to receive
* `{ hash, contractAddress }`.
* `{ hash, contractAddress, verification }`.
* @remarks Same post-deploy setup caveat as {@link deployTokenPool} — a fresh pool must be
* registered, role-granted, and lane-configured before it can bridge. `LockReleaseTokenPool`
* additionally requires a pre-deployed `lockbox` ({@link DeployLockReleaseTokenPoolParams})
Expand All @@ -208,8 +209,9 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
}

/**
* Deploys a token pool, signing + submitting with `opts.wallet`; resolves to the tx hash
* and the newly deployed pool address. `type` selects the pool contract (a
* Deploys a token pool, signing + submitting with `opts.wallet`; resolves to the tx hash, the
* newly deployed pool address, and a `verification` ({@link ExplorerVerificationInput}) for
* verifying the source on a block explorer. `type` selects the pool contract (a
* `DeployableTokenPoolType`, v2.0.0).
* @remarks Deploying the pool alone doesn't make it usable: register it with {@link setPool},
* grant it the token's mint/burn roles (`grantMintAndBurnRoles`), and configure its remote
Expand All @@ -223,7 +225,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
* @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address
* @example
* ```typescript
* const { hash, contractAddress } = await cct.deployTokenPool({
* const { hash, contractAddress, verification } = await cct.deployTokenPool({
* type: 'LockReleaseTokenPool',
* token: '0xToken...',
* localTokenDecimals: 18,
Expand All @@ -242,7 +244,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
* Builds an unsigned `ERC20LockBox` (v2.0.0) deployment tx (for multisig / offline signing).
* A lockbox escrows a single `token` for `LockReleaseTokenPool`s. The deployed address is
* only known once mined, so it is NOT returned here — use {@link deployLockbox} to receive
* `{ hash, contractAddress }`.
* `{ hash, contractAddress, verification }`.
* @remarks Deploy the lockbox before its pool, then authorize the pool on it with
* {@link authorizeLockboxCallers} before the pool can lock/release.
* @throws {@link CCTParamsInvalidError} if any param is invalid
Expand All @@ -260,7 +262,8 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {

/**
* Deploys an `ERC20LockBox` (v2.0.0), signing + submitting with `opts.wallet`; resolves to the
* tx hash and the newly deployed lockbox address.
* tx hash, the newly deployed lockbox address, and a `verification`
* ({@link ExplorerVerificationInput}) for verifying the source on a block explorer.
* @remarks Step two of the lock/release flow: {@link deployToken} → {@link deployLockbox} →
* {@link deployTokenPool} (passing this lockbox) → {@link authorizeLockboxCallers}
* (`addedCallers: [pool]`) → {@link setPool} → configure lanes.
Expand All @@ -269,7 +272,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {
* @throws {@link CCTTxFailedError} if the tx reverts, fails, or mines without an address
* @example
* ```typescript
* const { hash, contractAddress } = await cct.deployLockbox({
* const { hash, contractAddress, verification } = await cct.deployLockbox({
* token: '0xToken...',
* wallet,
* })
Expand Down Expand Up @@ -302,10 +305,7 @@ export class EVMTokenManager extends TokenManager<typeof ChainFamily.EVM> {

/**
* Adds/removes authorized callers on an `ERC20LockBox`, signing + submitting with `opts.wallet`
* (the lockbox owner). Authorize the `LockReleaseTokenPool` before it can lock/release — until
* then its lock/release reverts `UnauthorizedCaller(pool)`.
* @remarks Depositing the lockbox's initial liquidity is a manual final step with no SDK op: the
* depositor must itself be an authorized caller and have ERC20-approved the lockbox.
* (the lockbox owner). Authorize the `LockReleaseTokenPool` before it can lock/release.
* @throws {@link CCIPWalletInvalidError} if `wallet` is not a valid signer
* @throws {@link CCTParamsInvalidError} if any param is invalid, or if no caller is supplied
* @throws {@link CCTTxFailedError} if the tx reverts or fails
Expand Down Expand Up @@ -335,5 +335,10 @@ export type {
} from './token-pool/operations/deploy-token-pool.ts'
export type { DeployLockboxParams } from './lockbox/operations/deploy-lockbox.ts'
export type { AuthorizeLockboxCallersParams } from './lockbox/operations/authorize-callers.ts'
export type { DeployResult, EVMExecuteParams } from './operation.ts'
export type {
Comment thread
apedrob marked this conversation as resolved.
DeployArtifact,
DeployResult,
EVMExecuteParams,
ExplorerVerificationInput,
} from './operation.ts'
export type { TransactionResult } from '../operation.ts'
29 changes: 29 additions & 0 deletions ccip-sdk/src/cct/evm/lockbox/contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* EVM lockbox contract layer for CCT: the cached `ERC20LockBox` {@link Interface}
* ({@link LOCKBOX_INTERFACE}) for calldata encoding, and its deploy artifact
* ({@link getLockboxArtifact}). Only one lockbox version is deployable, so there is no version
* framework here. Mirrors `token/contracts.ts`.
*
* @packageDocumentation
*/

import { Interface } from 'ethers'

import ERC20_LOCKBOX_V2_0_0_ABI from '../artifacts/abi/V2_0_0/erc20-lockbox.ts'
import ERC20_LOCKBOX_V2_0_0_BYTECODE from '../artifacts/bytecode/V2_0_0/erc20-lockbox.ts'
import type { DeployArtifact } from '../operation.ts'

/** Shared, cached `ERC20LockBox` interface for constructor and calldata encoding. */
export const LOCKBOX_INTERFACE = new Interface(ERC20_LOCKBOX_V2_0_0_ABI)

/** `ERC20LockBox` creation bytecode for `deployLockbox`. */
export const LOCKBOX_BYTECODE = ERC20_LOCKBOX_V2_0_0_BYTECODE

/** `ERC20LockBox` deploy artifact: contract name + ctor {@link Interface} + creation bytecode. */
export function getLockboxArtifact(): DeployArtifact {
return {
contract: 'ERC20LockBox',
iface: LOCKBOX_INTERFACE,
bytecode: LOCKBOX_BYTECODE,
}
}
19 changes: 0 additions & 19 deletions ccip-sdk/src/cct/evm/lockbox/interface.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'

import { ZeroAddress, makeError } from 'ethers'
import { ZeroAddress, getIcapAddress, makeError } from 'ethers'

import { AuthorizeLockboxCallers } from './authorize-callers.ts'
import { CCIPExecTxRevertedError, CCIPWalletInvalidError } from '../../../../errors/index.ts'
Expand Down Expand Up @@ -129,6 +129,33 @@ describe('AuthorizeLockboxCallers (cct/evm lockbox operation)', () => {
)
})

it('rejects a zero-address lockbox', async () => {
// a call to 0x0 hits no code, so it would mine as a successful no-op
await assert.rejects(
() =>
new AuthorizeLockboxCallers().generate(stubChain(), {
lockbox: ZeroAddress,
addedCallers: [POOL],
}),
(err: unknown) =>
err instanceof CCTParamsInvalidError &&
err.context.operation === 'authorizeLockboxCallers' &&
err.context.param === 'lockbox',
)
})

it('rejects the zero address written in ICAP form', async () => {
// isAddress() accepts ICAP, and this never equals ZeroAddress literally
await assert.rejects(
() =>
new AuthorizeLockboxCallers().generate(stubChain(), {
lockbox: getIcapAddress(ZeroAddress),
addedCallers: [POOL],
}),
(err: unknown) => err instanceof CCTParamsInvalidError && err.context.param === 'lockbox',
)
})

it('rejects when no callers are supplied', async () => {
await assert.rejects(
() => new AuthorizeLockboxCallers().generate(stubChain(), { lockbox: LOCKBOX }),
Expand Down
24 changes: 7 additions & 17 deletions ccip-sdk/src/cct/evm/lockbox/operations/authorize-callers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,18 @@
* @packageDocumentation
*/

import { ZeroAddress } from 'ethers'

import type { EVMChain } from '../../../../evm/index.ts'
import type { UnsignedEVMTx } from '../../../../evm/types.ts'
import { ChainFamily } from '../../../../networks.ts'
import { CCTParamsInvalidError } from '../../../errors.ts'
import { EVMOperation } from '../../operation.ts'
import { validateAddress } from '../../validate.ts'
import { LOCKBOX_INTERFACE } from '../interface.ts'
import { EVMOperation, callTx } from '../../operation.ts'
import { validateNonZeroAddress } from '../../validate.ts'
import { LOCKBOX_INTERFACE } from '../contracts.ts'

/**
* Parameters for {@link AuthorizeLockboxCallers}. At least one caller across both arrays is required.
* @remarks `AuthorizedCallers._applyAuthorizedCallerUpdates` applies `removedCallers` first, so an
* address in both arrays ends up authorized. The list is a set: re-adding an existing caller is a
* no-op (though `AuthorizedCallerAdded` still fires), and removing an absent one emits nothing.
*
* Two validations here are SDK-side strictness, not contract behaviour: on-chain only *adds* revert
* `ZeroAddressNotAllowed`, and an update with both arrays empty is a successful owner-only no-op.
*/
export interface AuthorizeLockboxCallersParams {
/** Address of the `ERC20LockBox` to update. */
Expand All @@ -46,20 +40,16 @@ export class AuthorizeLockboxCallers extends EVMOperation<AuthorizeLockboxCaller
addedCallers = [],
removedCallers = [],
}: AuthorizeLockboxCallersParams): void {
validateAddress(this.name, 'lockbox', lockbox)
validateNonZeroAddress(this.name, 'lockbox', lockbox)
if (addedCallers.length + removedCallers.length === 0) {
throw new CCTParamsInvalidError(
this.name,
'addedCallers',
'at least one caller must be added or removed',
)
}
const validateCaller = (field: string, c: string, i: number): void => {
validateAddress(this.name, `${field}[${i}]`, c)
if (c === ZeroAddress) {
throw new CCTParamsInvalidError(this.name, `${field}[${i}]`, 'must not be the zero address')
}
}
const validateCaller = (field: string, c: string, i: number): void =>
validateNonZeroAddress(this.name, `${field}[${i}]`, c)
Comment thread
apedrob marked this conversation as resolved.
addedCallers.forEach((c, i) => validateCaller('addedCallers', c, i))
removedCallers.forEach((c, i) => validateCaller('removedCallers', c, i))
}
Expand All @@ -72,6 +62,6 @@ export class AuthorizeLockboxCallers extends EVMOperation<AuthorizeLockboxCaller
const data = LOCKBOX_INTERFACE.encodeFunctionData('applyAuthorizedCallerUpdates', [
{ addedCallers, removedCallers },
])
return { family: ChainFamily.EVM, transactions: [{ to: lockbox, data }] }
return callTx(lockbox, data)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,11 @@ describe('DeployLockbox (cct/evm lockbox operation)', () => {
token: TOKEN,
wallet: fakeSigner({ contractAddress: DEPLOYED }),
})
assert.deepEqual(result, { hash: HASH, contractAddress: DEPLOYED })
assert.deepEqual(result, {
hash: HASH,
contractAddress: DEPLOYED,
verification: { contract: 'ERC20LockBox', encodedConstructorArgs: '0x' + W_TOKEN },
})
})

it('throws CCTTxFailedError when the receipt carries no contract address', async () => {
Expand Down
54 changes: 13 additions & 41 deletions ccip-sdk/src/cct/evm/lockbox/operations/deploy-lockbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,11 @@
* @packageDocumentation
*/

import { ZeroAddress } from 'ethers'
import type { Interface } from 'ethers'

import type { EVMChain } from '../../../../evm/index.ts'
import type { UnsignedEVMTx } from '../../../../evm/types.ts'
import { CCTParamsInvalidError, CCTTxFailedError } from '../../../errors.ts'
import {
type DeployResult,
type EVMExecuteParams,
EVMOperation,
deploymentTx,
} from '../../operation.ts'
import { submit } from '../../submit.ts'
import { validateAddress } from '../../validate.ts'
import { LOCKBOX_BYTECODE, LOCKBOX_INTERFACE } from '../interface.ts'
import { type DeployArtifact, EVMDeployOperation } from '../../operation.ts'
import { validateNonZeroAddress } from '../../validate.ts'
import { getLockboxArtifact } from '../contracts.ts'

/** Parameters for {@link DeployLockbox} — deploys `ERC20LockBox` (v2.0.0). */
export interface DeployLockboxParams {
Expand All @@ -30,41 +21,22 @@ export interface DeployLockboxParams {
sender?: string
}

/** Deploys an `ERC20LockBox`; `execute` resolves to `{ hash, contractAddress }`. */
export class DeployLockbox extends EVMOperation<DeployLockboxParams> {
/** Deploys an `ERC20LockBox`; `execute` resolves to `{ hash, contractAddress, verification }`. */
export class DeployLockbox extends EVMDeployOperation<DeployLockboxParams> {
readonly name = 'deployLockbox'

/** Validates the constructor params before building init-code. */
protected validate(params: DeployLockboxParams): void {
validateAddress(this.name, 'token', params.token)
if (params.token === ZeroAddress)
throw new CCTParamsInvalidError(this.name, 'token', 'must not be the zero address')
validateNonZeroAddress(this.name, 'token', params.token)
}

/** Builds a deployment tx (no `to`): creation bytecode + ABI-encoded constructor args. */
protected buildUnsigned(_chain: EVMChain, params: DeployLockboxParams): UnsignedEVMTx {
return deploymentTx(LOCKBOX_BYTECODE, LOCKBOX_INTERFACE.encodeDeploy([params.token]))
/** Deploy artifact for `ERC20LockBox` (v2.0.0). */
protected artifact(): DeployArtifact {
return getLockboxArtifact()
}

/**
* {@link generate}, then sign and submit; resolves to the tx hash and the newly deployed
* lockbox address (read from the mined receipt).
* @throws {@link CCTTxFailedError} if the tx mined without producing a contract address
*/
override async execute(
chain: EVMChain,
params: EVMExecuteParams<DeployLockboxParams>,
): Promise<DeployResult> {
const { response, receipt } = await submit(
chain,
params.wallet,
await this.generate(chain, params),
this.name,
)
if (!receipt.contractAddress)
throw new CCTTxFailedError(this.name, 'deployment produced no contract address', {
context: { txHash: response.hash },
})
return { hash: response.hash, contractAddress: receipt.contractAddress }
/** ABI-encodes the `ERC20LockBox` (v2.0.0) constructor args. */
protected encode(iface: Interface, p: DeployLockboxParams): string {
return iface.encodeDeploy([p.token])
}
}
Loading
Loading