diff --git a/.github/workflows/aptos-ccip-tests.yml b/.github/workflows/aptos-ccip-tests.yml index b10715ed..759d7a48 100644 --- a/.github/workflows/aptos-ccip-tests.yml +++ b/.github/workflows/aptos-ccip-tests.yml @@ -70,6 +70,18 @@ jobs: run: | go build -o chainlink-aptos ./cmd/chainlink-aptos/main.go + # Clear up disk space, else we run out below: + # Unhandled exception. System.IO.IOException: No space left on device : '/home/runner/actions-runner/cached/_diag/Worker_20250801-102100-utc.log' + - name: Free up disk space + run: | + df -h + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + df -h + - name: Start PostgreSQL run: | docker run -d \ diff --git a/.github/workflows/aptos-run-smoke-tests.yml b/.github/workflows/aptos-run-smoke-tests.yml index dde82387..69184f6e 100644 --- a/.github/workflows/aptos-run-smoke-tests.yml +++ b/.github/workflows/aptos-run-smoke-tests.yml @@ -23,58 +23,58 @@ jobs: DEFAULT_CORE_REF: aptos-init runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get core ref from PR body - if: github.event_name == 'pull_request' - run: | - comment=$(echo "${{ github.event.pull_request.body }}") - core_ref=$(echo $comment | grep -oP 'core ref: \K\S+' || true) - if [ ! -z "$core_ref" ]; then - echo "CUSTOM_CORE_REF=${core_ref}" >> "${GITHUB_ENV}" - fi - - - name: Checkout chainlink repo - uses: actions/checkout@v4 - with: - repository: smartcontractkit/chainlink - ref: ${{ env.CUSTOM_CORE_REF || env.DEFAULT_CORE_REF }} - path: temp/chainlink - - - name: Build chainlink image - working-directory: temp/chainlink - run: | - docker buildx build --build-arg COMMIT_SHA=$(git rev-parse HEAD) -t local_chainlink -f plugins/chainlink.Dockerfile . - - - name: Build chainlink-aptos image - run: | - docker buildx build --build-arg BASE_IMAGE=local_chainlink -t chainlink-aptos -f ./Dockerfile . - - # Clear up disk space, else we run out below: - # Unhandled exception. System.IO.IOException: No space left on device : '/home/runner/actions-runner/cached/_diag/Worker_20250801-102100-utc.log' - - name: Flatten chainlink-aptos image - run: | - set -e - df -h - docker save chainlink-aptos:latest -o chainlink-aptos.tar - docker system prune -a -f - docker load -i chainlink-aptos.tar - rm -vf chainlink-aptos.tar - df -h - - - name: Extract Go version - uses: ./.github/actions/check-go-version - id: go-version - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ env.go_version }} - - - name: Create env file - working-directory: ./ - run: | + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get core ref from PR body + if: github.event_name == 'pull_request' + run: | + comment=$(echo "${{ github.event.pull_request.body }}") + core_ref=$(echo $comment | grep -oP 'core ref: \K\S+' || true) + if [ ! -z "$core_ref" ]; then + echo "CUSTOM_CORE_REF=${core_ref}" >> "${GITHUB_ENV}" + fi + + - name: Checkout chainlink repo + uses: actions/checkout@v4 + with: + repository: smartcontractkit/chainlink + ref: ${{ env.CUSTOM_CORE_REF || env.DEFAULT_CORE_REF }} + path: temp/chainlink + + - name: Build chainlink image + working-directory: temp/chainlink + run: | + docker buildx build --build-arg COMMIT_SHA=$(git rev-parse HEAD) -t local_chainlink -f plugins/chainlink.Dockerfile . + + - name: Build chainlink-aptos image + run: | + docker buildx build --build-arg BASE_IMAGE=local_chainlink -t chainlink-aptos -f ./Dockerfile . + + # Clear up disk space, else we run out below: + # Unhandled exception. System.IO.IOException: No space left on device : '/home/runner/actions-runner/cached/_diag/Worker_20250801-102100-utc.log' + - name: Flatten chainlink-aptos image + run: | + set -e + df -h + docker save chainlink-aptos:latest -o chainlink-aptos.tar + docker system prune -a -f + docker load -i chainlink-aptos.tar + rm -vf chainlink-aptos.tar + df -h + + - name: Extract Go version + uses: ./.github/actions/check-go-version + id: go-version + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.go_version }} + + - name: Create env file + working-directory: ./ + run: | cat <> .env # Postgres config POSTGRES_IMAGE=postgres:15.2-alpine @@ -105,43 +105,43 @@ jobs: CORE_P2P_PORT=6690 EOT - - name: Run Smoke Integration Test - env: - TEST_LOG_LEVEL: debug - run: | - set -e - cd integration-tests - # TODO: remove if scripts get fixed, see https://github.com/smartcontractkit/chainlink/commit/71f72665217bc17325c27109c107fb93b96e2607 - echo "replace github.com/smartcontractkit/chainlink/core/scripts => ../temp/chainlink/core/scripts" >> go.mod - echo "replace github.com/smartcontractkit/chainlink/v2 => ../temp/chainlink" >> go.mod - go mod tidy - TEST_LOG_LEVEL=${{env.TEST_LOG_LEVEL}} go test -timeout 24h -count=1 -run TestOCR3Keystone ./smoke - - - name: Configure AWS Credentials - if: success() - uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 - with: - aws-region: ${{ secrets.QA_AWS_REGION }} - role-to-assume: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} - role-duration-seconds: 3600 - mask-aws-account-id: true - - - name: Login to Amazon ECR - if: success() - id: login-ecr - uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 - with: - mask-password: "true" - - - name: Push Image to ECR - if: success() - run: | - docker tag chainlink-aptos ${{ env.CL_ECR }}:aptos.${{ github.sha }} - docker push ${{ env.CL_ECR }}:aptos.${{ github.sha }} - - - name: Upload Test Artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: smoke-test-logs - path: ./integration-tests/logs/ + - name: Run Smoke Integration Test + env: + TEST_LOG_LEVEL: debug + run: | + set -e + cd integration-tests + # TODO: remove if scripts get fixed, see https://github.com/smartcontractkit/chainlink/commit/71f72665217bc17325c27109c107fb93b96e2607 + echo "replace github.com/smartcontractkit/chainlink/core/scripts => ../temp/chainlink/core/scripts" >> go.mod + echo "replace github.com/smartcontractkit/chainlink/v2 => ../temp/chainlink" >> go.mod + go mod tidy + TEST_LOG_LEVEL=${{env.TEST_LOG_LEVEL}} go test -timeout 24h -count=1 -run TestOCR3Keystone ./smoke + + - name: Configure AWS Credentials + if: success() + uses: aws-actions/configure-aws-credentials@010d0da01d0b5a38af31e9c3470dbfdabdecca3a # v4.0.1 + with: + aws-region: ${{ secrets.QA_AWS_REGION }} + role-to-assume: ${{ secrets.QA_AWS_ROLE_TO_ASSUME }} + role-duration-seconds: 3600 + mask-aws-account-id: true + + - name: Login to Amazon ECR + if: success() + id: login-ecr + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + with: + mask-password: "true" + + - name: Push Image to ECR + if: success() + run: | + docker tag chainlink-aptos ${{ env.CL_ECR }}:aptos.${{ github.sha }} + docker push ${{ env.CL_ECR }}:aptos.${{ github.sha }} + + - name: Upload Test Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-test-logs + path: ./integration-tests/logs/ \ No newline at end of file diff --git a/bindings/ccip/receiver_registry/receiver_registry.go b/bindings/ccip/receiver_registry/receiver_registry.go index e1253ff5..fc7ee891 100644 --- a/bindings/ccip/receiver_registry/receiver_registry.go +++ b/bindings/ccip/receiver_registry/receiver_registry.go @@ -24,6 +24,7 @@ var ( type ReceiverRegistryInterface interface { TypeAndVersion(opts *bind.CallOpts) (string, error) IsRegisteredReceiver(opts *bind.CallOpts, receiverAddress aptos.AccountAddress) (bool, error) + IsRegisteredReceiverV2(opts *bind.CallOpts, receiverAddress aptos.AccountAddress) (bool, error) // Encoder returns the encoder implementation of this module. Encoder() ReceiverRegistryEncoder @@ -32,6 +33,7 @@ type ReceiverRegistryInterface interface { type ReceiverRegistryEncoder interface { TypeAndVersion() (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) IsRegisteredReceiver(receiverAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + IsRegisteredReceiverV2(receiverAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) FinishReceive(receiverAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) } @@ -54,6 +56,7 @@ const ( E_NON_EMPTY_INPUT uint64 = 5 E_PROOF_TYPE_ACCOUNT_MISMATCH uint64 = 6 E_PROOF_TYPE_MODULE_MISMATCH uint64 = 7 + E_UNAUTHORIZED uint64 = 8 ) // Structs @@ -65,6 +68,9 @@ type CCIPReceiverRegistration struct { DispatchMetadata bind.StdObject `move:"aptos_framework::object::Object"` } +type CCIPReceiverRegistrationV2 struct { +} + type ReceiverRegistered struct { ReceiverAddress aptos.AccountAddress `move:"address"` ReceiverModuleName []byte `move:"vector"` @@ -125,6 +131,27 @@ func (c ReceiverRegistryContract) IsRegisteredReceiver(opts *bind.CallOpts, rece return r0, nil } +func (c ReceiverRegistryContract) IsRegisteredReceiverV2(opts *bind.CallOpts, receiverAddress aptos.AccountAddress) (bool, error) { + module, function, typeTags, args, err := c.receiverRegistryEncoder.IsRegisteredReceiverV2(receiverAddress) + if err != nil { + return *new(bool), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(bool), err + } + + var ( + r0 bool + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(bool), err + } + return r0, nil +} + // Entry Functions // Encoder @@ -144,6 +171,14 @@ func (c receiverRegistryEncoder) IsRegisteredReceiver(receiverAddress aptos.Acco }) } +func (c receiverRegistryEncoder) IsRegisteredReceiverV2(receiverAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("is_registered_receiver_v2", nil, []string{ + "address", + }, []any{ + receiverAddress, + }) +} + func (c receiverRegistryEncoder) FinishReceive(receiverAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { return c.BoundContract.Encode("finish_receive", nil, []string{ "address", diff --git a/bindings/ccip/token_admin_registry/token_admin_registry.go b/bindings/ccip/token_admin_registry/token_admin_registry.go index 6974b4b1..ea18548a 100644 --- a/bindings/ccip/token_admin_registry/token_admin_registry.go +++ b/bindings/ccip/token_admin_registry/token_admin_registry.go @@ -26,6 +26,8 @@ type TokenAdminRegistryInterface interface { GetPools(opts *bind.CallOpts, localTokens []aptos.AccountAddress) ([]aptos.AccountAddress, error) GetPool(opts *bind.CallOpts, localToken aptos.AccountAddress) (aptos.AccountAddress, error) GetPoolLocalToken(opts *bind.CallOpts, tokenPoolAddress aptos.AccountAddress) (aptos.AccountAddress, error) + GetPoolLocalTokenV2(opts *bind.CallOpts, tokenPoolAddress aptos.AccountAddress) (aptos.AccountAddress, error) + HasTokenPoolConfig(opts *bind.CallOpts, tokenPoolAddress aptos.AccountAddress) (bool, error) GetTokenConfig(opts *bind.CallOpts, localToken aptos.AccountAddress) (aptos.AccountAddress, aptos.AccountAddress, aptos.AccountAddress, error) GetAllConfiguredTokens(opts *bind.CallOpts, startKey aptos.AccountAddress, maxCount uint64) ([]aptos.AccountAddress, aptos.AccountAddress, bool, error) IsAdministrator(opts *bind.CallOpts, localToken aptos.AccountAddress, administrator aptos.AccountAddress) (bool, error) @@ -45,6 +47,8 @@ type TokenAdminRegistryEncoder interface { GetPools(localTokens []aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) GetPool(localToken aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) GetPoolLocalToken(tokenPoolAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + GetPoolLocalTokenV2(tokenPoolAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) + HasTokenPoolConfig(tokenPoolAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) GetTokenConfig(localToken aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) GetAllConfiguredTokens(startKey aptos.AccountAddress, maxCount uint64) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) IsAdministrator(localToken aptos.AccountAddress, administrator aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) @@ -101,6 +105,7 @@ const ( E_ADMIN_NOT_SET_FOR_TOKEN uint64 = 27 E_ADMIN_ALREADY_SET_FOR_TOKEN uint64 = 28 E_ZERO_ADDRESS uint64 = 29 + E_POOL_NOT_REGISTERED uint64 = 30 ) // Structs @@ -150,6 +155,14 @@ type ReleaseOrMintOutputV1 struct { DestinationAmount uint64 `move:"u64"` } +type TokenPoolCallbacks struct { +} + +type TokenPoolConfig struct { + Callbacks TokenPoolCallbacks `move:"TokenPoolCallbacks"` + LocalToken aptos.AccountAddress `move:"address"` +} + type PoolSet struct { LocalToken aptos.AccountAddress `move:"address"` PreviousPoolAddress aptos.AccountAddress `move:"address"` @@ -272,6 +285,48 @@ func (c TokenAdminRegistryContract) GetPoolLocalToken(opts *bind.CallOpts, token return r0, nil } +func (c TokenAdminRegistryContract) GetPoolLocalTokenV2(opts *bind.CallOpts, tokenPoolAddress aptos.AccountAddress) (aptos.AccountAddress, error) { + module, function, typeTags, args, err := c.tokenAdminRegistryEncoder.GetPoolLocalTokenV2(tokenPoolAddress) + if err != nil { + return *new(aptos.AccountAddress), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(aptos.AccountAddress), err + } + + var ( + r0 aptos.AccountAddress + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(aptos.AccountAddress), err + } + return r0, nil +} + +func (c TokenAdminRegistryContract) HasTokenPoolConfig(opts *bind.CallOpts, tokenPoolAddress aptos.AccountAddress) (bool, error) { + module, function, typeTags, args, err := c.tokenAdminRegistryEncoder.HasTokenPoolConfig(tokenPoolAddress) + if err != nil { + return *new(bool), err + } + + callData, err := c.Call(opts, module, function, typeTags, args) + if err != nil { + return *new(bool), err + } + + var ( + r0 bool + ) + + if err := codec.DecodeAptosJsonArray(callData, &r0); err != nil { + return *new(bool), err + } + return r0, nil +} + func (c TokenAdminRegistryContract) GetTokenConfig(opts *bind.CallOpts, localToken aptos.AccountAddress) (aptos.AccountAddress, aptos.AccountAddress, aptos.AccountAddress, error) { module, function, typeTags, args, err := c.tokenAdminRegistryEncoder.GetTokenConfig(localToken) if err != nil { @@ -419,6 +474,22 @@ func (c tokenAdminRegistryEncoder) GetPoolLocalToken(tokenPoolAddress aptos.Acco }) } +func (c tokenAdminRegistryEncoder) GetPoolLocalTokenV2(tokenPoolAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("get_pool_local_token_v2", nil, []string{ + "address", + }, []any{ + tokenPoolAddress, + }) +} + +func (c tokenAdminRegistryEncoder) HasTokenPoolConfig(tokenPoolAddress aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { + return c.BoundContract.Encode("has_token_pool_config", nil, []string{ + "address", + }, []any{ + tokenPoolAddress, + }) +} + func (c tokenAdminRegistryEncoder) GetTokenConfig(localToken aptos.AccountAddress) (bind.ModuleInformation, string, []aptos.TypeTag, [][]byte, error) { return c.BoundContract.Encode("get_token_config", nil, []string{ "address", diff --git a/bindings/ccip_token_pools/regulated_token_pool/regulated_token_pool/regulated_token_pool.go b/bindings/ccip_token_pools/regulated_token_pool/regulated_token_pool/regulated_token_pool.go index 854def7f..e34fad23 100644 --- a/bindings/ccip_token_pools/regulated_token_pool/regulated_token_pool/regulated_token_pool.go +++ b/bindings/ccip_token_pools/regulated_token_pool/regulated_token_pool/regulated_token_pool.go @@ -105,12 +105,13 @@ func NewRegulatedTokenPool(address aptos.AccountAddress, client aptos.AptosRpcCl // Constants const ( - E_NOT_PUBLISHER uint64 = 1 - E_ALREADY_INITIALIZED uint64 = 2 - E_INVALID_FUNGIBLE_ASSET uint64 = 3 - E_LOCAL_TOKEN_MISMATCH uint64 = 4 - E_INVALID_ARGUMENTS uint64 = 5 - E_UNKNOWN_FUNCTION uint64 = 6 + E_NOT_PUBLISHER uint64 = 1 + E_ALREADY_INITIALIZED uint64 = 2 + E_INVALID_FUNGIBLE_ASSET uint64 = 3 + E_LOCAL_TOKEN_MISMATCH uint64 = 4 + E_INVALID_ARGUMENTS uint64 = 5 + E_UNKNOWN_FUNCTION uint64 = 6 + E_NOT_REGISTERED_RECEIVER uint64 = 7 ) // Structs diff --git a/contracts/ccip/ccip/sources/receiver_dispatcher.move b/contracts/ccip/ccip/sources/receiver_dispatcher.move index ac5b0cb1..1b5c4101 100644 --- a/contracts/ccip/ccip/sources/receiver_dispatcher.move +++ b/contracts/ccip/ccip/sources/receiver_dispatcher.move @@ -11,9 +11,13 @@ module ccip::receiver_dispatcher { ) { auth::assert_is_allowed_offramp(signer::address_of(caller)); - let dispatch_metadata = - receiver_registry::start_receive(receiver_address, message); - dispatchable_fungible_asset::derived_supply(dispatch_metadata); - receiver_registry::finish_receive(receiver_address); + if (receiver_registry::is_registered_receiver_v2(receiver_address)) { + receiver_registry::invoke_ccip_receive_v2(receiver_address, message); + } else { + let dispatch_metadata = + receiver_registry::start_receive(receiver_address, message); + dispatchable_fungible_asset::derived_supply(dispatch_metadata); + receiver_registry::finish_receive(receiver_address); + } } } diff --git a/contracts/ccip/ccip/sources/receiver_registry.move b/contracts/ccip/ccip/sources/receiver_registry.move index f90b9e96..7860c2e0 100644 --- a/contracts/ccip/ccip/sources/receiver_registry.move +++ b/contracts/ccip/ccip/sources/receiver_registry.move @@ -32,6 +32,11 @@ module ccip::receiver_registry { executing_input: Option } + struct CCIPReceiverRegistrationV2 has key { + callback: |client::Any2AptosMessage| has drop + copy + store, + proof_typeinfo: TypeInfo + } + #[event] struct ReceiverRegistered has store, drop { receiver_address: address, @@ -45,6 +50,7 @@ module ccip::receiver_registry { const E_NON_EMPTY_INPUT: u64 = 5; const E_PROOF_TYPE_ACCOUNT_MISMATCH: u64 = 6; const E_PROOF_TYPE_MODULE_MISMATCH: u64 = 7; + const E_UNAUTHORIZED: u64 = 8; #[view] public fun type_and_version(): String { @@ -138,9 +144,49 @@ module ccip::receiver_registry { ); } + public fun register_receiver_v2( + receiver_account: &signer, + receiver_module_name: vector, + callback: |client::Any2AptosMessage| has drop + copy + store, + _proof: ProofType + ) acquires ReceiverRegistryState { + let receiver_address = signer::address_of(receiver_account); + assert!( + !exists(receiver_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + + let proof_typeinfo = type_info::type_of(); + assert!( + proof_typeinfo.account_address() == receiver_address, + E_PROOF_TYPE_ACCOUNT_MISMATCH + ); + assert!( + proof_typeinfo.module_name() == receiver_module_name, + E_PROOF_TYPE_MODULE_MISMATCH + ); + + move_to( + receiver_account, + CCIPReceiverRegistrationV2 { callback, proof_typeinfo } + ); + + let state = borrow_state_mut(); + event::emit_event( + &mut state.receiver_registered_events, + ReceiverRegistered { receiver_address, receiver_module_name } + ); + } + #[view] public fun is_registered_receiver(receiver_address: address): bool { exists(receiver_address) + || exists(receiver_address) + } + + #[view] + public fun is_registered_receiver_v2(receiver_address: address): bool { + exists(receiver_address) } public fun get_receiver_input( @@ -185,6 +231,18 @@ module ccip::receiver_registry { ); } + public(friend) fun invoke_ccip_receive_v2( + receiver_address: address, message: client::Any2AptosMessage + ) acquires CCIPReceiverRegistrationV2 { + assert!( + exists(receiver_address), + error::invalid_argument(E_UNKNOWN_RECEIVER) + ); + + let registration = borrow_global(receiver_address); + (registration.callback) (message); + } + inline fun borrow_state(): &ReceiverRegistryState { borrow_global(state_object::object_address()) } diff --git a/contracts/ccip/ccip/sources/token_admin_dispatcher.move b/contracts/ccip/ccip/sources/token_admin_dispatcher.move index 7645fbfe..603b1fb3 100644 --- a/contracts/ccip/ccip/sources/token_admin_dispatcher.move +++ b/contracts/ccip/ccip/sources/token_admin_dispatcher.move @@ -16,17 +16,27 @@ module ccip::token_admin_dispatcher { ): (vector, vector) { auth::assert_is_allowed_onramp(signer::address_of(caller)); - let dispatch_fungible_store = - token_admin_registry::start_lock_or_burn( + if (token_admin_registry::has_token_pool_config(token_pool_address)) { + token_admin_registry::lock_or_burn_v2( token_pool_address, + fa, sender, remote_chain_selector, receiver - ); + ) + } else { + let dispatch_fungible_store = + token_admin_registry::start_lock_or_burn( + token_pool_address, + sender, + remote_chain_selector, + receiver + ); - dispatchable_fungible_asset::deposit(dispatch_fungible_store, fa); + dispatchable_fungible_asset::deposit(dispatch_fungible_store, fa); - token_admin_registry::finish_lock_or_burn(token_pool_address) + token_admin_registry::finish_lock_or_burn(token_pool_address) + } } public fun dispatch_release_or_mint( @@ -41,10 +51,8 @@ module ccip::token_admin_dispatcher { source_pool_data: vector, offchain_token_data: vector ): (FungibleAsset, u64) { - auth::assert_is_allowed_offramp(signer::address_of(caller)); - - let (dispatch_owner, dispatch_fungible_store) = - token_admin_registry::start_release_or_mint( + if (token_admin_registry::has_token_pool_config(token_pool_address)) { + token_admin_registry::release_or_mint_v2( token_pool_address, sender, receiver, @@ -54,16 +62,30 @@ module ccip::token_admin_dispatcher { source_pool_address, source_pool_data, offchain_token_data - ); + ) + } else { + let (dispatch_owner, dispatch_fungible_store) = + token_admin_registry::start_release_or_mint( + token_pool_address, + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + ); - let fa = - dispatchable_fungible_asset::withdraw( - &dispatch_owner, dispatch_fungible_store, 0 - ); + let fa = + dispatchable_fungible_asset::withdraw( + &dispatch_owner, dispatch_fungible_store, 0 + ); - let destination_amount = - token_admin_registry::finish_release_or_mint(token_pool_address); + let destination_amount = + token_admin_registry::finish_release_or_mint(token_pool_address); - (fa, destination_amount) + (fa, destination_amount) + } } } diff --git a/contracts/ccip/ccip/sources/token_admin_registry.move b/contracts/ccip/ccip/sources/token_admin_registry.move index a9e0d1c7..7d2dde78 100644 --- a/contracts/ccip/ccip/sources/token_admin_registry.move +++ b/contracts/ccip/ccip/sources/token_admin_registry.move @@ -4,7 +4,7 @@ module ccip::token_admin_registry { use std::error; use std::event::{Self, EventHandle}; use std::function_info::{Self, FunctionInfo}; - use std::fungible_asset::{Self, Metadata, FungibleStore}; + use std::fungible_asset::{Self, Metadata, FungibleStore, FungibleAsset}; use std::object::{Self, Object, ExtendRef, TransferRef}; use std::option::{Self, Option}; use std::signer; @@ -87,6 +87,17 @@ module ccip::token_admin_registry { destination_amount: u64 } + struct TokenPoolCallbacks has drop, copy, store { + lock_or_burn: |FungibleAsset, LockOrBurnInputV1| (vector, vector) has drop + + copy + store, + release_or_mint: |ReleaseOrMintInputV1| (FungibleAsset, u64) has drop + copy + store + } + + struct TokenPoolConfig has key { + callbacks: TokenPoolCallbacks, + local_token: address + } + #[event] struct PoolSet has store, drop { local_token: address, @@ -142,6 +153,7 @@ module ccip::token_admin_registry { const E_ADMIN_NOT_SET_FOR_TOKEN: u64 = 27; const E_ADMIN_ALREADY_SET_FOR_TOKEN: u64 = 28; const E_ZERO_ADDRESS: u64 = 29; + const E_POOL_NOT_REGISTERED: u64 = 30; #[view] public fun type_and_version(): String { @@ -221,6 +233,20 @@ module ccip::token_admin_registry { get_registration(token_pool_address).local_token } + #[view] + /// Returns the local token address for the token pool. + public fun get_pool_local_token_v2( + token_pool_address: address + ): address acquires TokenPoolConfig { + TokenPoolConfig[token_pool_address].local_token + } + + #[view] + /// Returns true if token pool has TokenPoolConfig resource + public fun has_token_pool_config(token_pool_address: address): bool { + exists(token_pool_address) + } + #[view] /// returns (token_pool_address, administrator, pending_administrator) public fun get_token_config( @@ -393,6 +419,44 @@ module ccip::token_admin_registry { ); } + public fun register_pool_v2( + token_pool_account: &signer, + token_pool_module_name: vector, + local_token: address, + lock_or_burn: |FungibleAsset, LockOrBurnInputV1| (vector, vector) has drop + + copy + store, + release_or_mint: |ReleaseOrMintInputV1| (FungibleAsset, u64) has drop + copy + store, + _proof: ProofType + ) { + let token_pool_address = signer::address_of(token_pool_account); + assert!( + !exists(token_pool_address), + error::invalid_argument(E_ALREADY_REGISTERED) + ); + assert!( + object::object_exists(local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + let proof_typeinfo = type_info::type_of(); + assert!( + proof_typeinfo.account_address() == token_pool_address, + error::invalid_argument(E_PROOF_NOT_AT_TOKEN_POOL_ADDRESS) + ); + assert!( + proof_typeinfo.module_name() == token_pool_module_name, + error::invalid_argument(E_PROOF_NOT_IN_TOKEN_POOL_MODULE) + ); + + move_to( + token_pool_account, + TokenPoolConfig { + callbacks: TokenPoolCallbacks { lock_or_burn, release_or_mint }, + local_token + } + ); + } + public entry fun unregister_pool( caller: &signer, local_token: address ) acquires TokenAdminRegistryState, TokenPoolRegistration { @@ -439,7 +503,7 @@ module ccip::token_admin_registry { public entry fun set_pool( caller: &signer, local_token: address, token_pool_address: address - ) acquires TokenAdminRegistryState, TokenPoolRegistration { + ) acquires TokenAdminRegistryState, TokenPoolRegistration, TokenPoolConfig { assert!( object::object_exists(local_token), error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) @@ -447,8 +511,17 @@ module ccip::token_admin_registry { let caller_addr = signer::address_of(caller); + let pool_local_token = + if (exists(token_pool_address)) { + get_pool_local_token_v2(token_pool_address) + } else if (exists(token_pool_address)) { + get_registration(token_pool_address).local_token + } else { + abort error::invalid_argument(E_POOL_NOT_REGISTERED) + }; + assert!( - get_registration(token_pool_address).local_token == local_token, + pool_local_token == local_token, error::invalid_argument(E_INVALID_TOKEN_FOR_POOL) ); @@ -992,6 +1065,48 @@ module ccip::token_admin_registry { output.destination_amount } + public(friend) fun lock_or_burn_v2( + token_pool_address: address, + fa: fungible_asset::FungibleAsset, + sender: address, + remote_chain_selector: u64, + receiver: vector + ): (vector, vector) acquires TokenPoolConfig { + let pool_config = &TokenPoolConfig[token_pool_address]; + let input = LockOrBurnInputV1 { sender, remote_chain_selector, receiver }; + + (pool_config.callbacks.lock_or_burn) + (fa, input) + } + + public(friend) fun release_or_mint_v2( + token_pool_address: address, + sender: vector, + receiver: address, + source_amount: u256, + local_token: address, + remote_chain_selector: u64, + source_pool_address: vector, + source_pool_data: vector, + offchain_token_data: vector + ): (FungibleAsset, u64) acquires TokenPoolConfig { + let pool_config = &TokenPoolConfig[token_pool_address]; + let input = + ReleaseOrMintInputV1 { + sender, + receiver, + source_amount, + local_token, + remote_chain_selector, + source_pool_address, + source_pool_data, + offchain_token_data + }; + + (pool_config.callbacks.release_or_mint) + (input) + } + inline fun borrow_state(): &TokenAdminRegistryState { borrow_global(state_object::object_address()) } @@ -1021,7 +1136,7 @@ module ccip::token_admin_registry { public fun mcms_entrypoint( _metadata: Object - ): option::Option acquires TokenAdminRegistryState, TokenPoolRegistration { + ): option::Option acquires TokenAdminRegistryState, TokenPoolRegistration, TokenPoolConfig { let (caller, function, data) = mcms_registry::get_callback_params(@ccip, McmsCallback {}); diff --git a/contracts/ccip/ccip_offramp/Move.toml b/contracts/ccip/ccip_offramp/Move.toml index 98f51424..af90b91e 100644 --- a/contracts/ccip/ccip_offramp/Move.toml +++ b/contracts/ccip/ccip_offramp/Move.toml @@ -18,8 +18,13 @@ mcms_register_entrypoints = "0x4001" ccip_token_pool = "0x8d62e11f76e6e92563c59e7a5e842a540f8c6c3a4ed8a32f40a5ad3425b55f86" burn_mint_token_pool = "0x8e03eb21315649c06acb9a860a72c2b8cef5bd36775402008b89af397756dad7" lock_release_token_pool = "0x2e1f4cc8fbc2c7ccd2c67ff453e00526919098304d70708ef504af944d6fede8" +managed_token_pool = "0xc0a1d7586bddbd46c9b3445cf9ad82c020dcb82818e6092c9683b51bfecc6633" +regulated_token_pool = "0xb761d8e8425c11486ecb38444aab3493baf7c45381ce973a0bc99618c35021d5" burn_mint_local_token = "0x15c084d10b071a4b180c8d050e421a533bd07d13f9d9386335709da567183768" lock_release_local_token = "0x6d1c246126d36fea774b12486de0a3737997f1b9b23806c67ca5ce72859ff5fa" +managed_token = "0xfda529bc9c3e1cd8bd3df66bd96bb20a36553ed454c909a96e6a0dadb728e896" +regulated_token = "0xb3cec8e3442cafe0c378411012bbcae6787bfc0fbdd528ee9a00aaaf0c88d1b6" +admin = "0x100" [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", rev = "16beac69835f3a71564c96164a606a23f259099a", subdir = "aptos-move/framework/aptos-framework" } @@ -28,3 +33,5 @@ ChainlinkCCIP = { local = "../ccip" } [dev-dependencies] BurnMintTokenPool = { local = "../ccip_token_pools/burn_mint_token_pool" } LockReleaseTokenPool = { local = "../ccip_token_pools/lock_release_token_pool" } +ManagedTokenPool = { local = "../ccip_token_pools/managed_token_pool" } +RegulatedTokenPool = { local = "../ccip_token_pools/regulated_token_pool" } diff --git a/contracts/ccip/ccip_offramp/sources/offramp.move b/contracts/ccip/ccip_offramp/sources/offramp.move index 9a9b4b43..f0edd5de 100644 --- a/contracts/ccip/ccip_offramp/sources/offramp.move +++ b/contracts/ccip/ccip_offramp/sources/offramp.move @@ -1812,4 +1812,9 @@ module ccip_offramp::offramp { public fun merkle_root_merkle_root(root: &MerkleRoot): vector { root.merkle_root } + + #[test_only] + public fun source_chain_config_on_ramp(config: &SourceChainConfig): vector { + config.on_ramp + } } diff --git a/contracts/ccip/ccip_offramp/tests/mock/mock_ccip_receiver.move b/contracts/ccip/ccip_offramp/tests/mock/mock_ccip_receiver.move new file mode 100644 index 00000000..0b3a3a53 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/mock/mock_ccip_receiver.move @@ -0,0 +1,317 @@ +#[test_only] +/// Compatible with dispatchable and non-dispatchable tokens +/// When transferring tokens, use `primary_fungible_store::transfer` as this triggers the dispatchable fungible asset hook +module ccip_offramp::mock_ccip_receiver { + use std::account; + use std::event; + use std::object::{Self, Object}; + use std::string::{Self, String}; + use std::fungible_asset::{Self, Metadata}; + use std::option::{Self, Option}; + use std::primary_fungible_store; + use std::from_bcs; + use std::signer; + + use ccip::client; + use ccip::receiver_registry; + + #[event] + struct ReceivedMessage has store, drop { + message: String + } + + #[event] + struct ForwardedTokens has store, drop { + final_recipient: address + } + + #[event] + struct ReceivedTokensOnly has store, drop { + token_count: u64 + } + + struct CCIPReceiverState has key { + signer_cap: account::SignerCapability, + received_message_handle: event::EventHandle, + forwarded_tokens_handle: event::EventHandle, + received_tokens_only_handle: event::EventHandle + } + + const E_RESOURCE_NOT_FOUND_ON_ACCOUNT: u64 = 1; + const E_UNAUTHORIZED: u64 = 2; + const E_INVALID_TOKEN_ADDRESS: u64 = 3; + const E_NO_TOKENS_AVAILABLE_TO_WITHDRAW: u64 = 4; + + #[view] + public fun type_and_version(): String { + string::utf8(b"MockCCIPReceiver 1.6.0") + } + + const MODULE_NAME: vector = b"mock_ccip_receiver"; + + fun init_module(publisher: &signer) { + // Create a signer capability for the receiver account + let signer_cap = account::create_test_signer_cap(signer::address_of(publisher)); + + // Create a unique handle for each event type + let received_message_handle = + account::new_event_handle(publisher); + let forwarded_tokens_handle = + account::new_event_handle(publisher); + let received_tokens_only_handle = + account::new_event_handle(publisher); + + // Move all state into the single resource struct + move_to( + publisher, + CCIPReceiverState { + signer_cap, + received_message_handle, + forwarded_tokens_handle, + received_tokens_only_handle + } + ); + + // Default to V2 registration + receiver_registry::register_receiver_v2( + publisher, + MODULE_NAME, + |message| ccip_receive_v2(message), + CCIPReceiverProof {} + ); + } + + /// Register this receiver as V1 (dispatchable fungible asset mode) + /// This is used for testing V1 compatibility + public fun register_as_v1(publisher: &signer) { + receiver_registry::register_receiver(publisher, MODULE_NAME, CCIPReceiverProof {}); + } + + /// Migrate from V1 to V2 registration + /// This demonstrates the upgrade path from dispatchable FA to closures + public fun migrate_to_v2(publisher: &signer) { + // V2 registration will coexist with V1 + // The dispatcher will prefer V2 when both exist + receiver_registry::register_receiver_v2( + publisher, + MODULE_NAME, + |message| ccip_receive_v2(message), + CCIPReceiverProof {} + ); + } + + #[view] + public fun get_state_address(): address acquires CCIPReceiverState { + let state = borrow_global(@ccip_offramp); + let state_signer = account::create_signer_with_capability(&state.signer_cap); + signer::address_of(&state_signer) + } + + struct CCIPReceiverProof has drop {} + + public fun ccip_receive_v2(message: client::Any2AptosMessage) acquires CCIPReceiverState { + /* load state and rebuild a signer for the resource account */ + let state = borrow_global_mut(@ccip_offramp); + let state_signer = account::create_signer_with_capability(&state.signer_cap); + + let data = client::get_data(&message); + + let dest_token_amounts = client::get_dest_token_amounts(&message); + + if (dest_token_amounts.length() != 0 && data.length() != 0) { + let final_recipient = from_bcs::to_address(data); + + for (i in 0..dest_token_amounts.length()) { + let token_amount_ref = &dest_token_amounts[i]; + let token_addr = client::get_token(token_amount_ref); + let amount = client::get_amount(token_amount_ref); + + // Implement the token transfer logic here + + let fa_token = object::address_to_object(token_addr); + + // Must use primary_fungible_store::transfer as token may be dispatchable + primary_fungible_store::transfer( + &state_signer, + fa_token, + final_recipient, + amount + ); + }; + + event::emit(ForwardedTokens { final_recipient }); + event::emit_event( + &mut state.forwarded_tokens_handle, ForwardedTokens { final_recipient } + ); + } else if (data.length() != 0) { + // Convert the vector to a string + let message = string::utf8(data); + + event::emit(ReceivedMessage { message }); + event::emit_event( + &mut state.received_message_handle, ReceivedMessage { message } + ); + + } else if (dest_token_amounts.length() != 0) { + // Tokens only (no forwarding data) - keep them at receiver + // Emit event to prove receiver was called + let token_count = dest_token_amounts.length(); + event::emit(ReceivedTokensOnly { token_count }); + event::emit_event( + &mut state.received_tokens_only_handle, + ReceivedTokensOnly { token_count } + ); + }; + + // Simple abort condition for testing + if (data == b"abort") { + abort 1 + }; + } + + #[deprecated] + /// Legacy V1 receive function, use ccip_receive_v2 as this supports dispatchable tokens + /// Only switch to v2 once TokenPools are migrated to V2 + public fun ccip_receive(_metadata: Object): Option acquires CCIPReceiverState { + /* load state and rebuild a signer for the resource account */ + let state = borrow_global_mut(@ccip_offramp); + let state_signer = account::create_signer_with_capability(&state.signer_cap); + + let message = + receiver_registry::get_receiver_input(@ccip_offramp, CCIPReceiverProof {}); + + let data = client::get_data(&message); + + let dest_token_amounts = client::get_dest_token_amounts(&message); + + if (dest_token_amounts.length() != 0 && data.length() != 0) { + let final_recipient = from_bcs::to_address(data); + + for (i in 0..dest_token_amounts.length()) { + let token_amount_ref = &dest_token_amounts[i]; + let token_addr = client::get_token(token_amount_ref); + let amount = client::get_amount(token_amount_ref); + + // Implement the token transfer logic here + + let fa_token = object::address_to_object(token_addr); + let fa_store_sender = + primary_fungible_store::ensure_primary_store_exists( + @ccip_offramp, fa_token + ); + let fa_store_receiver = + primary_fungible_store::ensure_primary_store_exists( + final_recipient, fa_token + ); + + fungible_asset::transfer( + &state_signer, + fa_store_sender, + fa_store_receiver, + amount + ); + }; + + event::emit(ForwardedTokens { final_recipient }); + event::emit_event( + &mut state.forwarded_tokens_handle, ForwardedTokens { final_recipient } + ); + + } else if (data.length() != 0) { + + // Convert the vector to a string + let message = string::utf8(data); + + event::emit(ReceivedMessage { message }); + event::emit_event( + &mut state.received_message_handle, ReceivedMessage { message } + ); + + } else if (dest_token_amounts.length() != 0) { + // Tokens only (no forwarding data) - keep them at receiver + // Emit event to prove receiver was called + let token_count = dest_token_amounts.length(); + event::emit(ReceivedTokensOnly { token_count }); + event::emit_event( + &mut state.received_tokens_only_handle, + ReceivedTokensOnly { token_count } + ); + }; + + // Simple abort condition for testing + if (data == b"abort") { + abort 1 + }; + + option::none() + } + + public entry fun withdraw_token( + sender: &signer, recipient: address, token_address: address + ) acquires CCIPReceiverState { + assert!( + exists(@ccip_offramp), E_RESOURCE_NOT_FOUND_ON_ACCOUNT + ); + assert!(signer::address_of(sender) == @ccip_offramp, E_UNAUTHORIZED); + + let state = borrow_global_mut(@ccip_offramp); + let state_signer = account::create_signer_with_capability(&state.signer_cap); + + let fa_token = object::address_to_object(token_address); + let balance = primary_fungible_store::balance(@ccip_offramp, fa_token); + + assert!(balance > 0, E_NO_TOKENS_AVAILABLE_TO_WITHDRAW); + + primary_fungible_store::transfer(&state_signer, fa_token, recipient, balance); + } + + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } + + /// Initialize without auto-registering (for testing V1/V2 manually) + public fun test_init_state_only(publisher: &signer) { + // Create a signer capability for the receiver account + let signer_cap = account::create_test_signer_cap(signer::address_of(publisher)); + + // Create a unique handle for each event type + let received_message_handle = + account::new_event_handle(publisher); + let forwarded_tokens_handle = + account::new_event_handle(publisher); + let received_tokens_only_handle = + account::new_event_handle(publisher); + + // Move all state into the single resource struct + move_to( + publisher, + CCIPReceiverState { + signer_cap, + received_message_handle, + forwarded_tokens_handle, + received_tokens_only_handle + } + ); + } + + public fun get_received_message_events(): vector acquires CCIPReceiverState { + let state = borrow_global(@ccip_offramp); + event::emitted_events_by_handle(&state.received_message_handle) + } + + public fun get_forwarded_tokens_events(): vector acquires CCIPReceiverState { + let state = borrow_global(@ccip_offramp); + event::emitted_events_by_handle(&state.forwarded_tokens_handle) + } + + public fun get_received_tokens_only_events(): vector acquires CCIPReceiverState { + let state = borrow_global(@ccip_offramp); + event::emitted_events_by_handle( + &state.received_tokens_only_handle + ) + } + + public fun received_message_get_message(event: &ReceivedMessage): String { + event.message + } +} diff --git a/contracts/ccip/ccip_offramp/tests/mock/mock_token.move b/contracts/ccip/ccip_offramp/tests/mock/mock_token.move new file mode 100644 index 00000000..08293318 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/mock/mock_token.move @@ -0,0 +1,44 @@ +#[test_only] +module ccip_offramp::mock_token { + use std::fungible_asset::{Self, FungibleAsset, TransferRef}; + use std::object::{Object, ConstructorRef}; + use std::string::{Self}; + use std::option::{Self}; + use std::function_info; + use std::dispatchable_fungible_asset; + + public fun add_dynamic_dispatch_function( + ccip_onramp_signer: &signer, constructor_ref: &ConstructorRef + ) { + let deposit = + function_info::new_function_info( + ccip_onramp_signer, + string::utf8(b"mock_token"), + string::utf8(b"deposit") + ); + let withdraw = + function_info::new_function_info( + ccip_onramp_signer, + string::utf8(b"mock_token"), + string::utf8(b"withdraw") + ); + dispatchable_fungible_asset::register_dispatch_functions( + constructor_ref, + option::some(withdraw), + option::some(deposit), + option::none() + ); + } + + public fun deposit( + store: Object, fa: FungibleAsset, transfer_ref: &TransferRef + ) { + fungible_asset::deposit_with_ref(transfer_ref, store, fa); + } + + public fun withdraw( + store: Object, amount: u64, transfer_ref: &TransferRef + ): FungibleAsset { + fungible_asset::withdraw_with_ref(transfer_ref, store, amount) + } +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_burn_mint_receiver_test.move b/contracts/ccip/ccip_offramp/tests/offramp_burn_mint_receiver_test.move new file mode 100644 index 00000000..c3127672 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/offramp_burn_mint_receiver_test.move @@ -0,0 +1,571 @@ +#[test_only] +module ccip_offramp::offramp_burn_mint_receiver_test { + use std::account; + use std::fungible_asset; + use std::object; + use std::primary_fungible_store; + use std::signer; + use std::string; + use std::bcs; + use std::timestamp; + + use ccip_offramp::offramp_test; + use ccip_offramp::offramp; + use ccip_offramp::mock_ccip_receiver; + use ccip::receiver_registry; + + const BURN_MINT_TOKEN_POOL: u8 = 0; + const BURN_MINT_TOKEN_SEED: vector = b"TestToken"; + const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; + const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; + const EVM_SENDER: vector = x"d87929a32cf0cbdc9e2d07ffc7c33344079de727"; + const GAS_LIMIT: u64 = 1000000; + + fun setup_mock_ccip_receiver(owner: &signer, ccip_offramp: &signer) { + account::create_account_if_does_not_exist(signer::address_of(ccip_offramp)); + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + } + + struct TestMessage has drop { + message: offramp::Any2AptosRampMessage, + merkle_root: vector, + proofs: vector> + } + + fun create_and_commit_message( + message_id: vector, + sequence_number: u64, + receiver: address, + data: vector, + token_amounts: vector + ): TestMessage { + let static_config = offramp::get_static_config(); + let dest_chain_selector = offramp::chain_selector(&static_config); + + let header = + offramp::test_create_ramp_message_header( + message_id, + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + sequence_number, + 0 + ); + + let message = + offramp::test_create_any2aptos_ramp_message( + header, + EVM_SENDER, + data, + receiver, + (GAS_LIMIT as u256), + token_amounts + ); + + let source_chain_config = + offramp::get_source_chain_config(EVM_SOURCE_CHAIN_SELECTOR); + let on_ramp = offramp::source_chain_config_on_ramp(&source_chain_config); + + let metadata_hash = + offramp::test_calculate_metadata_hash( + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + on_ramp + ); + + let message_hash = offramp::test_calculate_message_hash(&message, metadata_hash); + let merkle_root = message_hash; + let proofs = vector[]; + + offramp::test_add_root(merkle_root, timestamp::now_seconds() - 3700); + + TestMessage { message, merkle_root, proofs } + } + + fun execute_message_and_verify_success( + sequence_number: u64, + test_message: TestMessage, + offchain_token_data: vector> + ) { + let TestMessage { message, merkle_root: _, proofs } = test_message; + + let report = + offramp::test_create_execution_report( + EVM_SOURCE_CHAIN_SELECTOR, + message, + offchain_token_data, + proofs + ); + + offramp::test_execute_single_report(report); + + let execution_state = + offramp::get_execution_state(EVM_SOURCE_CHAIN_SELECTOR, sequence_number); + assert!(execution_state == 2); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // token is non-dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000001", + 1, + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(1, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // token is non-dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000002", + 2, + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(2, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_non_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // token is non-dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + // Sending 200,000 tokens to receiver contract first + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000003", + 3, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(3, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + true, // token is dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000004", + 4, // sequence number + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(4, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + true, // token is dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000005", + 5, // sequence number + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(5, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + true, // token is dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000006", + 6, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(6, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_lock_release_receiver_test.move b/contracts/ccip/ccip_offramp/tests/offramp_lock_release_receiver_test.move new file mode 100644 index 00000000..564ec8e4 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/offramp_lock_release_receiver_test.move @@ -0,0 +1,571 @@ +#[test_only] +module ccip_offramp::offramp_lock_release_receiver_test { + use std::account; + use std::fungible_asset; + use std::object; + use std::primary_fungible_store; + use std::signer; + use std::string; + use std::bcs; + use std::timestamp; + + use ccip_offramp::offramp_test; + use ccip_offramp::offramp; + use ccip_offramp::mock_ccip_receiver; + use ccip::receiver_registry; + + const LOCK_RELEASE_TOKEN_POOL: u8 = 1; + const LOCK_RELEASE_TOKEN_SEED: vector = b"LockReleaseToken"; + const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; + const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; + const EVM_SENDER: vector = x"d87929a32cf0cbdc9e2d07ffc7c33344079de727"; + const GAS_LIMIT: u64 = 1000000; + + fun setup_mock_ccip_receiver(owner: &signer, ccip_offramp: &signer) { + account::create_account_if_does_not_exist(signer::address_of(ccip_offramp)); + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + } + + struct TestMessage has drop { + message: offramp::Any2AptosRampMessage, + merkle_root: vector, + proofs: vector> + } + + fun create_and_commit_message( + message_id: vector, + sequence_number: u64, + receiver: address, + data: vector, + token_amounts: vector + ): TestMessage { + let static_config = offramp::get_static_config(); + let dest_chain_selector = offramp::chain_selector(&static_config); + + let header = + offramp::test_create_ramp_message_header( + message_id, + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + sequence_number, + 0 + ); + + let message = + offramp::test_create_any2aptos_ramp_message( + header, + EVM_SENDER, + data, + receiver, + (GAS_LIMIT as u256), + token_amounts + ); + + let source_chain_config = + offramp::get_source_chain_config(EVM_SOURCE_CHAIN_SELECTOR); + let on_ramp = offramp::source_chain_config_on_ramp(&source_chain_config); + + let metadata_hash = + offramp::test_calculate_metadata_hash( + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + on_ramp + ); + + let message_hash = offramp::test_calculate_message_hash(&message, metadata_hash); + let merkle_root = message_hash; + let proofs = vector[]; + + offramp::test_add_root(merkle_root, timestamp::now_seconds() - 3700); + + TestMessage { message, merkle_root, proofs } + } + + fun execute_message_and_verify_success( + sequence_number: u64, + test_message: TestMessage, + offchain_token_data: vector> + ) { + let TestMessage { message, merkle_root: _, proofs } = test_message; + + let report = + offramp::test_create_execution_report( + EVM_SOURCE_CHAIN_SELECTOR, + message, + offchain_token_data, + proofs + ); + + offramp::test_execute_single_report(report); + + let execution_state = + offramp::get_execution_state(EVM_SOURCE_CHAIN_SELECTOR, sequence_number); + assert!(execution_state == 2); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000001", + 1, + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(1, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // token is non-dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000002", + 2, + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(2, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_non_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + // Sending 200,000 tokens to receiver contract first + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000003", + 3, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(3, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000004", + 4, // sequence number + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(4, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000005", + 5, // sequence number + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(5, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + true, // token is dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000006", + 6, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(6, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_managed_receiver_test.move b/contracts/ccip/ccip_offramp/tests/offramp_managed_receiver_test.move new file mode 100644 index 00000000..43a54d94 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/offramp_managed_receiver_test.move @@ -0,0 +1,647 @@ +#[test_only] +module ccip_offramp::offramp_managed_receiver_test { + use std::account; + use std::fungible_asset; + use std::object; + use std::primary_fungible_store; + use std::signer; + use std::bcs; + use std::string; + use std::timestamp; + + use ccip_offramp::offramp_test; + use ccip_offramp::offramp; + use ccip_offramp::mock_ccip_receiver; + use ccip::receiver_registry; + use managed_token::managed_token; + use managed_token_pool::managed_token_pool; + + const MANAGED_TOKEN_POOL: u8 = 2; + const MANAGED_TOKEN_SEED: vector = b"ManagedToken"; + + const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; + const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; + const EVM_SENDER: vector = x"d87929a32cf0cbdc9e2d07ffc7c33344079de727"; + const GAS_LIMIT: u64 = 1000000; + + fun setup_mock_ccip_receiver(owner: &signer, ccip_offramp: &signer) { + account::create_account_if_does_not_exist(signer::address_of(ccip_offramp)); + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + } + + struct TestMessage has drop { + message: offramp::Any2AptosRampMessage, + merkle_root: vector, + proofs: vector> + } + + fun create_and_commit_message( + message_id: vector, + sequence_number: u64, + receiver: address, + data: vector, + token_amounts: vector + ): TestMessage { + let static_config = offramp::get_static_config(); + let dest_chain_selector = offramp::chain_selector(&static_config); + + let header = + offramp::test_create_ramp_message_header( + message_id, + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + sequence_number, + 0 + ); + + let message = + offramp::test_create_any2aptos_ramp_message( + header, + EVM_SENDER, + data, + receiver, + (GAS_LIMIT as u256), + token_amounts + ); + + let source_chain_config = + offramp::get_source_chain_config(EVM_SOURCE_CHAIN_SELECTOR); + let on_ramp = offramp::source_chain_config_on_ramp(&source_chain_config); + + let metadata_hash = + offramp::test_calculate_metadata_hash( + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + on_ramp + ); + + let message_hash = offramp::test_calculate_message_hash(&message, metadata_hash); + let merkle_root = message_hash; + let proofs = vector[]; + + offramp::test_add_root(merkle_root, timestamp::now_seconds() - 3700); + + TestMessage { message, merkle_root, proofs } + } + + fun execute_message_and_verify_success( + sequence_number: u64, + test_message: TestMessage, + offchain_token_data: vector> + ) { + let TestMessage { message, merkle_root: _, proofs } = test_message; + + let report = + offramp::test_create_execution_report( + EVM_SOURCE_CHAIN_SELECTOR, + message, + offchain_token_data, + proofs + ); + + offramp::test_execute_single_report(report); + + let execution_state = + offramp::get_execution_state(EVM_SOURCE_CHAIN_SELECTOR, sequence_number); + assert!(execution_state == 2); + } + + // ======================== NON DISPATCHABLE TESTS ======================== + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Add to allowlist for release_or_mint + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[pool_address] + ); + + // Add to allowlist for lock_or_burn + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[pool_address] + ); + + // // Grant receiver state signer minter and burner role for transfer during forwarding + // let state_address = managed_dispatchable_receiver::get_state_address(); + // managed_token::apply_allowed_minter_updates(owner, vector[], vector[state_address]); + // managed_token::apply_allowed_burner_updates(owner, vector[], vector[state_address]); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000004", + 4, // sequence number + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(4, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_non_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Add to allowlist for lock_or_burn/release_or_mint + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[pool_address] + ); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[pool_address] + ); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000002", + 2, + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(2, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_non_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Add to allowlist for lock_or_burn/release_or_mint + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[pool_address] + ); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[pool_address] + ); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + // Sending 200,000 tokens to receiver contract first + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000003", + 3, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(3, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Add to allowlist for lock_or_burn/release_or_mint + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[pool_address] + ); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[pool_address] + ); + + // Grant receiver state signer minter and burner role for transfer during forwarding + let state_address = mock_ccip_receiver::get_state_address(); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[state_address] + ); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[state_address] + ); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000004", + 4, // sequence number + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(4, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000005", + 5, // sequence number + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(5, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + MANAGED_TOKEN_POOL, + MANAGED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Add to allowlist for lock_or_burn/release_or_mint + let pool_address = managed_token_pool::get_store_address(); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[pool_address] + ); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[pool_address] + ); + + // Grant receiver state signer BRIDGE_MINTER_OR_BURNER_ROLE (role 6) for transfer during forwarding + let state_address = mock_ccip_receiver::get_state_address(); + managed_token::apply_allowed_minter_updates( + owner, vector[], vector[state_address] + ); + managed_token::apply_allowed_burner_updates( + owner, vector[], vector[state_address] + ); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000006", + 6, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(6, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_regulated_receiver_test.move b/contracts/ccip/ccip_offramp/tests/offramp_regulated_receiver_test.move new file mode 100644 index 00000000..f9bdb9f6 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/offramp_regulated_receiver_test.move @@ -0,0 +1,360 @@ +#[test_only] +/// Regulated token is always dispatchable, therefore we test the dispatchable token transfer only +module ccip_offramp::offramp_regulated_receiver_test { + use std::account; + use std::fungible_asset; + use std::object; + use std::primary_fungible_store; + use std::signer; + use std::timestamp; + + use ccip_offramp::offramp_test; + use ccip_offramp::offramp; + use ccip_offramp::mock_ccip_receiver; + use ccip::receiver_registry; + use regulated_token::regulated_token; + use regulated_token_pool::regulated_token_pool; + + const REGULATED_TOKEN_POOL: u8 = 3; + const REGULATED_TOKEN_SEED: vector = b"RegulatedToken"; + + const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; + const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; + const EVM_SENDER: vector = x"d87929a32cf0cbdc9e2d07ffc7c33344079de727"; + const GAS_LIMIT: u64 = 1000000; + + fun setup_mock_ccip_receiver(owner: &signer, ccip_offramp: &signer) { + account::create_account_if_does_not_exist(signer::address_of(ccip_offramp)); + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + } + + struct TestMessage has drop { + message: offramp::Any2AptosRampMessage, + merkle_root: vector, + proofs: vector> + } + + fun create_and_commit_message( + message_id: vector, + sequence_number: u64, + receiver: address, + data: vector, + token_amounts: vector + ): TestMessage { + let static_config = offramp::get_static_config(); + let dest_chain_selector = offramp::chain_selector(&static_config); + + let header = + offramp::test_create_ramp_message_header( + message_id, + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + sequence_number, + 0 + ); + + let message = + offramp::test_create_any2aptos_ramp_message( + header, + EVM_SENDER, + data, + receiver, + (GAS_LIMIT as u256), + token_amounts + ); + + let source_chain_config = + offramp::get_source_chain_config(EVM_SOURCE_CHAIN_SELECTOR); + let on_ramp = offramp::source_chain_config_on_ramp(&source_chain_config); + + let metadata_hash = + offramp::test_calculate_metadata_hash( + EVM_SOURCE_CHAIN_SELECTOR, + dest_chain_selector, + on_ramp + ); + + let message_hash = offramp::test_calculate_message_hash(&message, metadata_hash); + let merkle_root = message_hash; + let proofs = vector[]; + + offramp::test_add_root(merkle_root, timestamp::now_seconds() - 3700); + + TestMessage { message, merkle_root, proofs } + } + + fun execute_message_and_verify_success( + sequence_number: u64, + test_message: TestMessage, + offchain_token_data: vector> + ) { + let TestMessage { message, merkle_root: _, proofs } = test_message; + + let report = + offramp::test_create_execution_report( + EVM_SOURCE_CHAIN_SELECTOR, + message, + offchain_token_data, + proofs + ); + + offramp::test_execute_single_report(report); + + let execution_state = + offramp::get_execution_state(EVM_SOURCE_CHAIN_SELECTOR, sequence_number); + assert!(execution_state == 2); + } + + // ======================== DISPATCHABLE TESTS ======================== // + + // Regulated token is always dispatchable, therefore we test the dispatchable token transfer only + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_token_transfer_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + REGULATED_TOKEN_POOL, + REGULATED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Grant pool signer BRIDGE_MINTER_OR_BURNER_ROLE (role 6) for release_or_mint + let pool_address = regulated_token_pool::get_store_address(); + regulated_token::grant_role(owner, 6, pool_address); + + // Grant receiver state signer BRIDGE_MINTER_OR_BURNER_ROLE (role 6) for transfer during forwarding + let state_signer_address = mock_ccip_receiver::get_state_address(); + regulated_token::grant_role(owner, 6, state_signer_address); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 100000 + ); + + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000004", + 4, // sequence number + @ccip_offramp, // receiver + vector[], + vector[token_amounts] + ); + + execute_message_and_verify_success(4, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 100000); + + let tokens_only_events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(tokens_only_events.length() == 1); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_execute_dispatchable_message_only( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, _token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + REGULATED_TOKEN_POOL, + REGULATED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + let test_data = b"Hello from EVM chain!"; + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000005", + 5, // sequence number + @ccip_offramp, // receiver + test_data, + vector[] + ); + + execute_message_and_verify_success(5, test_message, vector[]); + + let received_events = mock_ccip_receiver::get_received_message_events(); + assert!(received_events.length() == 1); + + let event = received_events.borrow(0); + let event_message = mock_ccip_receiver::received_message_get_message(event); + assert!(event_message == std::string::utf8(test_data)); + } + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @admin, + recipient = @0x999, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_receiver_dispatchable_tokens_with_forwarding( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + recipient: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + REGULATED_TOKEN_POOL, + REGULATED_TOKEN_SEED, + true, // is_dispatchable + false // use_v1_init + ); + let token_addr = object::object_address(&token_obj); + + setup_mock_ccip_receiver(owner, ccip_offramp); + + // Grant pool signer BRIDGE_MINTER_OR_BURNER_ROLE (role 6) for release_or_mint + let pool_address = regulated_token_pool::get_store_address(); + regulated_token::grant_role(owner, 6, pool_address); + + // Grant receiver state signer BRIDGE_MINTER_OR_BURNER_ROLE (role 6) for transfer during forwarding + let state_signer_address = mock_ccip_receiver::get_state_address(); + regulated_token::grant_role(owner, 6, state_signer_address); + + let recipient_addr = signer::address_of(recipient); + account::create_account_for_test(recipient_addr); + + let token_amounts = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + 200000 + ); + + let test_data = std::bcs::to_bytes(&recipient_addr); + let test_message = + create_and_commit_message( + x"0000000000000000000000000000000000000000000000000000000000000006", + 6, // sequence number + @ccip_offramp, // receiver + test_data, + vector[token_amounts] + ); + + execute_message_and_verify_success(6, test_message, vector[vector[]]); + + let token_obj = object::address_to_object(token_addr); + + let receiver_store = + primary_fungible_store::primary_store(@ccip_offramp, token_obj); + let receiver_balance = fungible_asset::balance(receiver_store); + assert!(receiver_balance == 0); + + let recipient_store = + primary_fungible_store::primary_store(recipient_addr, token_obj); + let recipient_balance = fungible_asset::balance(recipient_store); + assert!(recipient_balance == 200000); + + let forwarded_events = mock_ccip_receiver::get_forwarded_tokens_events(); + assert!(forwarded_events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_offramp/tests/offramp_test.move b/contracts/ccip/ccip_offramp/tests/offramp_test.move index 50d4561e..cbf5b7ff 100644 --- a/contracts/ccip/ccip_offramp/tests/offramp_test.move +++ b/contracts/ccip/ccip_offramp/tests/offramp_test.move @@ -10,6 +10,7 @@ module ccip_offramp::offramp_test { use aptos_framework::primary_fungible_store; use aptos_framework::timestamp; use ccip_offramp::bcs_helper; + use ccip_offramp::mock_token; use ccip::merkle_proof; use ccip::token_admin_registry; @@ -23,6 +24,10 @@ module ccip_offramp::offramp_test { use burn_mint_token_pool::burn_mint_token_pool; use lock_release_token_pool::lock_release_token_pool; + use managed_token::managed_token; + use managed_token_pool::managed_token_pool; + use regulated_token_pool::regulated_token_pool; + use regulated_token::regulated_token; const CHAIN_ID: u8 = 100; const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; @@ -41,9 +46,13 @@ module ccip_offramp::offramp_test { const BURN_MINT_TOKEN_POOL: u8 = 0; const LOCK_RELEASE_TOKEN_POOL: u8 = 1; + const MANAGED_TOKEN_POOL: u8 = 2; + const REGULATED_TOKEN_POOL: u8 = 3; const BURN_MINT_TOKEN_SEED: vector = b"TestToken"; const LOCK_RELEASE_TOKEN_SEED: vector = b"LockReleaseToken"; + const MANAGED_TOKEN_SEED: vector = b"ManagedToken"; + const REGULATED_TOKEN_SEED: vector = b"RegulatedToken"; const MOCK_EVM_ADDRESS: address = @0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97; const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; @@ -61,15 +70,23 @@ module ccip_offramp::offramp_test { timestamp::update_global_time_for_test_secs(timestamp_seconds); } - fun setup( + /// use_v1_init: if true, uses test_init_v1 for token pools (V1 compatibility mode) + /// if false, uses test_init_module for token pools (V2 mode, default) + public fun setup( aptos_framework: &signer, ccip: &signer, ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer, pool_type: u8, - seed: vector + seed: vector, + is_dispatchable: bool, + use_v1_init: bool ): (address, Object) { let owner_addr = signer::address_of(owner); account::create_account_for_test(signer::address_of(burn_mint_token_pool)); @@ -124,10 +141,17 @@ module ccip_offramp::offramp_test { let (token_obj, token_addr) = create_test_token_and_pool( owner, + ccip_offramp, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, pool_type, - seed + seed, + is_dispatchable, + use_v1_init ); // Initialize fee quoter @@ -153,10 +177,17 @@ module ccip_offramp::offramp_test { fun create_test_token_and_pool( owner: &signer, + ccip_offramp_signer: &signer, burn_mint_token_pool: &signer, lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer, pool_type: u8, - seed: vector + seed: vector, + is_dispatchable: bool, + use_v1_init: bool ): (Object, address) { let constructor_ref = object::create_named_object(owner, seed); @@ -178,10 +209,21 @@ module ccip_offramp::offramp_test { let burn_ref = fungible_asset::generate_burn_ref(&constructor_ref); let transfer_ref = fungible_asset::generate_transfer_ref(&constructor_ref); + // Add dynamic dispatch function to the token if it is dispatchable + if (is_dispatchable) { + mock_token::add_dynamic_dispatch_function( + ccip_offramp_signer, &constructor_ref + ); + }; + let token_addr = object::object_address(&metadata); if (pool_type == BURN_MINT_TOKEN_POOL) { - burn_mint_token_pool::test_init_module(burn_mint_token_pool); + if (use_v1_init) { + burn_mint_token_pool::test_init_v1(burn_mint_token_pool); + } else { + burn_mint_token_pool::test_init_module(burn_mint_token_pool); + }; burn_mint_token_pool::initialize(owner, burn_ref, mint_ref); burn_mint_token_pool::apply_chain_updates( owner, @@ -210,8 +252,12 @@ module ccip_offramp::offramp_test { token_addr, signer::address_of(burn_mint_token_pool) ); - } else { - lock_release_token_pool::test_init_module(lock_release_token_pool); + } else if (pool_type == LOCK_RELEASE_TOKEN_POOL) { + if (use_v1_init) { + lock_release_token_pool::test_init_v1(lock_release_token_pool); + } else { + lock_release_token_pool::test_init_module(lock_release_token_pool); + }; lock_release_token_pool::initialize( owner, option::some(transfer_ref), @@ -244,17 +290,149 @@ module ccip_offramp::offramp_test { token_addr, signer::address_of(lock_release_token_pool) ); + } else if (pool_type == MANAGED_TOKEN_POOL) { + let seed = b"MT"; + let _constructor_ref = &object::create_named_object(owner, seed); + let _managed_token_pool_constructor_ref = + &object::create_named_object(owner, b"ManagedTokenPool"); + + managed_token::init_module_for_testing(managed_token); + managed_token::initialize( + owner, + option::none(), + string::utf8(b"Managed Token"), + string::utf8(seed), + 6, + string::utf8(b"https://managedtoken.com/images/pic.png"), + string::utf8(b"https://managedtoken.com") + ); + + managed_token_pool::test_init_module(managed_token_pool); + managed_token_pool::apply_chain_updates( + owner, + vector[], + vector[EVM_SOURCE_CHAIN_SELECTOR], + vector[vector[MOCK_EVM_ADDRESS_VECTOR]], + vector[MOCK_EVM_ADDRESS_VECTOR] + ); + managed_token_pool::set_chain_rate_limiter_config( + owner, + EVM_SOURCE_CHAIN_SELECTOR, + true, + OUTBOUND_CAPACITY, + OUTBOUND_RATE, + true, + INBOUND_CAPACITY, + INBOUND_RATE + ); + + token_addr = managed_token::token_metadata(); + metadata = object::address_to_object(token_addr); + + // Set admin for token + token_admin_registry::propose_administrator( + owner, token_addr, signer::address_of(owner) + ); + token_admin_registry::accept_admin_role(owner, token_addr); + token_admin_registry::set_pool( + owner, + token_addr, + signer::address_of(managed_token_pool) + ); + // Fund managed token pool + primary_fungible_store::mint( + &mint_ref, + managed_token_pool::get_store_address(), + 1000 + ); + } else if (pool_type == REGULATED_TOKEN_POOL) { + account::create_account_for_test(signer::address_of(owner)); + account::create_account_for_test(signer::address_of(regulated_token_pool)); + account::create_account_for_test(signer::address_of(regulated_token)); + + // Create an object at @regulated_token for the ownable functionality + let regulated_token_pool_constructor_ref = + object::create_named_object(owner, b"regulated_token_pool"); + account::create_account_for_test( + object::address_from_constructor_ref( + ®ulated_token_pool_constructor_ref + ) + ); + + // Setup regulated token first (use admin as the object creator) + let regulated_token_constructor_ref = + object::create_named_object(owner, b"regulated_token"); + account::create_account_for_test( + object::address_from_constructor_ref(®ulated_token_constructor_ref) + ); + + regulated_token::init_module_for_testing(regulated_token); + regulated_token::initialize( + owner, + option::none(), + string::utf8(b"Regulated Token"), + string::utf8(b"RT"), + 6, + string::utf8( + b"https://regulatedtoken.com/images/pic.png" + ), + string::utf8(b"https://regulatedtoken.com") + ); + + if (use_v1_init) { + regulated_token_pool::test_init_v1(regulated_token_pool); + } else { + regulated_token_pool::test_init_module(regulated_token_pool); + }; + regulated_token_pool::apply_chain_updates( + owner, + vector[], + vector[EVM_SOURCE_CHAIN_SELECTOR], + vector[vector[MOCK_EVM_ADDRESS_VECTOR]], + vector[MOCK_EVM_ADDRESS_VECTOR] + ); + regulated_token_pool::set_chain_rate_limiter_config( + owner, + EVM_SOURCE_CHAIN_SELECTOR, + true, + OUTBOUND_CAPACITY, + OUTBOUND_RATE, + true, + INBOUND_CAPACITY, + INBOUND_RATE + ); + + token_addr = regulated_token::token_address(); + metadata = regulated_token::token_metadata(); + + // Set admin for token + token_admin_registry::propose_administrator( + owner, token_addr, signer::address_of(owner) + ); + token_admin_registry::accept_admin_role(owner, token_addr); + token_admin_registry::set_pool( + owner, + token_addr, + signer::address_of(regulated_token_pool) + ); + + // Fund regulated token pool + primary_fungible_store::mint( + &mint_ref, + regulated_token_pool::get_store_address(), + 1000 + ); }; let mint_ref = fungible_asset::generate_mint_ref(&constructor_ref); let burn_ref = fungible_asset::generate_burn_ref(&constructor_ref); let transfer_ref = fungible_asset::generate_transfer_ref(&constructor_ref); - // Fund lock/release token pool + // Fund lock/release token pool with sufficient liquidity for tests primary_fungible_store::mint( &mint_ref, lock_release_token_pool::get_store_address(), - 1000 + 10000000 // 10M tokens for test liquidity ); move_to( @@ -265,7 +443,7 @@ module ccip_offramp::offramp_test { (metadata, token_addr) } - fun initialize_offramp(owner: &signer): address { + public fun initialize_offramp(owner: &signer): address { offramp::initialize( owner, DEST_CHAIN_SELECTOR, @@ -279,7 +457,7 @@ module ccip_offramp::offramp_test { offramp::get_state_address() } - fun setup_fee_quoter( + public fun setup_fee_quoter( owner: &signer, ccip_offramp: &signer, token_addr: address ) { fee_quoter::apply_fee_token_updates(owner, vector[], vector[token_addr]); @@ -354,7 +532,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] fun test_initialize( @@ -363,7 +545,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -372,8 +558,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); // Verify initialization was successful @@ -762,7 +954,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] fun test_commit_and_execute( @@ -771,7 +967,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -780,8 +980,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); let config_digest = x"000aed76a87f048dab766bc14ecdbb966f4253e309d742585062a75abfc16c38"; @@ -943,7 +1149,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] fun test_manually_execute( @@ -952,7 +1162,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -961,8 +1175,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); let merkle_root = @@ -985,6 +1205,10 @@ module ccip_offramp::offramp_test { owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token, receiver = @0xbed8 ) ] @@ -995,6 +1219,10 @@ module ccip_offramp::offramp_test { owner: &signer, burn_mint_token_pool: &signer, lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer, receiver: &signer ) { test_execute_single_report_with_token_transfer( @@ -1004,9 +1232,15 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, receiver, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ) } @@ -1018,6 +1252,10 @@ module ccip_offramp::offramp_test { owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token, receiver = @0xbed8 ) ] @@ -1028,6 +1266,10 @@ module ccip_offramp::offramp_test { owner: &signer, burn_mint_token_pool: &signer, lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer, receiver: &signer ) { test_execute_single_report_with_token_transfer( @@ -1037,9 +1279,15 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, receiver, LOCK_RELEASE_TOKEN_POOL, - LOCK_RELEASE_TOKEN_SEED + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ) } @@ -1052,9 +1300,15 @@ module ccip_offramp::offramp_test { owner: &signer, burn_mint_token_pool: &signer, lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer, receiver: &signer, pool_type: u8, - token_seed: vector + token_seed: vector, + is_dispatchable: bool, + use_v1_init: bool ) { let (_owner_addr, token_obj) = setup( @@ -1064,8 +1318,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, pool_type, - token_seed + token_seed, + is_dispatchable, + use_v1_init ); let token_addr = object::object_address(&token_obj); @@ -1167,7 +1427,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] fun test_transfer_ownership_flow( @@ -1176,7 +1440,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -1185,8 +1453,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); let new_owner = signer::address_of(aptos_framework); account::create_account_for_test(new_owner); @@ -1207,7 +1481,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] fun test_getters( @@ -1216,7 +1494,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -1225,8 +1507,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); let latest_price_sequence_number = offramp::get_latest_price_sequence_number(); @@ -1247,7 +1535,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] #[expected_failure(abort_code = 65540, location = ccip_offramp::offramp)] @@ -1257,7 +1549,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -1266,8 +1562,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); // E_UNKNOWN_SOURCE_CHAIN_SELECTOR @@ -1281,7 +1583,11 @@ module ccip_offramp::offramp_test { ccip_offramp = @ccip_offramp, owner = @0x100, burn_mint_token_pool = @burn_mint_token_pool, - lock_release_token_pool = @lock_release_token_pool + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token ) ] #[expected_failure(abort_code = 65550, location = ccip_offramp::offramp)] @@ -1291,7 +1597,11 @@ module ccip_offramp::offramp_test { ccip_offramp: &signer, owner: &signer, burn_mint_token_pool: &signer, - lock_release_token_pool: &signer + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer ) { setup( aptos_framework, @@ -1300,8 +1610,14 @@ module ccip_offramp::offramp_test { owner, burn_mint_token_pool, lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, BURN_MINT_TOKEN_POOL, - BURN_MINT_TOKEN_SEED + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init ); // E_INVALID_ROOT diff --git a/contracts/ccip/ccip_offramp/tests/offramp_v1_v2_compatibility_test.move b/contracts/ccip/ccip_offramp/tests/offramp_v1_v2_compatibility_test.move new file mode 100644 index 00000000..bb46ace5 --- /dev/null +++ b/contracts/ccip/ccip_offramp/tests/offramp_v1_v2_compatibility_test.move @@ -0,0 +1,688 @@ +#[test_only] +/// Verifies V1 → V2 migration path and that both can coexist +module ccip_offramp::offramp_v1_v2_compatibility_test { + use std::signer; + use std::object; + use std::primary_fungible_store; + use std::timestamp; + + use ccip_offramp::offramp; + use ccip_offramp::offramp_test; + use ccip_offramp::mock_ccip_receiver; + use ccip::receiver_registry; + use ccip::token_admin_registry; + use ccip::merkle_proof; + + use burn_mint_token_pool::upgrade_v2; + + const BURN_MINT_TOKEN_POOL: u8 = 0; + const LOCK_RELEASE_TOKEN_POOL: u8 = 1; + const BURN_MINT_TOKEN_SEED: vector = b"TestToken"; + const LOCK_RELEASE_TOKEN_SEED: vector = b"LockReleaseToken"; + const EVM_SOURCE_CHAIN_SELECTOR: u64 = 909606746561742123; + const DEST_CHAIN_SELECTOR: u64 = 743186221051783445; + const MOCK_EVM_ADDRESS_VECTOR: vector = x"4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97"; + const MOCK_EVM_ADDRESS_VECTOR_2: vector = x"1234567890abcdef1234567890abcdef12345678"; + const ONRAMP_ADDRESS: vector = x"47a1f0a819457f01153f35c6b6b0d42e2e16e91e"; + + // ============================================ + // Test Helper Functions + // ============================================ + + fun create_and_execute_message( + message_id: vector, + sequence_number: u64, + receiver: address, + data: vector, + token_transfers: vector, + owner: &signer + ) { + // Configure source chain if first message + if (sequence_number == 0) { + offramp::apply_source_chain_config_updates( + owner, + vector[EVM_SOURCE_CHAIN_SELECTOR], + vector[true], // is_enabled + vector[true], // is_rmn_verification_disabled + vector[ONRAMP_ADDRESS] + ); + }; + + let nonce: u64 = 0; + let sender = x"d87929a32cf0cbdc9e2d07ffc7c33344079de727"; + let gas_limit: u256 = 100000; + + let header = + offramp::test_create_ramp_message_header( + message_id, + EVM_SOURCE_CHAIN_SELECTOR, + DEST_CHAIN_SELECTOR, + sequence_number, + nonce + ); + + // Create offchain_token_data: one empty vector per token transfer + let num_tokens = token_transfers.length(); + let offchain_token_data: vector> = vector[]; + let i = 0; + while (i < num_tokens) { + offchain_token_data.push_back(vector[]); + i = i + 1; + }; + + let message = + offramp::test_create_any2aptos_ramp_message( + header, + sender, + data, + receiver, + gas_limit, + token_transfers + ); + + let metadata_hash = + offramp::test_calculate_metadata_hash( + EVM_SOURCE_CHAIN_SELECTOR, DEST_CHAIN_SELECTOR, ONRAMP_ADDRESS + ); + + let hashed_leaf = offramp::test_calculate_message_hash(&message, metadata_hash); + let proofs = vector[]; + let root = merkle_proof::merkle_root(hashed_leaf, proofs); + + // Commit root (with timestamp in the past to allow execution) + offramp::test_add_root(root, timestamp::now_seconds() - 3700); + + let execution_report = + offramp::test_create_execution_report( + EVM_SOURCE_CHAIN_SELECTOR, + message, + offchain_token_data, + vector[] + ); + + offramp::test_execute_single_report(execution_report); + + // Verify execution state is SUCCESS (2) + let execution_state = + offramp::get_execution_state(EVM_SOURCE_CHAIN_SELECTOR, sequence_number); + assert!(execution_state == 2); + } + + // ============================================ + // Test 1: V1 Receiver Works (Baseline) + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_v1_receiver_works( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + // Setup with V1 pool (use_v1_init = true) + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + // Setup receiver (need receiver_registry initialized) + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_state_only(ccip_offramp); + + // Register as V1 (dispatchable FA mode) + mock_ccip_receiver::register_as_v1(ccip_offramp); + + // Verify V1 receiver registered (not V2) + assert!( + !receiver_registry::is_registered_receiver_v2( + signer::address_of(ccip_offramp) + ) + ); + assert!( + receiver_registry::is_registered_receiver(signer::address_of(ccip_offramp)) + ); + + // Pool is V1-only (test_init_v1 was called in setup) + // No public function to verify V1 pool registration, but successful execution proves it works + assert!( + !token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + let token_addr = object::object_address(&token_obj); + let token_amount = 1000; + + // Create token transfer + let token_transfer = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, // source_pool_address + token_addr, // dest_token_address + 1000000, // dest_gas_amount + vector[], // extra_data + (token_amount as u256) // amount + ); + + // Execute message with tokens + create_and_execute_message( + x"0001", + 0, + signer::address_of(ccip_offramp), + vector[], // no data, just tokens + vector[token_transfer], + owner + ); + + // Verify tokens received by V1 receiver + let receiver_balance = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(receiver_balance == token_amount); + + // Verify V1 receiver callback was invoked via dispatchable FA + let events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(events.length() == 1); + } + + // ============================================ + // Test 2: V1 → V2 Migration Works + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_v1_to_v2_migration( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + // Setup with V1 pool (use_v1_init = true) + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + // Setup receiver (need receiver_registry initialized) + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_state_only(ccip_offramp); + + // STEP 1: Register as V1 + mock_ccip_receiver::register_as_v1(ccip_offramp); + assert!( + !receiver_registry::is_registered_receiver_v2( + signer::address_of(ccip_offramp) + ) + ); + assert!( + receiver_registry::is_registered_receiver(signer::address_of(ccip_offramp)) + ); + + let token_addr = object::object_address(&token_obj); + let token_amount = 1000; + + // Execute message with V1 registration + let token_transfer_v1 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + (token_amount as u256) + ); + + create_and_execute_message( + x"0002", + 0, + signer::address_of(ccip_offramp), + vector[], + vector[token_transfer_v1], + owner + ); + + // Verify V1 worked + let balance_after_v1 = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(balance_after_v1 == token_amount); + + // STEP 2: Upgrade pool to V2 (realistic upgrade pattern) + upgrade_v2::test_init_module(burn_mint_token_pool); + + // STEP 3: Migrate receiver to V2 + mock_ccip_receiver::migrate_to_v2(ccip_offramp); + + // Verify V2 is now active (V2 registration exists) + assert!( + receiver_registry::is_registered_receiver_v2(signer::address_of(ccip_offramp)) + ); + + // Execute message with V2 registration (dispatcher should prefer V2) + let token_transfer_v2 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + (token_amount as u256) + ); + + create_and_execute_message( + x"0003", + 1, + signer::address_of(ccip_offramp), + vector[], + vector[token_transfer_v2], + owner + ); + + // Verify V2 worked - should now have 2x token_amount + let balance_after_v2 = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(balance_after_v2 == token_amount * 2); + + // Verify both V1 and V2 callbacks were invoked + let events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(events.length() == 2); // 1 from V1 execution, 1 from V2 execution + } + + // ============================================ + // Test 3: Direct V2 Registration Works + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_v2_receiver_direct( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + // Setup with V1 pool initially (use_v1_init = true) + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + // Upgrade pool to V2 immediately (realistic new deployment with V2) + upgrade_v2::test_init_module(burn_mint_token_pool); + + // Setup V2 receiver directly (default behavior) + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + + // Verify V2 receiver registered + assert!( + receiver_registry::is_registered_receiver_v2(signer::address_of(ccip_offramp)) + ); + + // Verify V2 pool registered + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + let token_addr = object::object_address(&token_obj); + let token_amount = 1000; + + // Create token transfer + let token_transfer = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + (token_amount as u256) + ); + + // Execute message with tokens + create_and_execute_message( + x"0004", + 0, + signer::address_of(ccip_offramp), + vector[], + vector[token_transfer], + owner + ); + + // Verify tokens received by V2 receiver + let receiver_balance = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(receiver_balance == token_amount); + + // Verify V2 receiver callback was invoked + let events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(events.length() == 1); + } + + // ============================================ + // Test 4: Dispatcher Routes Correctly + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_dispatcher_routing( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + // Setup with V1 pool initially (use_v1_init = true) + let (_owner_addr, token_obj) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + // Upgrade pool to V2 before testing (mixed V1 receiver + V2 pool scenario) + upgrade_v2::test_init_module(burn_mint_token_pool); + + // Setup receiver (need receiver_registry initialized) + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_state_only(ccip_offramp); + + let token_addr = object::object_address(&token_obj); + let token_amount = 500; + + // Phase 1: Register V1, verify dispatcher uses V1 path + mock_ccip_receiver::register_as_v1(ccip_offramp); + assert!( + !receiver_registry::is_registered_receiver_v2( + signer::address_of(ccip_offramp) + ) + ); + + let token_transfer_1 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + (token_amount as u256) + ); + + create_and_execute_message( + x"0005", + 0, + signer::address_of(ccip_offramp), + vector[], + vector[token_transfer_1], + owner + ); + + let balance_1 = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(balance_1 == token_amount); + + // Phase 2: Add V2 registration, verify dispatcher now uses V2 path + mock_ccip_receiver::migrate_to_v2(ccip_offramp); + assert!( + receiver_registry::is_registered_receiver_v2(signer::address_of(ccip_offramp)) + ); + + let token_transfer_2 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, + token_addr, + 1000000, + vector[], + (token_amount as u256) + ); + + create_and_execute_message( + x"0006", + 1, + signer::address_of(ccip_offramp), + vector[], + vector[token_transfer_2], + owner + ); + + // Should have received both transfers + let balance_2 = + primary_fungible_store::balance(signer::address_of(ccip_offramp), token_obj); + assert!(balance_2 == token_amount * 2); + + // Both callbacks should have been invoked + let events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(events.length() == 2); + } + + // ============================================ + // Test 5: Multi-Transfer Message with V2 Receiver + // Tests V2 receiver handling multiple token transfers in a single message + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + ccip = @ccip, + ccip_offramp = @ccip_offramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + managed_token_pool = @managed_token_pool, + managed_token = @managed_token, + regulated_token_pool = @regulated_token_pool, + regulated_token = @regulated_token + ) + ] + fun test_multi_token_v2_receiver( + aptos_framework: &signer, + ccip: &signer, + ccip_offramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + managed_token_pool: &signer, + managed_token: &signer, + regulated_token_pool: &signer, + regulated_token: &signer + ) { + // Setup first pool: burn_mint with V2 + let (_owner_addr, token_obj_1) = + offramp_test::setup( + aptos_framework, + ccip, + ccip_offramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + managed_token_pool, + managed_token, + regulated_token_pool, + regulated_token, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v1_init = false (V2 from start) + ); + + let token_addr_1 = object::object_address(&token_obj_1); + + receiver_registry::init_module_for_testing(owner); + mock_ccip_receiver::test_init_module(ccip_offramp); + + assert!( + receiver_registry::is_registered_receiver_v2(signer::address_of(ccip_offramp)) + ); + + // Verify pool has V2 config + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + // Create 2 token transfers of the same token + // This tests that V2 receiver can handle multiple transfers in one message + // and that pool closures can be invoked multiple times sequentially + let token_amount_1 = 1000; + let token_amount_2 = 2000; + + let token_transfer_1 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, // source_pool_address + token_addr_1, // dest_token_address + 1000000, // dest_gas_amount + vector[], // extra_data + (token_amount_1 as u256) // amount + ); + + let token_transfer_2 = + offramp::test_create_any2aptos_token_transfer( + MOCK_EVM_ADDRESS_VECTOR, // same source_pool_address + token_addr_1, // dest_token_address (same token) + 1000000, // dest_gas_amount + vector[], // extra_data + (token_amount_2 as u256) // amount + ); + + // Execute message with 2 transfers + create_and_execute_message( + x"0007", // unique message_id + 0, // sequence_number + signer::address_of(ccip_offramp), // receiver + vector[], // no data, just tokens + vector[token_transfer_1, token_transfer_2], // 2 token transfers + owner + ); + + // Verify BOTH 2 transfers were received (total amount = amount_1 + amount_2) + let total_balance = + primary_fungible_store::balance( + signer::address_of(ccip_offramp), token_obj_1 + ); + assert!( + total_balance == token_amount_1 + token_amount_2 + ); + + // Verify V2 receiver callback was invoked once with multiple tokens + // The mock receiver's ccip_receive_v2 handles multiple tokens in a loop + // and emits a single ReceivedTokensOnly event + let events = mock_ccip_receiver::get_received_tokens_only_events(); + assert!(events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_onramp/tests/mock_token.move b/contracts/ccip/ccip_onramp/tests/mock/mock_token.move similarity index 77% rename from contracts/ccip/ccip_onramp/tests/mock_token.move rename to contracts/ccip/ccip_onramp/tests/mock/mock_token.move index eded7cfc..d22f1566 100644 --- a/contracts/ccip/ccip_onramp/tests/mock_token.move +++ b/contracts/ccip/ccip_onramp/tests/mock/mock_token.move @@ -1,10 +1,9 @@ #[test_only] module ccip_onramp::mock_token { use std::fungible_asset::{Self, FungibleAsset, TransferRef}; - use std::object::{Self, Object, ConstructorRef}; + use std::object::{Object, ConstructorRef}; use std::string::{Self}; use std::option::{Self}; - use std::primary_fungible_store; public fun add_dynamic_dispatch_function( ccip_onramp_signer: &signer, constructor_ref: &ConstructorRef @@ -30,16 +29,14 @@ module ccip_onramp::mock_token { } public fun lock_or_burn( - store: Object, fa: FungibleAsset, _transfer_ref: &TransferRef + store: Object, fa: FungibleAsset, transfer_ref: &TransferRef ) { - fungible_asset::deposit(store, fa); + fungible_asset::deposit_with_ref(transfer_ref, store, fa); } public fun release_or_mint( store: Object, amount: u64, transfer_ref: &TransferRef ): FungibleAsset { - primary_fungible_store::withdraw_with_ref( - transfer_ref, object::owner(store), amount - ) + fungible_asset::withdraw_with_ref(transfer_ref, store, amount) } } diff --git a/contracts/ccip/ccip_onramp/tests/onramp_migration_test.move b/contracts/ccip/ccip_onramp/tests/onramp_migration_test.move index f5a63fd3..cad9841b 100644 --- a/contracts/ccip/ccip_onramp/tests/onramp_migration_test.move +++ b/contracts/ccip/ccip_onramp/tests/onramp_migration_test.move @@ -844,7 +844,8 @@ module ccip_onramp::onramp_migration_test { lock_release_token_pool, pool_type, seed, - is_dispatchable + is_dispatchable, + false // use_v1_init ); let one_e_18 = 1_000_000_000_000_000_000; diff --git a/contracts/ccip/ccip_onramp/tests/onramp_test.move b/contracts/ccip/ccip_onramp/tests/onramp_test.move index c11abb70..d4fdb33b 100644 --- a/contracts/ccip/ccip_onramp/tests/onramp_test.move +++ b/contracts/ccip/ccip_onramp/tests/onramp_test.move @@ -66,6 +66,17 @@ module ccip_onramp::onramp_test { transfer_ref: TransferRef } + public fun mint_test_tokens( + token_addr: address, recipient: address, amount: u64 + ) acquires TestToken { + let token = borrow_global(token_addr); + let recipient_store = + primary_fungible_store::ensure_primary_store_exists( + recipient, token.metadata + ); + fungible_asset::mint_to(&token.mint_ref, recipient_store, amount); + } + fun init_timestamp(aptos_framework: &signer, timestamp_seconds: u64) { timestamp::set_time_has_started_for_testing(aptos_framework); timestamp::update_global_time_for_test_secs(timestamp_seconds); @@ -81,7 +92,8 @@ module ccip_onramp::onramp_test { lock_release_token_pool: &signer, pool_type: u8, // 0 for burn_mint, 1 for lock_release seed: vector, - is_dispatchable: bool + is_dispatchable: bool, + use_v1_init: bool ): (address, Object) { let owner_addr = signer::address_of(owner); account::create_account_for_test(signer::address_of(burn_mint_token_pool)); @@ -138,7 +150,8 @@ module ccip_onramp::onramp_test { lock_release_token_pool, pool_type, seed, - is_dispatchable + is_dispatchable, + use_v1_init ); let one_e_18 = 1_000_000_000_000_000_000; @@ -185,12 +198,26 @@ module ccip_onramp::onramp_test { vector[900_000_000_000_000_000] // premium_multiplier_wei_per_eth ); + // Configure token transfer fees (needed for token transfers) + fee_quoter::apply_token_transfer_fee_config_updates( + owner, + DEST_CHAIN_SELECTOR, + vector[token_addr], + vector[50], // min_fee_usd_cents + vector[500], // max_fee_usd_cents + vector[10], // deci_bps + vector[5000], // dest_gas_overhead + vector[64], // dest_bytes_overhead + vector[true], // is_enabled + vector[] + ); + // To be able to call token_admin_dispatcher::dispatch_lock_or_burn - // Need to register onramp signer as an allowed onramp + // Need to register onramp state address as an allowed onramp auth::apply_allowed_onramp_updates( owner, vector[], // onramps_to_remove - vector[signer::address_of(ccip_onramp)] // onramps_to_add + vector[onramp::get_state_address()] // onramps_to_add ); // To be able to call fee_quoter::update_prices, need to register as an allowed offramp @@ -218,7 +245,8 @@ module ccip_onramp::onramp_test { lock_release_token_pool: &signer, pool_type: u8, // 0 for burn_mint, 1 for lock_release seed: vector, - is_dispatchable: bool + is_dispatchable: bool, + use_v1_init: bool ): (Object, address) { let constructor_ref = object::create_named_object(owner, seed); @@ -252,7 +280,11 @@ module ccip_onramp::onramp_test { eth_abi::encode_address(&mut remote_token_address, MOCK_EVM_ADDRESS); if (pool_type == BURN_MINT_TOKEN_POOL) { - burn_mint_token_pool::test_init_module(burn_mint_token_pool); + if (use_v1_init) { + burn_mint_token_pool::test_init_v1(burn_mint_token_pool); + } else { + burn_mint_token_pool::test_init_module(burn_mint_token_pool); + }; burn_mint_token_pool::initialize(owner, burn_ref, mint_ref); burn_mint_token_pool::apply_chain_updates( owner, @@ -282,7 +314,11 @@ module ccip_onramp::onramp_test { signer::address_of(burn_mint_token_pool) ); } else { - lock_release_token_pool::test_init_module(lock_release_token_pool); + if (use_v1_init) { + lock_release_token_pool::test_init_v1(lock_release_token_pool); + } else { + lock_release_token_pool::test_init_module(lock_release_token_pool); + }; lock_release_token_pool::initialize( owner, transfer_ref, @@ -333,7 +369,7 @@ module ccip_onramp::onramp_test { (metadata, token_addr) } - fun initialize_onramp(owner: &signer, router: &signer): address { + public fun initialize_onramp(owner: &signer, router: &signer): address { onramp::initialize( owner, SOURCE_CHAIN_SELECTOR, @@ -387,6 +423,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -443,6 +480,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -498,6 +536,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -535,6 +574,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -615,6 +655,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -703,6 +744,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -823,6 +865,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, LOCK_RELEASE_TOKEN_POOL, b"LockReleaseToken", + false, false ); @@ -947,6 +990,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); initialize_onramp(owner, router); @@ -987,6 +1031,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1024,6 +1069,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1063,6 +1109,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1123,6 +1170,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1172,6 +1220,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1211,6 +1260,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1277,6 +1327,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1334,6 +1385,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1385,6 +1437,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); @@ -1464,6 +1517,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); setup_mcms(mcms); @@ -1531,6 +1585,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); setup_mcms(mcms); @@ -1606,6 +1661,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); setup_mcms(mcms); @@ -1689,6 +1745,7 @@ module ccip_onramp::onramp_test { lock_release_token_pool, BURN_MINT_TOKEN_POOL, b"TestToken", + false, false ); setup_mcms(mcms); diff --git a/contracts/ccip/ccip_onramp/tests/onramp_v1_v2_pool_compatibility_test.move b/contracts/ccip/ccip_onramp/tests/onramp_v1_v2_pool_compatibility_test.move new file mode 100644 index 00000000..3c3bae6c --- /dev/null +++ b/contracts/ccip/ccip_onramp/tests/onramp_v1_v2_pool_compatibility_test.move @@ -0,0 +1,594 @@ +#[test_only] +/// Verifies V1 → V2 migration path and that both can coexist +module ccip_onramp::onramp_v1_v2_pool_compatibility_test { + use std::signer; + use std::object; + use std::primary_fungible_store; + + use ccip::client; + use ccip::token_admin_registry; + use ccip::eth_abi; + use ccip_onramp::onramp; + + use burn_mint_token_pool::burn_mint_token_pool; + use burn_mint_token_pool::upgrade_v2 as burn_mint_upgrade_v2; + use lock_release_token_pool::lock_release_token_pool; + use lock_release_token_pool::upgrade_v2 as lock_release_upgrade_v2; + + use ccip_onramp::onramp_test; + + const DEST_CHAIN_SELECTOR: u64 = 5678; + const TOKEN_AMOUNT: u64 = 5000; + + const SENDER: address = @0x500; + + const BURN_MINT_TOKEN_POOL: u8 = 0; + const LOCK_RELEASE_TOKEN_POOL: u8 = 1; + + const BURN_MINT_TOKEN_SEED: vector = b"TestToken"; + const LOCK_RELEASE_TOKEN_SEED: vector = b"LockReleaseToken"; + + const MOCK_EVM_ADDRESS: address = @0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97; + + const GAS_LIMIT: u64 = 5000000; + const ALLOW_OUT_OF_ORDER_EXECUTION: bool = true; + + fun create_extra_args_v2(): vector { + client::encode_generic_extra_args_v2( + GAS_LIMIT as u256, ALLOW_OUT_OF_ORDER_EXECUTION + ) + } + + fun encode_receiver(): vector { + let receiver = vector[]; + eth_abi::encode_address(&mut receiver, MOCK_EVM_ADDRESS); + receiver + } + + /// Helper to calculate fee and mint enough tokens for sender + fun mint_tokens_for_transfer( + token_addr: address, num_transfers: u64 + ) { + let receiver = encode_receiver(); + let extra_args = create_extra_args_v2(); + + // Calculate fee for one transfer + let fee_amount = + onramp::get_fee( + DEST_CHAIN_SELECTOR, + receiver, + vector[], // data + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + extra_args + ); + + // Mint enough for transfers + fees + let total_needed = (TOKEN_AMOUNT + fee_amount) * num_transfers; + onramp_test::mint_test_tokens(token_addr, SENDER, total_needed); + } + + // ============================================ + // Test 1: V1 Burn/Mint Pool Baseline + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v1_burn_mint_pool_baseline( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for transfer + fees + mint_tokens_for_transfer(token_addr, 1); + + // Verify V1 pool registered (not V2) + assert!( + !token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + // Send tokens via onramp + let sender_balance_before = primary_fungible_store::balance(SENDER, token_obj); + + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], // data + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], // token_store_addresses - use primary store + token_addr, // fee_token + @0x0, // fee_token_store - use primary store + create_extra_args_v2() + ); + + // Verify tokens were burned from sender + let sender_balance_after = primary_fungible_store::balance(SENDER, token_obj); + assert!( + sender_balance_before - sender_balance_after >= TOKEN_AMOUNT + ); + + // Verify V1 callback worked - check events + let events = + burn_mint_token_pool::get_locked_or_burned_events( + burn_mint_token_pool::get_store_address() + ); + assert!(events.length() >= 1); + } + + // ============================================ + // Test 2: V1 → V2 Burn/Mint Migration + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v1_to_v2_burn_mint_migration( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for 2 transfers + fees + mint_tokens_for_transfer(token_addr, 2); + + // STEP 1: Send with V1 pool + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V1 callback worked + let events_before = + burn_mint_token_pool::get_locked_or_burned_events( + burn_mint_token_pool::get_store_address() + ); + assert!(events_before.length() == 1); + + // STEP 2: Upgrade to V2 + burn_mint_upgrade_v2::test_init_module(burn_mint_token_pool); + + // Verify V2 config now exists + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + // STEP 3: Send with V2 pool + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V2 callback worked + let events_after = + burn_mint_token_pool::get_locked_or_burned_events( + burn_mint_token_pool::get_store_address() + ); + assert!(events_after.length() == 2); + } + + // ============================================ + // Test 3: V2 Burn/Mint Direct (no migration) + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v2_burn_mint_direct( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + BURN_MINT_TOKEN_POOL, + BURN_MINT_TOKEN_SEED, + false, // is_dispatchable + false // use_v2_init (V2) + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for transfer + fees + mint_tokens_for_transfer(token_addr, 1); + + // Verify V2 config registered (already done by setup) + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(burn_mint_token_pool) + ) + ); + + // Send message using V2 pool + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V2 callback worked + let events = + burn_mint_token_pool::get_locked_or_burned_events( + burn_mint_token_pool::get_store_address() + ); + assert!(events.length() == 1); + } + + // ============================================ + // Test 4: V1 Lock/Release Pool Baseline + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v1_lock_release_pool_baseline( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for transfer + fees + mint_tokens_for_transfer(token_addr, 1); + + // Verify V1 pool registered (not V2) + assert!( + !token_admin_registry::has_token_pool_config( + signer::address_of(lock_release_token_pool) + ) + ); + + // Send tokens via onramp + let sender_balance_before = primary_fungible_store::balance(SENDER, token_obj); + + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify tokens were locked from sender + let sender_balance_after = primary_fungible_store::balance(SENDER, token_obj); + assert!( + sender_balance_before - sender_balance_after >= TOKEN_AMOUNT + ); + + // Verify V1 callback worked - check events + let events = + lock_release_token_pool::get_locked_or_burned_events( + lock_release_token_pool::get_store_address() + ); + assert!(events.length() >= 1); + } + + // ============================================ + // Test 5: V1 → V2 Lock/Release Migration + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v1_to_v2_lock_release_migration( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + true // use_v1_init + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for 2 transfers + fees + mint_tokens_for_transfer(token_addr, 2); + + // STEP 1: Send with V1 + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V1 callback worked + let events_before = + lock_release_token_pool::get_locked_or_burned_events( + lock_release_token_pool::get_store_address() + ); + assert!(events_before.length() == 1); + + // STEP 2: Upgrade to V2 + lock_release_upgrade_v2::test_init_module(lock_release_token_pool); + + // Verify V2 config now exists + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(lock_release_token_pool) + ) + ); + + // STEP 3: Send with V2 pool + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V2 callback worked + let events_after = + lock_release_token_pool::get_locked_or_burned_events( + lock_release_token_pool::get_store_address() + ); + assert!(events_after.length() == 2); + } + + // ============================================ + // Test 6: V2 Lock/Release Direct (no migration) + // ============================================ + + #[ + test( + aptos_framework = @aptos_framework, + router = @0x200, + ccip = @ccip, + ccip_onramp = @ccip_onramp, + owner = @0x100, + burn_mint_token_pool = @burn_mint_token_pool, + lock_release_token_pool = @lock_release_token_pool, + sender = @0x500 + ) + ] + fun test_v2_lock_release_direct( + aptos_framework: &signer, + router: &signer, + ccip: &signer, + ccip_onramp: &signer, + owner: &signer, + burn_mint_token_pool: &signer, + lock_release_token_pool: &signer, + sender: &signer + ) { + let (_owner_addr, token_obj) = + onramp_test::setup( + aptos_framework, + router, + ccip, + ccip_onramp, + owner, + burn_mint_token_pool, + lock_release_token_pool, + LOCK_RELEASE_TOKEN_POOL, + LOCK_RELEASE_TOKEN_SEED, + false, // is_dispatchable + false // use_v2_init (V2) + ); + + let token_addr = object::object_address(&token_obj); + + // Fund sender with enough tokens for transfer + fees + mint_tokens_for_transfer(token_addr, 1); + + assert!( + token_admin_registry::has_token_pool_config( + signer::address_of(lock_release_token_pool) + ) + ); + + // Send message using V2 pool + onramp::ccip_send( + router, + sender, + DEST_CHAIN_SELECTOR, + encode_receiver(), + vector[], + vector[token_addr], + vector[TOKEN_AMOUNT], + vector[@0x0], + token_addr, + @0x0, + create_extra_args_v2() + ); + + // Verify V2 callback worked + let events = + lock_release_token_pool::get_locked_or_burned_events( + lock_release_token_pool::get_store_address() + ); + assert!(events.length() == 1); + } +} diff --git a/contracts/ccip/ccip_token_pools/burn_mint_token_pool/sources/burn_mint_token_pool.move b/contracts/ccip/ccip_token_pools/burn_mint_token_pool/sources/burn_mint_token_pool.move index ba4778d6..702956bc 100644 --- a/contracts/ccip/ccip_token_pools/burn_mint_token_pool/sources/burn_mint_token_pool.move +++ b/contracts/ccip/ccip_token_pools/burn_mint_token_pool/sources/burn_mint_token_pool.move @@ -7,9 +7,9 @@ module burn_mint_token_pool::burn_mint_token_pool { use std::option::{Self, Option}; use std::signer; use std::string::{Self, String}; - use aptos_framework::fungible_asset::{BurnRef, MintRef}; + use std::fungible_asset::{BurnRef, MintRef}; - use ccip::token_admin_registry; + use ccip::token_admin_registry::{Self, ReleaseOrMintInputV1, LockOrBurnInputV1}; use ccip_token_pool::ownable; use ccip_token_pool::rate_limiter; use ccip_token_pool::token_pool; @@ -73,6 +73,7 @@ module burn_mint_token_pool::burn_mint_token_pool { register_mcms_entrypoint(publisher, token_pool_module_name); }; + // Register V1 pool (for backward compatibility) token_admin_registry::register_pool( publisher, token_pool_module_name, @@ -80,6 +81,20 @@ module burn_mint_token_pool::burn_mint_token_pool { CallbackProof {} ); + let lock_or_burn_closure = |fa, input| lock_or_burn_v2(fa, input); + let release_or_mint_closure = |input| release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @burn_mint_local_token, + lock_or_burn_closure, + release_or_mint_closure, + CallbackProof {} + ); + // create a resource account to be the owner of the primary FungibleStore we will use. let (store_signer, store_signer_cap) = account::create_resource_account(publisher, STORE_OBJECT_SEED); @@ -363,6 +378,64 @@ module burn_mint_token_pool::burn_mint_token_pool { fa } + public fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Burn the token + assert!(pool.burn_ref.is_some(), E_BURN_REF_NOT_SET); + fungible_asset::burn(pool.burn_ref.borrow(), fa); + + // Emit event + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + public fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires BurnMintTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release + assert!(pool.mint_ref.is_some(), E_MINT_REF_NOT_SET); + let fa = fungible_asset::mint(pool.mint_ref.borrow(), local_amount); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + // ================================================================ // | Rate limit config | // ================================================================ @@ -731,4 +804,61 @@ module burn_mint_token_pool::burn_mint_token_pool { &borrow_global(state).token_pool_state ) } + + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } + + #[test_only] + public fun test_init_v1(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @ccip_burn_mint_pool. + assert!( + object::object_exists(@burn_mint_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + let metadata = object::address_to_object(@burn_mint_local_token); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@burn_mint_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"burn_mint_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + token_admin_registry::register_pool( + publisher, + token_pool_module_name, + @burn_mint_local_token, + CallbackProof {} + ); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + move_to( + publisher, + BurnMintTokenPoolDeployment { + store_signer_cap, + ownable_state: ownable::new(&store_signer, @burn_mint_token_pool), + token_pool_state: token_pool::initialize( + &store_signer, @burn_mint_local_token, vector[] + ) + } + ); + } } diff --git a/contracts/ccip/ccip_token_pools/burn_mint_token_pool/tests/upgrade_v2.move b/contracts/ccip/ccip_token_pools/burn_mint_token_pool/tests/upgrade_v2.move new file mode 100644 index 00000000..6d838345 --- /dev/null +++ b/contracts/ccip/ccip_token_pools/burn_mint_token_pool/tests/upgrade_v2.move @@ -0,0 +1,50 @@ +#[test_only] +module burn_mint_token_pool::upgrade_v2 { + use std::account::{Self}; + use std::error; + use std::fungible_asset::{Metadata}; + use std::object::{Self}; + + use burn_mint_token_pool::burn_mint_token_pool; + + use ccip::token_admin_registry::{Self}; + + const E_INVALID_FUNGIBLE_ASSET: u64 = 1; + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @burn_mint_token_pool. + assert!( + object::object_exists(@burn_mint_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@burn_mint_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"burn_mint_token_pool"; + + let lock_or_burn_closure = + |fa, input| burn_mint_token_pool::lock_or_burn_v2(fa, input); + let release_or_mint_closure = + |input| burn_mint_token_pool::release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @burn_mint_local_token, + lock_or_burn_closure, + release_or_mint_closure, + burn_mint_token_pool::create_callback_proof() + ); + } + + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +} diff --git a/contracts/ccip/ccip_token_pools/lock_release_token_pool/sources/lock_release_token_pool.move b/contracts/ccip/ccip_token_pools/lock_release_token_pool/sources/lock_release_token_pool.move index 835a6d24..c8be7f04 100644 --- a/contracts/ccip/ccip_token_pools/lock_release_token_pool/sources/lock_release_token_pool.move +++ b/contracts/ccip/ccip_token_pools/lock_release_token_pool/sources/lock_release_token_pool.move @@ -75,6 +75,7 @@ module lock_release_token_pool::lock_release_token_pool { register_mcms_entrypoint(publisher, token_pool_module_name); }; + // Register V1 pool (for backward compatibility) token_admin_registry::register_pool( publisher, token_pool_module_name, @@ -82,6 +83,18 @@ module lock_release_token_pool::lock_release_token_pool { CallbackProof {} ); + let lock_or_burn_closure = |fa, input| lock_or_burn_v2(fa, input); + let release_or_mint_closure = |input| release_or_mint_v2(input); + + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @lock_release_local_token, + lock_or_burn_closure, + release_or_mint_closure, + CallbackProof {} + ); + // create a resource account to be the owner of the primary FungibleStore we will use. let (store_signer, store_signer_cap) = account::create_resource_account(publisher, STORE_OBJECT_SEED); @@ -431,6 +444,64 @@ module lock_release_token_pool::lock_release_token_pool { fa } + public fun lock_or_burn_v2( + fa: FungibleAsset, input: token_admin_registry::LockOrBurnInputV1 + ): (vector, vector) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Lock the funds in the pool + primary_fungible_store::deposit(pool.store_signer_address, fa); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + public fun release_or_mint_v2( + input: token_admin_registry::ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires LockReleaseTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + let metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + + // Withdraw the amount from the store for release + let fa = primary_fungible_store::withdraw(&store_signer, metadata, local_amount); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + // ================================================================ // | Rate limit config | // ================================================================ @@ -893,6 +964,63 @@ module lock_release_token_pool::lock_release_token_pool { init_module(publisher); } + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } + + #[test_only] + public fun test_init_v1(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @ccip_lock_release_pool. + assert!( + object::object_exists(@lock_release_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + let metadata = object::address_to_object(@lock_release_local_token); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@lock_release_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"lock_release_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + token_admin_registry::register_pool( + publisher, + token_pool_module_name, + @lock_release_local_token, + CallbackProof {} + ); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + move_to( + publisher, + LockReleaseTokenPoolDeployment { + store_signer_cap, + ownable_state: ownable::new(&store_signer, @lock_release_token_pool), + token_pool_state: token_pool::initialize( + &store_signer, @lock_release_local_token, vector[] + ) + } + ); + } + #[test_only] public fun get_locked_or_burned_events( state: address diff --git a/contracts/ccip/ccip_token_pools/lock_release_token_pool/tests/upgrade_v2.move b/contracts/ccip/ccip_token_pools/lock_release_token_pool/tests/upgrade_v2.move new file mode 100644 index 00000000..7d42c801 --- /dev/null +++ b/contracts/ccip/ccip_token_pools/lock_release_token_pool/tests/upgrade_v2.move @@ -0,0 +1,50 @@ +#[test_only] +module lock_release_token_pool::upgrade_v2 { + use std::account::{Self}; + use std::error; + use std::fungible_asset::{Metadata}; + use std::object::{Self}; + + use lock_release_token_pool::lock_release_token_pool; + + use ccip::token_admin_registry::{Self}; + + const E_INVALID_FUNGIBLE_ASSET: u64 = 1; + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @lock_release_token_pool. + assert!( + object::object_exists(@lock_release_local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@lock_release_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"lock_release_token_pool"; + + let lock_or_burn_closure = + |fa, input| lock_release_token_pool::lock_or_burn_v2(fa, input); + let release_or_mint_closure = + |input| lock_release_token_pool::release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @lock_release_local_token, + lock_or_burn_closure, + release_or_mint_closure, + lock_release_token_pool::create_callback_proof() + ); + } + + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +} diff --git a/contracts/ccip/ccip_token_pools/managed_token_pool/sources/managed_token_pool.move b/contracts/ccip/ccip_token_pools/managed_token_pool/sources/managed_token_pool.move index 59d515aa..f41c0e93 100644 --- a/contracts/ccip/ccip_token_pools/managed_token_pool/sources/managed_token_pool.move +++ b/contracts/ccip/ccip_token_pools/managed_token_pool/sources/managed_token_pool.move @@ -10,7 +10,7 @@ module managed_token_pool::managed_token_pool { use managed_token::managed_token; - use ccip::token_admin_registry; + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; use ccip_token_pool::ownable; use ccip_token_pool::rate_limiter; use ccip_token_pool::token_pool; @@ -60,6 +60,8 @@ module managed_token_pool::managed_token_pool { }; let managed_token_address = managed_token::token_metadata(); + + // Register V1 pool (for backward compatibility) token_admin_registry::register_pool( publisher, token_pool_module_name, @@ -67,6 +69,18 @@ module managed_token_pool::managed_token_pool { CallbackProof {} ); + let lock_or_burn_closure = |fa, input| lock_or_burn_v2(fa, input); + let release_or_mint_closure = |input| release_or_mint_v2(input); + + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + managed_token_address, + lock_or_burn_closure, + release_or_mint_closure, + CallbackProof {} + ); + // create a resource account to be the owner of the primary FungibleStore we will use. let (store_signer, store_signer_cap) = account::create_resource_account(publisher, STORE_OBJECT_SEED); @@ -327,6 +341,76 @@ module managed_token_pool::managed_token_pool { fa } + public fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + // This method validates various aspects of the lock or burn operation. If any of the + // validations fail, the transaction will abort. + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + // Burn the funds + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, fungible_asset::asset_metadata(&fa) + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + fungible_asset::deposit(store, fa); + managed_token::burn(signer, pool.store_signer_address, fa_amount); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + public fun release_or_mint_v2(input: ReleaseOrMintInputV1): (FungibleAsset, u64) { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release. + let local_token = token_admin_registry::get_release_or_mint_local_token(&input); + let metadata = object::address_to_object(local_token); + let store = + primary_fungible_store::ensure_primary_store_exists( + pool.store_signer_address, metadata + ); + let signer = &account::create_signer_with_capability(&pool.store_signer_cap); + managed_token::mint(signer, pool.store_signer_address, local_amount); + + // Calling into `fungible_asset::withdraw` works as managed token is not dispatchable + let fa = fungible_asset::withdraw(signer, store, local_amount); + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + // ================================================================ // | Rate limit config | // ================================================================ @@ -642,4 +726,10 @@ module managed_token_pool::managed_token_pool { public entry fun test_init_module(owner: &signer) { init_module(owner); } + + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } } diff --git a/contracts/ccip/ccip_token_pools/managed_token_pool/tests/upgrade_v2.move b/contracts/ccip/ccip_token_pools/managed_token_pool/tests/upgrade_v2.move new file mode 100644 index 00000000..e2be9476 --- /dev/null +++ b/contracts/ccip/ccip_token_pools/managed_token_pool/tests/upgrade_v2.move @@ -0,0 +1,44 @@ +#[test_only] +module managed_token_pool::upgrade_v2 { + use std::account::{Self}; + + use managed_token::managed_token; + use managed_token_pool::managed_token_pool; + + use ccip::token_admin_registry::{Self}; + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @managed_token_pool. + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@managed_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"managed_token_pool"; + + let managed_token_address = managed_token::token_metadata(); + + let lock_or_burn_closure = + |fa, input| managed_token_pool::lock_or_burn_v2(fa, input); + let release_or_mint_closure = + |input| managed_token_pool::release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + managed_token_address, + lock_or_burn_closure, + release_or_mint_closure, + managed_token_pool::create_callback_proof() + ); + } + + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +} diff --git a/contracts/ccip/ccip_token_pools/regulated_token_pool/sources/regulated_token_pool.move b/contracts/ccip/ccip_token_pools/regulated_token_pool/sources/regulated_token_pool.move index 60daaab2..ebda6763 100644 --- a/contracts/ccip/ccip_token_pools/regulated_token_pool/sources/regulated_token_pool.move +++ b/contracts/ccip/ccip_token_pools/regulated_token_pool/sources/regulated_token_pool.move @@ -10,7 +10,7 @@ module regulated_token_pool::regulated_token_pool { use regulated_token::regulated_token::{Self}; - use ccip::token_admin_registry; + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; use ccip_token_pool::ownable; use ccip_token_pool::rate_limiter; use ccip_token_pool::token_pool; @@ -33,6 +33,7 @@ module regulated_token_pool::regulated_token_pool { const E_LOCAL_TOKEN_MISMATCH: u64 = 4; const E_INVALID_ARGUMENTS: u64 = 5; const E_UNKNOWN_FUNCTION: u64 = 6; + const E_NOT_REGISTERED_RECEIVER: u64 = 7; // ================================================================ // | Init | @@ -60,6 +61,8 @@ module regulated_token_pool::regulated_token_pool { }; let regulated_token_address = regulated_token::token_address(); + + // Register V1 pool (for backward compatibility) token_admin_registry::register_pool( publisher, token_pool_module_name, @@ -67,6 +70,18 @@ module regulated_token_pool::regulated_token_pool { CallbackProof {} ); + let lock_or_burn_closure = |fa, input| lock_or_burn_v2(fa, input); + let release_or_mint_closure = |input| release_or_mint_v2(input); + + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + regulated_token_address, + lock_or_burn_closure, + release_or_mint_closure, + CallbackProof {} + ); + // create a resource account to be the owner of the primary FungibleStore we will use. let (store_signer, store_signer_cap) = account::create_resource_account(publisher, STORE_OBJECT_SEED); @@ -316,6 +331,63 @@ module regulated_token_pool::regulated_token_pool { fa } + public fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let sender = token_admin_registry::get_lock_or_burn_sender(&input); + regulated_token::bridge_burn(pool_signer, sender, fa); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, token_pool::encode_local_decimals(&pool.token_pool_state)) + } + + public fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires RegulatedTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_pool::calculate_release_or_mint_amount(&pool.token_pool_state, &input); + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + // Mint the amount for release using regulated token's bridge mint function + let pool_signer = &account::create_signer_with_capability(&pool.store_signer_cap); + let receiver = token_admin_registry::get_release_or_mint_receiver(&input); + let fa = regulated_token::bridge_mint(pool_signer, receiver, local_amount); + + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + receiver, + local_amount, + remote_chain_selector + ); + + (fa, local_amount) + } + // ================================================================ // | Rate limit config | // ================================================================ @@ -631,4 +703,59 @@ module regulated_token_pool::regulated_token_pool { public entry fun test_init_module(owner: &signer) { init_module(owner); } + + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } + + #[test_only] + public fun test_init_v1(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @regulated_token_pool. + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@regulated_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"regulated_token_pool"; + + // Register the entrypoint with mcms + if (@mcms_register_entrypoints == @0x1) { + register_mcms_entrypoint(publisher, token_pool_module_name); + }; + + let regulated_token_address = regulated_token::token_address(); + token_admin_registry::register_pool( + publisher, + token_pool_module_name, + regulated_token_address, + CallbackProof {} + ); + + // create a resource account to be the owner of the primary FungibleStore we will use. + let (store_signer, store_signer_cap) = + account::create_resource_account(publisher, STORE_OBJECT_SEED); + + let metadata = object::address_to_object(regulated_token_address); + + // make sure this is a valid fungible asset that is primary fungible store enabled, + // ie. created with primary_fungible_store::create_primary_store_enabled_fungible_asset + primary_fungible_store::ensure_primary_store_exists( + signer::address_of(&store_signer), metadata + ); + + let pool = RegulatedTokenPoolState { + ownable_state: ownable::new(&store_signer, @regulated_token_pool), + store_signer_address: signer::address_of(&store_signer), + store_signer_cap, + token_pool_state: token_pool::initialize( + &store_signer, regulated_token_address, vector[] + ) + }; + + move_to(&store_signer, pool); + } } diff --git a/contracts/ccip/ccip_token_pools/regulated_token_pool/tests/upgrade_v2.move b/contracts/ccip/ccip_token_pools/regulated_token_pool/tests/upgrade_v2.move new file mode 100644 index 00000000..9cd54956 --- /dev/null +++ b/contracts/ccip/ccip_token_pools/regulated_token_pool/tests/upgrade_v2.move @@ -0,0 +1,44 @@ +#[test_only] +module regulated_token_pool::upgrade_v2 { + use std::account::{Self}; + + use regulated_token::regulated_token::{Self}; + use regulated_token_pool::regulated_token_pool; + + use ccip::token_admin_registry::{Self}; + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @regulated_token_pool. + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@regulated_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"regulated_token_pool"; + + let regulated_token_address = regulated_token::token_address(); + + let lock_or_burn_closure = + |fa, input| regulated_token_pool::lock_or_burn_v2(fa, input); + let release_or_mint_closure = + |input| regulated_token_pool::release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + regulated_token_address, + lock_or_burn_closure, + release_or_mint_closure, + regulated_token_pool::create_callback_proof() + ); + } + + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +} diff --git a/contracts/ccip/ccip_token_pools/usdc_token_pool/sources/usdc_token_pool.move b/contracts/ccip/ccip_token_pools/usdc_token_pool/sources/usdc_token_pool.move index de0c8996..5c47a4a2 100644 --- a/contracts/ccip/ccip_token_pools/usdc_token_pool/sources/usdc_token_pool.move +++ b/contracts/ccip/ccip_token_pools/usdc_token_pool/sources/usdc_token_pool.move @@ -13,7 +13,7 @@ module usdc_token_pool::usdc_token_pool { use ccip::address; use ccip::eth_abi; - use ccip::token_admin_registry; + use ccip::token_admin_registry::{Self, LockOrBurnInputV1, ReleaseOrMintInputV1}; use ccip_token_pool::ownable; use ccip_token_pool::rate_limiter; use ccip_token_pool::token_pool; @@ -120,6 +120,18 @@ module usdc_token_pool::usdc_token_pool { CallbackProof {} ); + let lock_or_burn_closure = |fa, input| lock_or_burn_v2(fa, input); + let release_or_mint_closure = |input| release_or_mint_v2(input); + + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @local_token, + lock_or_burn_closure, + release_or_mint_closure, + CallbackProof {} + ); + // create a resource account to be the owner of the primary FungibleStore we will use. let (store_signer, store_signer_cap) = account::create_resource_account(publisher, STORE_OBJECT_SEED); @@ -497,6 +509,110 @@ module usdc_token_pool::usdc_token_pool { ); } + public fun lock_or_burn_v2( + fa: FungibleAsset, input: LockOrBurnInputV1 + ): (vector, vector) acquires USDCTokenPoolState { + let pool = borrow_pool_mut(); + let fa_amount = fungible_asset::amount(&fa); + + let dest_token_address = + token_pool::validate_lock_or_burn( + &mut pool.token_pool_state, + &fa, + &input, + fa_amount + ); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + + let remote_chain_selector = + token_admin_registry::get_lock_or_burn_remote_chain_selector(&input); + assert!( + pool.chain_to_domain.contains(remote_chain_selector), + error::invalid_argument(E_DOMAIN_NOT_FOUND) + ); + + let remote_domain_info = pool.chain_to_domain.borrow(remote_chain_selector); + + assert!( + remote_domain_info.enabled, + error::invalid_argument(E_DOMAIN_DISABLED) + ); + + let mint_recipient_bytes = + token_admin_registry::get_lock_or_burn_receiver(&input); + let mint_recipient = from_bcs::to_address(mint_recipient_bytes); + let nonce = + token_messenger::deposit_for_burn_with_caller( + &store_signer, + fa, + remote_domain_info.domain_identifier, + mint_recipient, + from_bcs::to_address(remote_domain_info.allowed_caller) + ); + + let dest_pool_data = encode_dest_pool_data(pool.local_domain_identifier, nonce); + + token_pool::emit_locked_or_burned( + &mut pool.token_pool_state, fa_amount, remote_chain_selector + ); + + (dest_token_address, dest_pool_data) + } + + public fun release_or_mint_v2( + input: ReleaseOrMintInputV1 + ): (FungibleAsset, u64) acquires USDCTokenPoolState { + let pool = borrow_pool_mut(); + let local_amount = + token_admin_registry::get_release_or_mint_source_amount(&input) as u64; + + token_pool::validate_release_or_mint( + &mut pool.token_pool_state, &input, local_amount + ); + + let store_signer = account::create_signer_with_capability(&pool.store_signer_cap); + + let (source_domain_identifier, nonce) = + decode_dest_pool_data( + token_admin_registry::get_release_or_mint_source_pool_data(&input) + ); + let offchain_token_data = + token_admin_registry::get_release_or_mint_offchain_token_data(&input); + + let (message_bytes, attestation) = + parse_message_and_attestation(offchain_token_data); + + validate_message( + &message_bytes, + source_domain_identifier, + nonce, + pool.local_domain_identifier + ); + + let receipt = + message_transmitter::receive_message( + &store_signer, &message_bytes, &attestation + ); + + assert!(token_messenger::handle_receive_message(receipt)); + + let recipient = token_admin_registry::get_release_or_mint_receiver(&input); + let remote_chain_selector = + token_admin_registry::get_release_or_mint_remote_chain_selector(&input); + + token_pool::emit_released_or_minted( + &mut pool.token_pool_state, + recipient, + local_amount, + remote_chain_selector + ); + + let fa_metadata = token_pool::get_fa_metadata(&pool.token_pool_state); + + (fungible_asset::zero(fa_metadata), local_amount) + } + // ================================================================ // | USDC Domains | // ================================================================ @@ -922,4 +1038,10 @@ module usdc_token_pool::usdc_token_pool { public fun test_init_module(publisher: &signer) { init_module(publisher); } + + #[test_only] + /// Used for registering the pool with V2 closure-based callbacks. + public fun create_callback_proof(): CallbackProof { + CallbackProof {} + } } diff --git a/contracts/ccip/ccip_token_pools/usdc_token_pool/tests/upgrade_v2.move b/contracts/ccip/ccip_token_pools/usdc_token_pool/tests/upgrade_v2.move new file mode 100644 index 00000000..62ecd44d --- /dev/null +++ b/contracts/ccip/ccip_token_pools/usdc_token_pool/tests/upgrade_v2.move @@ -0,0 +1,50 @@ +#[test_only] +module usdc_token_pool::upgrade_v2 { + use std::account::{Self}; + use std::error; + use std::fungible_asset::{Metadata}; + use std::object::{Self}; + + use usdc_token_pool::usdc_token_pool; + + use ccip::token_admin_registry::{Self}; + + const E_INVALID_FUNGIBLE_ASSET: u64 = 1; + + fun init_module(publisher: &signer) { + // register the pool on deployment, because in the case of object code deployment, + // this is the only time we have a signer ref to @usdc_token_pool. + assert!( + object::object_exists(@local_token), + error::invalid_argument(E_INVALID_FUNGIBLE_ASSET) + ); + + // create an Account on the object for event handles. + account::create_account_if_does_not_exist(@usdc_token_pool); + + // the name of this module. if incorrect, callbacks will fail to be registered and + // register_pool will revert. + let token_pool_module_name = b"usdc_token_pool"; + + let lock_or_burn_closure = |fa, input| usdc_token_pool::lock_or_burn_v2( + fa, input + ); + let release_or_mint_closure = |input| usdc_token_pool::release_or_mint_v2(input); + + // If the contract has already been deployed with V1 and needs to be upgraded to V2, + // create a new module and pass in `publisher` from `fun init_module(publisher: &signer)` + token_admin_registry::register_pool_v2( + publisher, + token_pool_module_name, + @local_token, + lock_or_burn_closure, + release_or_mint_closure, + usdc_token_pool::create_callback_proof() + ); + } + + #[test_only] + public fun test_init_module(publisher: &signer) { + init_module(publisher); + } +}