feat(cct-sdk): Add transfer admin evm op - #334
Conversation
…10418-handle-pool-versioning
…10418-handle-pool-versioning
…10418-handle-pool-versioning
…10418-handle-pool-versioning
* 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.
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.
|
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 |
aelmanaa
left a comment
There was a problem hiding this comment.
✅ 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
transferAdminRoletx 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
senderBoundToWalletto prevent authorization leaks - Both have optional
senderinexecute(defaults to wallet), required ingenerate - 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
senderrejection (required forgenerate) - 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 !== administratorcomparison (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.TokenAdminRegistryin test
Wallet Binding & Authorization Leak Prevention
Uses senderBoundToWallet (from PR #333's EVMOperation enhancement):
generate()requires explicitsender(can be any address; used for offline flows)execute()optionalsenderdefaults to wallet address (only address that can satisfy the admin check for a signed tx)- If
senderis given toexecute, 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
generateUnsignedTransferAdminandtransferAdminexamples 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.
…e distinction warning to transferAdmin execute method
|
✅ Feedback addressed (commit 7485c06):
Type check: ✅ |
* 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>
What
transferAdminto EVMTokenManager, encodingtransferAdminRole; two step: newAdmin has to call acceptAdmin to finish the hand-offWhy
Testing
Notes