Skip to content

feat(cct-sdk): Add transfer admin evm op - #334

Merged
mervin-link merged 41 commits into
cct-sdkfrom
feature/DAPP-10818-evm-transfer-admin
Aug 10, 2026
Merged

feat(cct-sdk): Add transfer admin evm op#334
mervin-link merged 41 commits into
cct-sdkfrom
feature/DAPP-10818-evm-transfer-admin

Conversation

@apedrob

@apedrob apedrob commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What

  • Add transferAdmin to EVMTokenManager, encoding transferAdminRole; two step: newAdmin has to call acceptAdmin to finish the hand-off

Why

  • Lets an existing administrator assign the role to someone else
  • Pre-tx read tells apart three situations that would otherwise all surface as the same revert: the token is not registered, it is registered but still waiting to be accepted, or the caller is simply not the administrator; registration state is checked before comparing sender, so a zero address sender cannot match an unregistered token's zero administrator and slip through

Testing

  • Unit tests cover the encoding, all three rejection messages and the wallet binding

Notes

  • This is the registry admin role, which is separate from a pool's transferOwnership

apedrob and others added 30 commits July 6, 2026 15:25
* feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip

* Remove outdated bytecodes
* feat(cct-sdk): Source token + pool abi/bytecode from contracts-ccip

* feat(cct-sdk): Add versioned Deploy Token (CCT + ERC20)

* Remove outdated bytecodes

* Deploy only CrossChainToken

* Recover previous type changes
Vendor ERC20LockBox v2.0.0 artifacts; add the cached lockbox Interface + DeployLockbox op, wire generateUnsignedDeployLockbox/deployLockbox into EVMTokenManager, and document the LockRelease deploy sequence.
Add AuthorizeLockboxCallers op (wraps ERC20LockBox applyAuthorizedCallerUpdates) + tests, and wire generateUnsignedAuthorizeLockboxCallers/authorizeLockboxCallers into EVMTokenManager to complete the LockRelease flow.
Query wires validate -> read so params are rejected before any RPC, mirroring how
Operation.generate gates buildUnsigned on the write side. EVMQuery binds it to an
EVMChain and owns getTypedContract, the single ethers -> ethers-abitype bridge that
CCT read ops decode through.
BurnMintTokenPoolType / LockReleaseTokenPoolType split TOKEN_POOL_TYPES by ABI family,
and isLockReleaseTokenPoolType narrows to the lock/release set per getTokenPoolFamily.
apedrob added 8 commits August 3, 2026 16:55
Reads a pool's admin state across v1.5.0-v2.0.0 through the getters each version has:
one reader per generation, dispatched explicitly rather than floor-matched, so adding a
pool version fails to compile instead of silently inheriting a reader. The result is a
union discriminated by version, then by type for a lock/release pool's lockBox. v1.5.0
has no getTokenDecimals, so legacy decimals come from the token.
Also groups the operation fields by area (token / token admin registry / token pool /
lockbox), matching SolanaTokenManager.
…10823)

GetTokenPoolState gains name + validate and drops its params-conditional result: read
now resolves to the union, which removes a query override that only re-typed the base's
two steps, along with the two casts that conditional return required. Caller narrowing
moves to SolanaTokenManager.getTokenPoolState overloads, so call sites and inferred
types are unchanged.
@apedrob
apedrob requested review from a team, PabloMansanet and aelmanaa as code owners August 6, 2026 11:56
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

You must have Developer access to commit code to Chainlink Labs on Vercel. If you contact an administrator and receive Developer access, commit again to see your changes.

Learn more: https://vercel.com/docs/accounts/team-members-and-roles/access-roles#team-level-roles

@apedrob
apedrob requested a review from mervin-link August 6, 2026 11:56
Base automatically changed from feature/DAPP-10817-evm-register-token to cct-sdk August 7, 2026 17:22

@aelmanaa aelmanaa left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ APPROVE — EVM transferAdmin op (bf7feb3)

Reviewed for authorization checks, three-state registration validation, and test coverage. Strong work — the op pairs with registerAdmin to complete the two-step admin handoff, disambiguates three error cases, and catches a critical zero-address bypass regression.


Strengths

Three-state registration pre-flight with explicit error disambiguation

  • Not registered (administrator = zero): Clear error "token is not registered in the TokenAdminRegistry"
  • Pending acceptance (pendingAdministrator ≠ zero): Clear error "registration still pending acceptance by {address}; that address must call acceptAdmin first"
  • Already registered (administrator ≠ zero): Checks if sender equals administrator, with error "must be the current token administrator"
  • Registration state is checked BEFORE comparing against sender, preventing a critical bypass

Critical regression guard: zero-address sender on unregistered token

  • Without the state-first guard, a zero sender would compare equal to an unregistered token's zero administrator and silently build a transferAdminRole tx for an unregistered token (security issue)
  • Regression tests explicitly cover both branches (not-registered + pending-acceptance) with zero sender
  • State check happens first, independently of sender, preventing the bypass

Pair-wise symmetry with registerAdmin

  • Both are two-step flows: registerAdmin proposes, acceptAdmin completes
  • Both use senderBoundToWallet to prevent authorization leaks
  • Both have optional sender in execute (defaults to wallet), required in generate
  • Both pre-flight the authorization before encoding

Version-stable encoding

  • transferAdminRole(token, newAdmin) is version-identical across v1.5–v2.0
  • No version dispatch needed (unlike some other ops)
  • TAR resolved from any compatible contract (registry, Router, OnRamp, OffRamp, TokenPool)

Clear authorization boundary

  • Doc explicitly distinguishes registry admin role (this op) from pool owner (transferOwnership)
  • Common practice: same EOA/multisig for both roles, but roles live on separate contracts
  • Two roles transferred independently — confusion not possible with clear naming

Test coverage (301 lines, 16+ test cases)

  • Calldata golden vector: transferAdminRole(token, newAdmin) encoded against known interface
  • Selector validation: TAR discovery confirmed
  • All three rejection states with state-first guard regression tests (zero sender + not-registered, zero sender + pending)
  • Missing sender rejection (required for generate)
  • Wallet binding: reject non-signer, reject wallet≠sender divergence
  • Default sender to wallet address in execute

Per-Scenario Analysis

Happy path: current admin transfers role

  • Registry read confirms administrator ≠ zero (registered)
  • Sender matches administrator
  • Encodes transferAdminRole(token, newAdmin)
  • ✅ Verified in test "encodes transferAdminRole to discovered TAR"

Rejection: not registered

  • Administrator = zero
  • Error: "token is not registered in the TokenAdminRegistry"
  • ✅ Verified; explicit test "rejects token that is not registered"

Rejection: pending acceptance

  • Administrator = zero, pendingAdministrator ≠ zero
  • Error: "registration still pending acceptance by {address}; that address must acceptAdmin"
  • ✅ Verified; explicit test "distinguishes pending from not-registered"

Rejection: sender not current admin

  • Administrator ≠ zero, sender ≠ administrator
  • Error: "must be the current token administrator ({administrator})"
  • ✅ Verified; explicit test "rejects sender not current administrator"

Regression guard: zero sender on unregistered token

  • State-first check catches it: administrator = zero → "not registered"
  • Error surfaces before sender !== administrator comparison (which would succeed, silently)
  • ✅ Verified; explicit regression test "rejects zero-address sender on unregistered token"

Regression guard: zero sender on pending token

  • State-first check catches it: administrator = zero, pendingAdministrator set → "still pending"
  • ✅ Verified; explicit regression test "rejects zero-address sender on pending token"

Calldata Verification

Golden vector: transferAdminRole(token, newAdmin)

  • Token parameter serialized first, newAdmin second (matches TAR ABI order)
  • Selector validated against cached TAR interface
  • ✅ Verified against interfaces.TokenAdminRegistry in test

Wallet Binding & Authorization Leak Prevention

Uses senderBoundToWallet (from PR #333's EVMOperation enhancement):

  • generate() requires explicit sender (can be any address; used for offline flows)
  • execute() optional sender defaults to wallet address (only address that can satisfy the admin check for a signed tx)
  • If sender is given to execute, it must match wallet address or throws before signing
  • Prevents the leak where pre-tx checks authorize one address, signing wallet is different

Parameter Semantics

tokenAddress — Token whose registry admin role is being handed over
newAdmin — The proposed new administrator; must separately call acceptAdmin to accept
address — Contract to resolve TAR from (can be TAR, Router, OnRamp, OffRamp, or TokenPool)
sender — Current registry administrator; required for generate, optional for execute (defaults to wallet)


Nits (non-blocking)

  • Example parity: Both generateUnsignedTransferAdmin and transferAdmin examples are clear; inline example in the op class docstring might show the three rejection scenarios (pedagogical, not needed for merge)

  • Zero-address bypass context: The regression test comment is excellent ("the guard compared administrator !== sender first"), but a reference link to the registerAdmin PR (#333) could clarify why this pattern matters (senderBoundToWallet also added there)


npm run check status

✅ Exit 0 — no type errors.


Verdict

APPROVE. All three registration states are disambiguated with clear errors, the zero-address bypass regression is caught and tested, pre-tx validation prevents ambiguous on-chain reverts, and test coverage is comprehensive. The op correctly completes the two-step admin handoff pair with registerAdmin. Ready for merge.

Comment thread ccip-sdk/src/cct/evm/index.ts
…e distinction warning to transferAdmin execute method
@aelmanaa

aelmanaa commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Feedback addressed (commit 7485c06):

  1. newAdmin = ZeroAddress behavior documented — Updated TransferAdminParams.newAdmin JSDoc to note:

    "Pass {@link ZeroAddress} to cancel any pending transfer — the pending proposal is discarded without accepting the role."

  2. Role distinction warning added to execute method — Updated transferAdmin() JSDoc in evm/index.ts to include:

    "This is the registry's ADMIN role — distinct from a pool's Ownable2Step owner (see {@link transferOwnership}); do not confuse the two."

    • Now consistent with generateUnsignedTransferAdmin() documentation
    • Provides unified guidance across both method variants

Type check:npm run check exit 0

* feat(cct-sdk): Add accept admin evm op

* feat(cct-sdk): Add get token admin registry config evm query (#335)

* feat(cct-sdk): Add get token admin registry config evm op

* docs(cct-sdk): add ZeroAddress caveat to GetTokenAdminRegistryResult type alias

Clarify that administrator may be ZeroAddress for pending-acceptance state;
guide callers to test with === ZeroAddress instead of truthiness to avoid
silently missing the pending-registration case.

* feat(cct-sdk): Add get supported tokens evm query (#336)

* feat(cct-sdk): Add get supported tokens evm query

* docs+types(cct-sdk): add default page size and explicit result type to getSupportedTokens

- Document that page parameter defaults to 1000 tokens per call
- Add explicit GetSupportedTokensResult type export for surface parity with
  sibling ops and Solana variant
- Makes the intent clearer and provides consistency across CCT query operations

* fix: use GetSupportedTokensResult

---------

Co-authored-by: aelmanaa <aelmanaa@users.noreply.github.com>
Co-authored-by: mervin-link <tvc-mervin.villaceran@smartcontract.com>

---------

Co-authored-by: aelmanaa <aelmanaa@users.noreply.github.com>
Co-authored-by: mervin-link <tvc-mervin.villaceran@smartcontract.com>

---------

Co-authored-by: aelmanaa <aelmanaa@users.noreply.github.com>
Co-authored-by: mervin-link <tvc-mervin.villaceran@smartcontract.com>
@mervin-link
mervin-link merged commit 6ee85dd into cct-sdk Aug 10, 2026
2 of 4 checks passed
@mervin-link
mervin-link deleted the feature/DAPP-10818-evm-transfer-admin branch August 10, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants