From 68ffe1f12cba471758ea51c188c861d712ac7a2e Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Thu, 7 Sep 2023 16:35:19 -0500 Subject: [PATCH] solana: refactor; fix various things; add tests Co-authored-by: gator-boi --- solana/Anchor.toml | 32 +- solana/Cargo.lock | 18 +- solana/Makefile | 17 +- .../core-bridge-hello-world/Cargo.toml | 3 +- .../core-bridge-hello-world/src/lib.rs | 40 +- .../token-bridge-hello-world/Cargo.toml | 26 + .../token-bridge-hello-world/src/lib.rs | 320 +++++++ solana/old-tests/.gitignore | 3 - .../00__environment/00__environment.ts | 120 --- .../01__setUpPrograms/01__coreBridge.ts | 126 --- .../01__setUpPrograms/bong02__tokenBridge.ts | 87 -- .../02__coreBridge/000__oldImplementation.ts | 73 -- .../02__coreBridge/001__legacyPostMessage.ts | 160 ---- .../02__coreBridge/002__legacyPostVaa.ts | 45 - .../003__legacySetMessageFee.ts | 78 -- .../02__coreBridge/004__legacyTransferFees.ts | 113 --- .../006__legacyGuardianSetUpdate.ts | 222 ----- .../008__legacyPostMessageUnreliable.ts | 265 ------ .../02__coreBridge/100__newImplementation.ts | 73 -- .../02__coreBridge/101__legacyPostMessage.ts | 185 ----- .../02__coreBridge/102__legacyPostVaa.ts | 284 ------- .../103__legacySetMessageFee.ts | 141 ---- .../02__coreBridge/104__legacyTransferFees.ts | 113 --- .../106__legacyGuardianSetUpdate.ts | 211 ----- .../108__legacyPostMessageUnreliable.ts | 182 ---- .../02__coreBridge/201__PostMessage.ts | 211 ----- .../02__coreBridge/202__ParseAndVerify.ts | 374 --------- solana/old-tests/helpers/consts.ts | 64 -- solana/old-tests/helpers/index.ts | 4 - solana/old-tests/helpers/misc.ts | 36 - solana/old-tests/helpers/transaction.ts | 125 --- solana/old-tests/helpers/vaa.ts | 19 - solana/old-tests/run_mocha_tests.sh | 77 -- solana/package-lock.json | 148 +++- solana/package.json | 1 + solana/programs/core-bridge/Cargo.toml | 4 +- solana/programs/core-bridge/README.md | 107 ++- solana/programs/core-bridge/src/constants.rs | 4 +- solana/programs/core-bridge/src/error.rs | 12 +- .../core-bridge/src/legacy/accounts.rs | 40 - .../core-bridge/src/legacy/accounts/mod.rs | 85 ++ .../src/legacy/instruction/initialize.rs | 9 - .../core-bridge/src/legacy/instruction/mod.rs | 128 ++- .../src/legacy/instruction/post_message.rs | 53 -- .../instruction/post_message_unreliable.rs | 39 - .../src/legacy/instruction/post_vaa.rs | 22 - .../legacy/instruction/verify_signatures.rs | 11 - solana/programs/core-bridge/src/legacy/mod.rs | 33 +- .../governance/guardian_set_update.rs | 94 ++- .../src/legacy/processor/governance/mod.rs | 27 +- .../processor/governance/set_message_fee.rs | 65 +- .../processor/governance/transfer_fees.rs | 70 +- .../processor/governance/upgrade_contract.rs | 57 +- .../src/legacy/processor/post_message/mod.rs | 195 +++-- .../processor/post_message/unreliable.rs | 55 +- .../src/legacy/processor/post_vaa.rs | 19 +- .../src/legacy/processor/verify_signatures.rs | 4 +- .../core-bridge/src/legacy/state/claim.rs | 25 - .../src/legacy/state/guardian_set.rs | 3 +- .../core-bridge/src/legacy/state/mod.rs | 6 - .../mod.rs} | 11 +- .../unreliable.rs} | 9 +- .../src/legacy/state/posted_vaa_v1.rs | 7 +- .../core-bridge/src/legacy/utils/mod.rs | 61 +- solana/programs/core-bridge/src/lib.rs | 70 +- .../parse_and_verify_vaa/close_encoded_vaa.rs | 36 + .../close_posted_vaa_v1.rs | 24 +- .../parse_and_verify_vaa/init_encoded_vaa.rs | 56 +- .../src/processor/parse_and_verify_vaa/mod.rs | 18 +- .../parse_and_verify_vaa/post_vaa_v1.rs | 103 ++- .../process_encoded_vaa.rs | 261 ------ .../verify_encoded_vaa_v1.rs | 161 ++++ .../parse_and_verify_vaa/write_encoded_vaa.rs | 68 ++ .../post_message/close_message_v1.rs | 47 ++ .../post_message/finalize_message_v1.rs | 70 ++ .../processor/post_message/init_message_v1.rs | 84 +- .../src/processor/post_message/mod.rs | 10 +- .../post_message/process_message_v1.rs | 161 ---- .../post_message/write_message_v1.rs | 73 ++ .../src/sdk/cpi/close_encoded_vaa.rs | 24 + .../programs/core-bridge/src/sdk/cpi/mod.rs | 27 +- .../src/sdk/cpi/prepare_message.rs | 204 +++++ .../src/sdk/cpi/prepare_message_v1.rs | 91 -- .../src/sdk/cpi/publish_message.rs | 325 ++++---- solana/programs/core-bridge/src/sdk/mod.rs | 34 +- .../core-bridge/src/state/encoded_vaa.rs | 48 +- solana/programs/core-bridge/src/types.rs | 60 +- solana/programs/core-bridge/src/utils/cpi.rs | 39 + solana/programs/core-bridge/src/utils/mod.rs | 10 +- solana/programs/core-bridge/src/utils/vaa.rs | 78 +- .../core-bridge/src/zero_copy/config.rs | 15 +- .../core-bridge/src/zero_copy/encoded_vaa.rs | 85 -- .../core-bridge/src/zero_copy/message/mod.rs | 49 ++ .../zero_copy/message/posted_message_v1.rs | 98 +++ .../programs/core-bridge/src/zero_copy/mod.rs | 20 +- .../src/zero_copy/posted_message_v1.rs | 80 -- .../zero_copy/posted_message_v1_unreliable.rs | 78 -- .../src/zero_copy/vaa/encoded_vaa.rs | 112 +++ .../core-bridge/src/zero_copy/vaa/mod.rs | 86 ++ .../src/zero_copy/{ => vaa}/posted_vaa_v1.rs | 63 +- .../mock-cpi/src/processor/core_bridge/mod.rs | 4 +- .../src/processor/core_bridge/post_message.rs | 67 +- .../core_bridge/post_message_unreliable.rs | 35 +- ...epare_message_v1.rs => prepare_message.rs} | 14 +- .../legacy/complete_transfer/native.rs | 95 ++- .../legacy/complete_transfer/wrapped.rs | 92 ++- .../complete_transfer_with_payload/native.rs | 109 ++- .../complete_transfer_with_payload/wrapped.rs | 107 ++- .../legacy/transfer_tokens/native.rs | 123 ++- .../legacy/transfer_tokens/wrapped.rs | 117 ++- .../transfer_tokens_with_payload/native.rs | 177 ++-- .../transfer_tokens_with_payload/wrapped.rs | 168 ++-- solana/programs/token-bridge/Cargo.toml | 4 +- solana/programs/token-bridge/README.md | 779 ++++++++++++++++++ solana/programs/token-bridge/src/constants.rs | 5 + solana/programs/token-bridge/src/error.rs | 42 +- .../token-bridge/src/legacy/accounts/mod.rs | 358 ++++++++ .../src/legacy/instruction/attest_token.rs | 6 - .../instruction/complete_transfer/mod.rs | 5 - .../instruction/complete_transfer/native.rs | 54 -- .../instruction/complete_transfer/wrapped.rs | 54 -- .../complete_transfer_with_payload/mod.rs | 5 - .../complete_transfer_with_payload/native.rs | 54 -- .../complete_transfer_with_payload/wrapped.rs | 54 -- .../src/legacy/instruction/initialize.rs | 7 - .../src/legacy/instruction/mod.rs | 141 +++- .../legacy/instruction/transfer_tokens/mod.rs | 16 - .../instruction/transfer_tokens/native.rs | 61 -- .../instruction/transfer_tokens/wrapped.rs | 60 -- .../transfer_tokens_with_payload/mod.rs | 18 - .../transfer_tokens_with_payload/native.rs | 63 -- .../transfer_tokens_with_payload/wrapped.rs | 62 -- .../programs/token-bridge/src/legacy/mod.rs | 592 +++++++------ .../src/legacy/processor/attest_token.rs | 105 ++- .../legacy/processor/complete_transfer/mod.rs | 108 ++- .../processor/complete_transfer/native.rs | 153 ++-- .../processor/complete_transfer/wrapped.rs | 149 ++-- .../complete_transfer_with_payload/mod.rs | 104 ++- .../complete_transfer_with_payload/native.rs | 134 +-- .../complete_transfer_with_payload/wrapped.rs | 135 +-- .../processor/create_or_update_wrapped.rs | 243 ++++-- .../src/legacy/processor/governance/mod.rs | 24 - .../processor/governance/upgrade_contract.rs | 66 +- .../legacy/processor/transfer_tokens/mod.rs | 77 ++ .../processor/transfer_tokens/native.rs | 110 +-- .../processor/transfer_tokens/wrapped.rs | 101 ++- .../transfer_tokens_with_payload/mod.rs | 78 ++ .../transfer_tokens_with_payload/native.rs | 117 +-- .../transfer_tokens_with_payload/wrapped.rs | 104 ++- .../token-bridge/src/legacy/state/claim.rs | 20 - .../token-bridge/src/legacy/state/mod.rs | 9 +- .../src/legacy/state/wrapped_asset.rs | 47 +- solana/programs/token-bridge/src/lib.rs | 29 +- solana/programs/token-bridge/src/messages.rs | 3 + .../src/processor/governance/mod.rs | 22 +- .../processor/governance/register_chain.rs | 110 ++- .../governance/secure_registered_emitter.rs | 45 +- .../token-bridge/src/processor/mod.rs | 136 --- .../src/processor/transfer_tokens.rs | 199 ----- .../transfer_tokens_with_payload/mod.rs | 1 - .../src/sdk/cpi/complete_transfer.rs | 218 +++++ .../programs/token-bridge/src/sdk/cpi/mod.rs | 20 + .../src/sdk/cpi/transfer_tokens.rs | 352 ++++++++ solana/programs/token-bridge/src/sdk/mod.rs | 34 + solana/programs/token-bridge/src/state/mod.rs | 2 + solana/programs/token-bridge/src/utils/cpi.rs | 222 +++++ solana/programs/token-bridge/src/utils/mod.rs | 8 +- .../programs/token-bridge/src/utils/token.rs | 34 +- solana/programs/token-bridge/src/utils/vaa.rs | 17 +- .../token-bridge/src/zero_copy/mint.rs | 64 +- .../src/zero_copy/token_account.rs | 39 +- solana/scripts/prune_idl_types.ts | 51 ++ solana/scripts/tsconfig.json | 15 + .../tests/01__coreBridge/006__postMessage.ts | 72 ++ .../008__postMessageUnreliable.ts | 79 +- .../01__coreBridge/010__setMessageFee.ts | 4 +- .../tests/01__coreBridge/012__transferFees.ts | 22 +- .../01__coreBridge/014__guardianSetUpdate.ts | 6 +- .../01__coreBridge/098__upgradeContract.ts | 30 +- .../01__coreBridge/102__processMessageV1.ts | 103 +-- .../tests/01__coreBridge/104__postMessage.ts | 130 ++- .../01__coreBridge/110__initEncodedVaa.ts | 2 +- .../01__coreBridge/112__processEncodedVaa.ts | 266 +++--- solana/tests/01__coreBridge/114__postVaaV1.ts | 86 +- .../002__createOrUpdateWrapped.ts | 223 ++++- .../tests/02__tokenBridge/003__attestToken.ts | 112 ++- .../004__transferTokensNative.ts | 84 ++ .../006__transferTokensWithPayloadNative.ts | 87 +- .../012__completeTransferNative.ts | 130 +++ .../014__completeTransferWithPayloadNative.ts | 148 +++- .../022__completeTransferWrapped.ts | 149 +++- ...024__completeTransferWithPayloadWrapped.ts | 194 ++++- .../026__transferTokensWrapped.ts | 98 ++- .../028__transferTokensWithPayloadWrapped.ts | 97 ++- .../02__tokenBridge/098__upgradeContract.ts | 13 +- .../02__tokenBridge/100__registerChain.ts | 7 +- .../102__secureRegisteredEmitter.ts | 26 +- solana/tests/03__mockCpi/100__coreBridge.ts | 1 - solana/tests/03__mockCpi/200__tokenBridge.ts | 51 +- .../tests/accounts/fork.wrapped_asset_2.json | 13 + .../tests/accounts/fork.wrapped_mint_2.json | 13 + .../fork.wrapped_mint_2_metadata.json | 13 + solana/tests/accounts/wrapped_asset_2.json | 13 + solana/tests/accounts/wrapped_mint_2.json | 13 + .../accounts/wrapped_mint_2_metadata.json | 13 + solana/tests/helpers/consts.ts | 6 + solana/tests/helpers/coreBridge/consts.ts | 4 - .../parseAndVerify/closeEncodedVaa.ts | 14 + .../instructions/parseAndVerify/index.ts | 2 + .../instructions/parseAndVerify/postVaaV1.ts | 20 +- .../parseAndVerify/processEncodedVaa.ts | 26 +- .../parseAndVerify/writeEncodedVaa.ts | 20 + .../postMessage/closeMessageV1.ts | 15 + .../postMessage/finalizeMessageV1.ts | 14 + .../instructions/postMessage/index.ts | 4 +- .../postMessage/processMessageV1.ts | 30 - .../postMessage/writeMessageV1.ts | 20 + .../helpers/coreBridge/state/EncodedVaa.ts | 21 +- solana/tests/helpers/coreBridge/testing.ts | 40 +- .../governance/secureRegisteredEmitter.ts | 13 +- .../instructions/completeTransfer/native.ts | 32 +- .../instructions/completeTransfer/wrapped.ts | 18 +- .../completeTransferWithPayload/native.ts | 32 +- .../completeTransferWithPayload/wrapped.ts | 18 +- .../instructions/createOrUpdateWrapped.ts | 18 +- .../governance/upgradeContract.ts | 11 +- .../instructions/transferTokens/native.ts | 24 +- .../instructions/transferTokens/wrapped.ts | 26 +- .../transferTokensWithPayload/native.ts | 24 +- .../transferTokensWithPayload/wrapped.ts | 28 +- .../tokenBridge/legacy/state/WrappedAsset.ts | 23 +- solana/tests/helpers/utils.ts | 41 +- 232 files changed, 9479 insertions(+), 8535 deletions(-) create mode 100644 solana/examples/token-bridge-hello-world/Cargo.toml create mode 100644 solana/examples/token-bridge-hello-world/src/lib.rs delete mode 100644 solana/old-tests/.gitignore delete mode 100644 solana/old-tests/00__environment/00__environment.ts delete mode 100644 solana/old-tests/01__setUpPrograms/01__coreBridge.ts delete mode 100644 solana/old-tests/01__setUpPrograms/bong02__tokenBridge.ts delete mode 100644 solana/old-tests/02__coreBridge/000__oldImplementation.ts delete mode 100644 solana/old-tests/02__coreBridge/001__legacyPostMessage.ts delete mode 100644 solana/old-tests/02__coreBridge/002__legacyPostVaa.ts delete mode 100644 solana/old-tests/02__coreBridge/003__legacySetMessageFee.ts delete mode 100644 solana/old-tests/02__coreBridge/004__legacyTransferFees.ts delete mode 100644 solana/old-tests/02__coreBridge/006__legacyGuardianSetUpdate.ts delete mode 100644 solana/old-tests/02__coreBridge/008__legacyPostMessageUnreliable.ts delete mode 100644 solana/old-tests/02__coreBridge/100__newImplementation.ts delete mode 100644 solana/old-tests/02__coreBridge/101__legacyPostMessage.ts delete mode 100644 solana/old-tests/02__coreBridge/102__legacyPostVaa.ts delete mode 100644 solana/old-tests/02__coreBridge/103__legacySetMessageFee.ts delete mode 100644 solana/old-tests/02__coreBridge/104__legacyTransferFees.ts delete mode 100644 solana/old-tests/02__coreBridge/106__legacyGuardianSetUpdate.ts delete mode 100644 solana/old-tests/02__coreBridge/108__legacyPostMessageUnreliable.ts delete mode 100644 solana/old-tests/02__coreBridge/201__PostMessage.ts delete mode 100644 solana/old-tests/02__coreBridge/202__ParseAndVerify.ts delete mode 100644 solana/old-tests/helpers/consts.ts delete mode 100644 solana/old-tests/helpers/index.ts delete mode 100644 solana/old-tests/helpers/misc.ts delete mode 100644 solana/old-tests/helpers/transaction.ts delete mode 100644 solana/old-tests/helpers/vaa.ts delete mode 100755 solana/old-tests/run_mocha_tests.sh delete mode 100644 solana/programs/core-bridge/src/legacy/accounts.rs create mode 100644 solana/programs/core-bridge/src/legacy/accounts/mod.rs delete mode 100644 solana/programs/core-bridge/src/legacy/instruction/initialize.rs delete mode 100644 solana/programs/core-bridge/src/legacy/instruction/post_message.rs delete mode 100644 solana/programs/core-bridge/src/legacy/instruction/post_message_unreliable.rs delete mode 100644 solana/programs/core-bridge/src/legacy/instruction/post_vaa.rs delete mode 100644 solana/programs/core-bridge/src/legacy/instruction/verify_signatures.rs delete mode 100644 solana/programs/core-bridge/src/legacy/state/claim.rs rename solana/programs/core-bridge/src/legacy/state/{posted_message_v1.rs => posted_message_v1/mod.rs} (94%) rename solana/programs/core-bridge/src/legacy/state/{posted_message_v1_unreliable.rs => posted_message_v1/unreliable.rs} (84%) create mode 100644 solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_encoded_vaa.rs delete mode 100644 solana/programs/core-bridge/src/processor/parse_and_verify_vaa/process_encoded_vaa.rs create mode 100644 solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs create mode 100644 solana/programs/core-bridge/src/processor/parse_and_verify_vaa/write_encoded_vaa.rs create mode 100644 solana/programs/core-bridge/src/processor/post_message/close_message_v1.rs create mode 100644 solana/programs/core-bridge/src/processor/post_message/finalize_message_v1.rs delete mode 100644 solana/programs/core-bridge/src/processor/post_message/process_message_v1.rs create mode 100644 solana/programs/core-bridge/src/processor/post_message/write_message_v1.rs create mode 100644 solana/programs/core-bridge/src/sdk/cpi/close_encoded_vaa.rs create mode 100644 solana/programs/core-bridge/src/sdk/cpi/prepare_message.rs delete mode 100644 solana/programs/core-bridge/src/sdk/cpi/prepare_message_v1.rs create mode 100644 solana/programs/core-bridge/src/utils/cpi.rs delete mode 100644 solana/programs/core-bridge/src/zero_copy/encoded_vaa.rs create mode 100644 solana/programs/core-bridge/src/zero_copy/message/mod.rs create mode 100644 solana/programs/core-bridge/src/zero_copy/message/posted_message_v1.rs delete mode 100644 solana/programs/core-bridge/src/zero_copy/posted_message_v1.rs delete mode 100644 solana/programs/core-bridge/src/zero_copy/posted_message_v1_unreliable.rs create mode 100644 solana/programs/core-bridge/src/zero_copy/vaa/encoded_vaa.rs create mode 100644 solana/programs/core-bridge/src/zero_copy/vaa/mod.rs rename solana/programs/core-bridge/src/zero_copy/{ => vaa}/posted_vaa_v1.rs (62%) rename solana/programs/mock-cpi/src/processor/core_bridge/{prepare_message_v1.rs => prepare_message.rs} (84%) create mode 100644 solana/programs/token-bridge/README.md create mode 100644 solana/programs/token-bridge/src/legacy/accounts/mod.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/attest_token.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer/mod.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer/native.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer/wrapped.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/mod.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/native.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/wrapped.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/initialize.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/mod.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/native.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/wrapped.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/mod.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/native.rs delete mode 100644 solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/wrapped.rs delete mode 100644 solana/programs/token-bridge/src/legacy/state/claim.rs delete mode 100644 solana/programs/token-bridge/src/processor/transfer_tokens.rs delete mode 100644 solana/programs/token-bridge/src/processor/transfer_tokens_with_payload/mod.rs create mode 100644 solana/programs/token-bridge/src/sdk/cpi/complete_transfer.rs create mode 100644 solana/programs/token-bridge/src/sdk/cpi/mod.rs create mode 100644 solana/programs/token-bridge/src/sdk/cpi/transfer_tokens.rs create mode 100644 solana/programs/token-bridge/src/sdk/mod.rs create mode 100644 solana/programs/token-bridge/src/utils/cpi.rs create mode 100644 solana/scripts/prune_idl_types.ts create mode 100644 solana/scripts/tsconfig.json create mode 100644 solana/tests/accounts/fork.wrapped_asset_2.json create mode 100644 solana/tests/accounts/fork.wrapped_mint_2.json create mode 100644 solana/tests/accounts/fork.wrapped_mint_2_metadata.json create mode 100644 solana/tests/accounts/wrapped_asset_2.json create mode 100644 solana/tests/accounts/wrapped_mint_2.json create mode 100644 solana/tests/accounts/wrapped_mint_2_metadata.json create mode 100644 solana/tests/helpers/coreBridge/instructions/parseAndVerify/closeEncodedVaa.ts create mode 100644 solana/tests/helpers/coreBridge/instructions/parseAndVerify/writeEncodedVaa.ts create mode 100644 solana/tests/helpers/coreBridge/instructions/postMessage/closeMessageV1.ts create mode 100644 solana/tests/helpers/coreBridge/instructions/postMessage/finalizeMessageV1.ts delete mode 100644 solana/tests/helpers/coreBridge/instructions/postMessage/processMessageV1.ts create mode 100644 solana/tests/helpers/coreBridge/instructions/postMessage/writeMessageV1.ts diff --git a/solana/Anchor.toml b/solana/Anchor.toml index 333217c48c..8eebe6cbb9 100644 --- a/solana/Anchor.toml +++ b/solana/Anchor.toml @@ -78,4 +78,34 @@ filename = "tests/accounts/registered_emitter_2.json" ### Registered Token Bridge (Ethereum) for Fork [[test.validator.account]] address = "DujfLgMKW71CT2W8pxknf42FT86VbcK5PjQ6LsutjWKC" -filename = "tests/accounts/fork.registered_emitter_2.json" \ No newline at end of file +filename = "tests/accounts/fork.registered_emitter_2.json" + +### Token Bridge Wrapped Mint (Ethereum) +[[test.validator.account]] +address = "ATdV7cKoJnw3s6kaoY2jrQAnoc3wg3PmGpHFdUuHYK67" +filename = "tests/accounts/wrapped_mint_2.json" + +### Token Bridge Wrapped Asset (Ethereum) +[[test.validator.account]] +address = "CfBrfpS7cCNU5m5FtHvV7mPoURa71kT34Mf9XP8dpjmb" +filename = "tests/accounts/wrapped_asset_2.json" + +### Token Bridge Wrapped Mint (Ethereum) Metadata +[[test.validator.account]] +address = "6XqcNZL4yn8X8saab9fMuJRxZFEtNyp96DCeDTkqxduY" +filename = "tests/accounts/wrapped_mint_2_metadata.json" + +### Token Bridge Wrapped Mint (Ethereum) for Fork +[[test.validator.account]] +address = "CAjoti21mmQZxtdTdmrfUL7oCzA1Yqa3u6NdWvFkmqej" +filename = "tests/accounts/fork.wrapped_mint_2.json" + +### Token Bridge Wrapped Asset (Ethereum) for Fork +[[test.validator.account]] +address = "CQn6Ts4su6V7upffbEthe2xsVnSxMdTC42yZWKRveXkU" +filename = "tests/accounts/fork.wrapped_asset_2.json" + +### Token Bridge Wrapped Mint (Ethereum) Metadata for Fork +[[test.validator.account]] +address = "BR9RYXcjBzyKYVfze3K2SbFFS5DG78UUfUf1Peh16uPz" +filename = "tests/accounts/fork.wrapped_mint_2_metadata.json" \ No newline at end of file diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 44420661ed..a27b60c182 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -521,7 +521,6 @@ name = "core-bridge-hello-world" version = "0.0.0" dependencies = [ "anchor-lang", - "anchor-spl", "wormhole-core-bridge-solana", ] @@ -1997,6 +1996,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token-bridge-hello-world" +version = "0.0.0" +dependencies = [ + "anchor-lang", + "anchor-spl", + "wormhole-token-bridge-solana", +] + [[package]] name = "toml" version = "0.5.11" @@ -2251,7 +2259,7 @@ dependencies = [ [[package]] name = "wormhole-core-bridge-solana" -version = "0.0.0-alpha.4" +version = "0.0.0-alpha.6" dependencies = [ "anchor-lang", "cfg-if", @@ -2280,9 +2288,9 @@ dependencies = [ [[package]] name = "wormhole-raw-vaas" -version = "0.0.0-alpha.7" +version = "0.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1a069e2dd4b3db0899b5e2284d866ada874952c9c64cede1011d11dc839231d" +checksum = "0dc5314e1f430f0a77fedb578fdb1cd6dfe830673f2b51497a5cf77902883c00" dependencies = [ "ruint", "ruint-macro", @@ -2290,7 +2298,7 @@ dependencies = [ [[package]] name = "wormhole-token-bridge-solana" -version = "0.0.0-alpha.1" +version = "0.0.0-alpha.2" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/solana/Makefile b/solana/Makefile index 8a42a5c90c..794314fc39 100644 --- a/solana/Makefile +++ b/solana/Makefile @@ -5,10 +5,13 @@ out_mainnet=artifacts-mainnet out_testnet=artifacts-testnet out_localnet=artifacts-localnet -.PHONY: all clean check build test lint +.PHONY: all clean check build test lint prune_idl ci all: check +check: + cargo check --all-features + clean: anchor clean rm -rf node_modules artifacts-mainnet artifacts-testnet artifacts-localnet tests/artifacts @@ -16,6 +19,9 @@ clean: node_modules: npm ci +prune_idl: scripts/prune_idl_types.ts + cd scripts && npx ts-node prune_idl_types.ts + build: $(out_$(NETWORK)) $(out_$(NETWORK)): ifdef out_$(NETWORK) @@ -23,18 +29,21 @@ ifdef out_$(NETWORK) anchor build -p wormhole_token_bridge_solana --arch sbf -- --features "$(NETWORK)" -- --no-default-features mkdir -p $(out_$(NETWORK)) cp target/deploy/*.so $(out_$(NETWORK))/ + $(MAKE) prune_idl endif test: node_modules - cargo test + cargo test --all-features anchor build -p wormhole_core_bridge_solana --arch sbf -- --features mainnet -- --no-default-features anchor build -p wormhole_token_bridge_solana --arch sbf -- --features mainnet -- --no-default-features mkdir -p tests/artifacts && cp target/deploy/*.so tests/artifacts/ - anchor test --arch sbf -- --features localnet -- --no-default-features + anchor build --arch sbf -- --features localnet -- --no-default-features + $(MAKE) prune_idl + anchor test --skip-build lint: cargo fmt --check - cargo clippy --no-deps --all-targets -- -D warnings + cargo clippy --no-deps --all-targets --all-features -- -D warnings ci: DOCKER_BUILDKIT=1 docker build -f Dockerfile.ci \ diff --git a/solana/examples/core-bridge-hello-world/Cargo.toml b/solana/examples/core-bridge-hello-world/Cargo.toml index eb44bd5366..784cc51c6e 100644 --- a/solana/examples/core-bridge-hello-world/Cargo.toml +++ b/solana/examples/core-bridge-hello-world/Cargo.toml @@ -22,5 +22,4 @@ cpi = ["no-entrypoint"] [dependencies] wormhole-core-bridge-solana = { path = "../../programs/core-bridge", features = ["cpi"], default-features = false } -anchor-lang = { version = "0.28.0", features = ["init-if-needed"] } -anchor-spl = "0.28.0" +anchor-lang = "0.28.0" diff --git a/solana/examples/core-bridge-hello-world/src/lib.rs b/solana/examples/core-bridge-hello-world/src/lib.rs index 862d67b396..4c92bda2c9 100644 --- a/solana/examples/core-bridge-hello-world/src/lib.rs +++ b/solana/examples/core-bridge-hello-world/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::result_large_err)] + use anchor_lang::prelude::*; use wormhole_core_bridge_solana::sdk as core_bridge_sdk; @@ -30,19 +32,15 @@ pub struct PublishHelloWorld<'info> { /// CHECK: This account is needed for the Core Bridge program. #[account(mut)] - core_fee_collector: Option>, + core_fee_collector: UncheckedAccount<'info>, system_program: Program<'info, System>, core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for PublishHelloWorld<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} - -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for PublishHelloWorld<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -53,16 +51,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'in } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_emitter(&self) -> Option> { - None + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter_authority(&self) -> Option> { - Some(self.core_program_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_program_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -70,13 +68,7 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'i } fn core_fee_collector(&self) -> Option> { - self.core_fee_collector - .as_ref() - .map(|acc| acc.to_account_info()) - } - - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + Some(self.core_fee_collector.to_account_info()) } } @@ -90,17 +82,17 @@ pub mod core_bridge_hello_world { core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::ProgramMessage { program_id: crate::ID, nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[ + Some(&[&[ core_bridge_sdk::PROGRAM_EMITTER_SEED_PREFIX, &[ctx.bumps["core_program_emitter"]], - ], - None, + ]]), ) } } diff --git a/solana/examples/token-bridge-hello-world/Cargo.toml b/solana/examples/token-bridge-hello-world/Cargo.toml new file mode 100644 index 0000000000..a9a62884c3 --- /dev/null +++ b/solana/examples/token-bridge-hello-world/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "token-bridge-hello-world" +description = "Token Bridge Hello World" +version = "0.0.0" +edition = "2021" +authors = ["W7"] +license = "Apache-2.0" +homepage = "https://docs.rs/wormhole-token-bridge-solana" +repository = "https://github.com/wormhole-foundation/wormhole" + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +default = ["localnet"] +localnet = ["wormhole-token-bridge-solana/localnet"] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] + +[dependencies] +wormhole-token-bridge-solana = { path = "../../programs/token-bridge", features = ["cpi"], default-features = false } + +anchor-lang = "0.28.0" +anchor-spl = "0.28.0" diff --git a/solana/examples/token-bridge-hello-world/src/lib.rs b/solana/examples/token-bridge-hello-world/src/lib.rs new file mode 100644 index 0000000000..3edf6cd14a --- /dev/null +++ b/solana/examples/token-bridge-hello-world/src/lib.rs @@ -0,0 +1,320 @@ +#![allow(clippy::result_large_err)] + +use anchor_lang::prelude::*; +use anchor_spl::token; +use wormhole_token_bridge_solana::sdk::{self as token_bridge_sdk, core_bridge_sdk}; + +declare_id!("TokenBridgeHe11oWor1d1111111111111111111111"); + +#[derive(Accounts)] +pub struct RedeemHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. This account is mutable in case the transferred asset is + /// Token Bridge wrapped, which requires the Token Bridge program to mint to a token account. + #[account( + mut, + owner = token::ID + )] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge complete transfer with payload. + /// This PDA validates the redeemer address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX], + bump, + )] + redeemer_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This account warehouses the + /// VAA of the token transfer from another chain. + encoded_vaa: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + token_bridge_claim: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_registered_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + #[account(mut)] + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_mint_authority: Option>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} + +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for RedeemHelloWorld<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> for RedeemHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.encoded_vaa.to_account_info() + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn redeemer_authority(&self) -> Option> { + Some(self.redeemer_authority.to_account_info()) + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_mint_authority(&self) -> Option> { + self.token_bridge_mint_authority.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } +} + +#[derive(Accounts)] +pub struct TransferHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. This account is mutable in case the transferred asset is + /// Token Bridge wrapped, which requires the Token Bridge program to burn from a token + /// account. + #[account( + mut, + owner = token::ID + )] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge transfer with payload. This PDA + /// validates the sender address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX], + bump, + )] + sender_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_transfer_authority: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + #[account(mut)] + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_core_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_bridge_config: UncheckedAccount<'info>, + + /// CHECK: This account will be created using a generated keypair. + #[account(mut)] + core_message: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_emitter_sequence: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_fee_collector: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + core_bridge_program: UncheckedAccount<'info>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} + +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for TransferHelloWorld<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferHelloWorld<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.token_bridge_core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + Some(self.core_fee_collector.to_account_info()) + } +} + +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> for TransferHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn sender_authority(&self) -> Option> { + Some(self.sender_authority.to_account_info()) + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } +} + +#[program] +pub mod token_bridge_hello_world { + use super::*; + + pub fn redeem_hello_world(ctx: Context) -> Result<()> { + token_bridge_sdk::cpi::complete_transfer( + ctx.accounts, + Some(&[&[ + token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX, + &[ctx.bumps["redeemer_authority"]], + ]]), + ) + } + + pub fn transfer_hello_world(ctx: Context, amount: u64) -> Result<()> { + let nonce = 420; + let redeemer = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xbe, 0xef, + ]; + let redeemer_chain = 2; + let payload = b"Hello, world!".to_vec(); + + token_bridge_sdk::cpi::transfer_tokens( + ctx.accounts, + token_bridge_sdk::cpi::TransferTokensDirective::ProgramTransferWithPayload { + program_id: crate::ID, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, + Some(&[&[ + token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX, + &[ctx.bumps["sender_authority"]], + ]]), + ) + } +} diff --git a/solana/old-tests/.gitignore b/solana/old-tests/.gitignore deleted file mode 100644 index 7446dc570a..0000000000 --- a/solana/old-tests/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.so -.tmp -.test diff --git a/solana/old-tests/00__environment/00__environment.ts b/solana/old-tests/00__environment/00__environment.ts deleted file mode 100644 index afe275619e..0000000000 --- a/solana/old-tests/00__environment/00__environment.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import * as fs from "fs"; -import { BpfProgramData, ProgramData } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - TOKEN_BRIDGE_PROGRAM_ID, - airdrop, - artifactsPath, - coreBridgeKeyPath, - deployProgram, - removeTmpPath, - tmpPath, - tokenBridgeKeyPath, -} from "../helpers"; -import { - Metadata, - PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID, -} from "@metaplex-foundation/mpl-token-metadata"; -import { NATIVE_MINT } from "@solana/spl-token"; - -describe("Environment", () => { - // This is the first test that runs. We will purge the tmp directory if it exists. - removeTmpPath(); - - const connection = new web3.Connection(LOCALHOST, "processed"); - - // Deployer for all programs. - // - // NOTE: After initialization, this keypair will no longer be able to upgrade accounts because - // the upgrade authority will be each program's upgrade PDA. - const deployerSigner = web3.Keypair.generate(); - - // Write keypair to temporary file. - const deployerKeypath = `${tmpPath()}/deployer.json`; - fs.writeFileSync( - deployerKeypath, - JSON.stringify(Array.from(deployerSigner.secretKey)) - ); - - // For convenience. - const deployer = deployerSigner.publicKey; - - // Make sure the deployer has some SOL. - it("Airdrop SOL", async () => { - const lamports = await airdrop(connection, deployer); - - const balance = await connection.getBalance(deployer); - expect(balance).equals(lamports); - }); - - it(`Verify ${NATIVE_MINT.toString()} Token Metadata`, async () => { - const [metadataPubkey, _] = web3.PublicKey.findProgramAddressSync( - [ - Buffer.from("metadata"), - TOKEN_METADATA_PROGRAM_ID.toBuffer(), - NATIVE_MINT.toBuffer(), - ], - TOKEN_METADATA_PROGRAM_ID - ); - - const metadata = await Metadata.fromAccountAddress( - connection, - metadataPubkey - ); - expect(metadata.data.symbol).equals("SOL".padEnd(10, "\u0000")); - expect(metadata.data.name).equals("Wrapped SOL".padEnd(32, "\u0000")); - }); - - it.skip("Create NFTs", async () => { - // TODO - }); - - it.skip("Create NFT Metadata", async () => { - // TODO - }); - - it("Deploy Core Bridge", async () => { - const output = deployProgram( - deployerKeypath, - `${artifactsPath()}/solana_wormhole_core_bridge.so`, - coreBridgeKeyPath() - ); - expect(output).equals(`Program Id: ${CORE_BRIDGE_PROGRAM_ID.toString()}\n`); - - const addr = ProgramData.address(CORE_BRIDGE_PROGRAM_ID); - const programMetadata = await ProgramData.fromAccountAddress( - connection, - addr - ); - - const programData = programMetadata as BpfProgramData; - expect(programData.upgradeAuthorityAddress!.equals(deployer)).is.true; - }); - - it.skip("Deploy Token Bridge", async () => { - const output = deployProgram( - deployerKeypath, - `${artifactsPath()}/solana_wormhole_token_bridge.so`, - tokenBridgeKeyPath() - ); - expect(output).equals( - `Program Id: ${TOKEN_BRIDGE_PROGRAM_ID.toString()}\n` - ); - - const addr = ProgramData.address(TOKEN_BRIDGE_PROGRAM_ID); - const programMetadata = await ProgramData.fromAccountAddress( - connection, - addr - ); - - const programData = programMetadata as BpfProgramData; - expect(programData.upgradeAuthorityAddress!.equals(deployer)).is.true; - }); - - it.skip("Deploy NFT Bridge", async () => { - // TODO - }); -}); diff --git a/solana/old-tests/01__setUpPrograms/01__coreBridge.ts b/solana/old-tests/01__setUpPrograms/01__coreBridge.ts deleted file mode 100644 index 3bdbc24d94..0000000000 --- a/solana/old-tests/01__setUpPrograms/01__coreBridge.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { ethers } from "ethers"; -import { readFileSync } from "fs"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GUARDIAN_KEYS, - LOCALHOST, - artifactsPath, - coreBridgeKeyPath, - deployProgram, - expectIxTransactionDetails, - expectIxErr, - tmpPath, -} from "../helpers"; - -describe("Set Up Programs: Core Bridge", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const deployerSigner = web3.Keypair.fromSecretKey( - Uint8Array.from( - JSON.parse(readFileSync(`${tmpPath()}/deployer.json`, "utf-8")) - ) - ); - const deployer = deployerSigner.publicKey; - - describe("Ok", async () => { - it("Invoke `initialize` with 19 Devnet Guardians", async () => { - const guardianSetIndex = 0; - - // Accounts. - const accounts = coreBridge.InitializeContext.new( - CORE_BRIDGE_PROGRAM_ID, - deployer - ); - - // Instruction handler args. - const guardianSetTtlSeconds = 15; - const feeLamports = new BN(42069); - const initialGuardians = GUARDIAN_KEYS.map( - (privateKey): Array<20> => - Array.from( - ethers.utils.arrayify(new ethers.Wallet(privateKey).address) - ) as 20[] - ); - - const ix = await coreBridge.initializeIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - { - guardianSetTtlSeconds, - feeLamports, - initialGuardians, - } - ); - - const txDetails = await expectIxTransactionDetails( - connection, - [ix], - [deployerSigner] - ); - - const { guardianSet } = accounts; - const guardianSetData = await coreBridge.GuardianSet.fromAccountAddress( - connection, - guardianSet - ); - expect(guardianSetData.creationTime).equals(txDetails?.blockTime); - expect(guardianSetData.expirationTime).equals(0); - expect(guardianSetData.index).equals(0); - - const numGuardians = initialGuardians.length; - expect(guardianSetData.keys).has.length(numGuardians); - for (let i = 0; i < numGuardians; ++i) { - const actual = Buffer.from(guardianSetData.keys[i]); - const expected = Buffer.from(initialGuardians[i]); - expect(actual.equals(expected)).is.true; - } - }); - - // it("Cannot Invoke `initialize` again", async () => { - // // Accounts. - // const accounts = coreBridge.InitializeContext.new( - // CORE_BRIDGE_PROGRAM_ID, - // deployer - // ); - - // // Instruction handler args. - // const guardianSetTtlSeconds = 69; - // const feeLamports = new BN(420); - // const initialGuardians = [Array.from(Buffer.alloc(20, 69)) as 20[]]; - - // // This invocation should fail because we cannot create any of the accounts that have been - // // created before. - // const ix = await coreBridge.initializeIx( - // connection, - // CORE_BRIDGE_PROGRAM_ID, - // accounts, - // { - // guardianSetTtlSeconds, - // feeLamports, - // initialGuardians, - // } - // ); - - // await expectIxErr(connection, [ix], [deployerSigner], "already in use"); - // }); - - // it("Cannot Upgrade Core Bridge without Upgrade Authority", async () => { - // const deployerKeypath = `${tmpPath()}/deployer.json`; - - // try { - // deployProgram( - // deployerKeypath, - // `${artifactsPath()}/solana_wormhole_core_bridge.so`, - // coreBridgeKeyPath() - // ); - // throw new Error("borked"); - // } catch (err) { - // expect(err.stderr).includes("does not match authority provided"); - // } - // }); - }); -}); diff --git a/solana/old-tests/01__setUpPrograms/bong02__tokenBridge.ts b/solana/old-tests/01__setUpPrograms/bong02__tokenBridge.ts deleted file mode 100644 index b7913e11ec..0000000000 --- a/solana/old-tests/01__setUpPrograms/bong02__tokenBridge.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { readFileSync } from "fs"; -import { tokenBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - TOKEN_BRIDGE_PROGRAM_ID, - artifactsPath, - coreBridgeKeyPath, - deployProgram, - expectIxErr, - expectIxOk, - tmpPath, -} from "../helpers"; - -describe("Set Up Programs: Token Bridge", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const deployerSigner = web3.Keypair.fromSecretKey( - Uint8Array.from( - JSON.parse(readFileSync(`${tmpPath()}/deployer.json`, "utf-8")) - ) - ); - const deployer = deployerSigner.publicKey; - - describe("Ok", async () => { - it("Invoke `initialize`", async () => { - // Accounts. - const accounts = tokenBridge.InitializeContext.new( - TOKEN_BRIDGE_PROGRAM_ID, - deployer, - CORE_BRIDGE_PROGRAM_ID - ); - - const ix = await tokenBridge.initializeIx( - connection, - TOKEN_BRIDGE_PROGRAM_ID, - accounts - ); - - await expectIxOk(connection, [ix], [deployerSigner]); - - const configData = await tokenBridge.Config.fromPda( - connection, - TOKEN_BRIDGE_PROGRAM_ID - ); - expect( - configData.coreBridge.equals(new web3.PublicKey(CORE_BRIDGE_PROGRAM_ID)) - ).is.true; - }); - - it("Cannot Invoke `initialize` again", async () => { - // Accounts. - const accounts = tokenBridge.InitializeContext.new( - TOKEN_BRIDGE_PROGRAM_ID, - deployer, - CORE_BRIDGE_PROGRAM_ID - ); - - // This invocation should fail because we cannot create any of the accounts that have been - // created before. - const ix = await tokenBridge.initializeIx( - connection, - TOKEN_BRIDGE_PROGRAM_ID, - accounts - ); - - await expectIxErr(connection, [ix], [deployerSigner], "already in use"); - }); - - it("Cannot Upgrade Token Bridge without Upgrade Authority", async () => { - const deployerKeypath = `${tmpPath()}/deployer.json`; - - try { - deployProgram( - deployerKeypath, - `${artifactsPath()}/solana_wormhole_token_bridge.so`, - coreBridgeKeyPath() - ); - throw new Error("borked"); - } catch (err) { - expect(err.stderr).includes("does not match authority provided"); - } - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/000__oldImplementation.ts b/solana/old-tests/02__coreBridge/000__oldImplementation.ts deleted file mode 100644 index 20e2c66078..0000000000 --- a/solana/old-tests/02__coreBridge/000__oldImplementation.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { createPostSignedVaaTransactions } from "@certusone/wormhole-sdk/lib/cjs/solana/sendAndConfirmPostVaa"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - artifactsPath, - expectIxOk, - loadProgramBpf, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 0; - -describe("Core Bridge: Old Implementation", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - 2_000_000 - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Load Old Implementation", async () => { - const artifactPath = `${__dirname}/../existing_core_bridge.so`; - const bufferAuthority = coreBridge.upgradeAuthority(CORE_BRIDGE_PROGRAM_ID); - const implementation = loadProgramBpf(payerSigner, artifactPath, bufferAuthority); - - localVariables.set("implementation", implementation); - }); - - it("Invoke `upgrade_contract`", async () => { - const newContract: web3.PublicKey = localVariables.get("implementation")!; - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeUpgradeContract( - timestamp, - chain, - newContract.toString() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // Upgrade. - await expectIxOk( - connection, - [coreBridgeSDK.createUpgradeContractInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner] - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/001__legacyPostMessage.ts b/solana/old-tests/02__coreBridge/001__legacyPostMessage.ts deleted file mode 100644 index d3bdc8bf7f..0000000000 --- a/solana/old-tests/02__coreBridge/001__legacyPostMessage.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - COMMON_EMITTER, - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - airdrop, - expectLegacyPostMessageErr, - expectLegacyPostMessageOk, -} from "../helpers"; -import { expect } from "chai"; - -describe("Core Bridge: Legacy Post Message", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - let payerSequence = new BN(0); - let commonEmitterSequence = new BN(0); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Known Issues", async () => { - it("Cannot Invoke `post_message` Without Clock Sysvar", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.alloc(69); - const finalityRepr = 0; - - // We must pay the fee collector prior publishing a message. - const preInstructions = await Promise.all([ - coreBridge.transferMessageFeeIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - payer - ), - ]); - - await expectLegacyPostMessageErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - "InvalidSysvar", - preInstructions - ); - }); - }); - - describe("Ok", async () => { - it("Invoke `post_message` With Small Payload", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("All your base are belong to us."); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `post_message` Again With Same Emitter", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 69; - const payload = Buffer.from("Somebody set up us the bomb."); - const finalityRepr = 1; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `post_message` With Payer as Emitter", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("I'm the captain now."); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - payerSequence - ); - payerSequence.iaddn(1); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/002__legacyPostVaa.ts b/solana/old-tests/02__coreBridge/002__legacyPostVaa.ts deleted file mode 100644 index 87cbc64f6b..0000000000 --- a/solana/old-tests/02__coreBridge/002__legacyPostVaa.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { web3 } from "@coral-xyz/anchor"; -import { GUARDIAN_KEYS, LOCALHOST, airdrop, invokeVerifySignaturesAndPostVaa } from "../helpers"; - -const GUARDIAN_SET_INDEX = 0; - -describe("Core Bridge: Legacy Verify Signatures and Post VAA", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const foreignEmitter = new MockEmitter( - "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", - 2, - 2_002_000 - ); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `legacy_verify_signatures` and `legacy_post_vaa`", async () => { - const timestamp = 12345678; - const nonce = 420; - const payload = Buffer.from("Someone set us up the bomb."); - const consistencyLevel = 1; - const published = foreignEmitter.publishMessage(nonce, payload, consistencyLevel, timestamp); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // TODO: Check Posted VAA account. - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/003__legacySetMessageFee.ts b/solana/old-tests/02__coreBridge/003__legacySetMessageFee.ts deleted file mode 100644 index 044d6977c2..0000000000 --- a/solana/old-tests/02__coreBridge/003__legacySetMessageFee.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; - -const GUARDIAN_SET_INDEX = 0; -const GOVERNANCE_SEQUENCE = 2_003_000; - -describe("Core Bridge: Legacy Set Message Fee (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `set_message_fee`", async () => { - const amount = new BN(6969); - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeSetMessageFee( - timestamp, - chain, - BigInt(amount.toString()) - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // Set message fee. - await expectIxOk( - connection, - [coreBridgeSDK.createSetFeesInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner] - ); - - // TODO: Check bridge program data to see if message fee was set correctly. - - localVariables.set("signedVaa", signedVaa); - }); - - it("Cannot Invoke `set_message_fee` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - - await expectIxErr( - connection, - [coreBridgeSDK.createSetFeesInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner], - "AlreadyInitialized" - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/004__legacyTransferFees.ts b/solana/old-tests/02__coreBridge/004__legacyTransferFees.ts deleted file mode 100644 index 588573caf4..0000000000 --- a/solana/old-tests/02__coreBridge/004__legacyTransferFees.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 0; -const GOVERNANCE_SEQUENCE = 2_004_000; - -describe("Core Bridge: Legacy Transfer Fees (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `transfer_fees`", async () => { - const recipient = web3.Keypair.generate().publicKey; - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(0); - } - - // First send lamports over to the fee collector. - const amount = 42069420; - const transferIx = web3.SystemProgram.transfer({ - fromPubkey: payer, - toPubkey: coreBridge.FeeCollector.address(CORE_BRIDGE_PROGRAM_ID), - lamports: amount, - }); - await expectIxOk(connection, [transferIx], [payerSigner]); - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeTransferFees( - timestamp, - chain, - BigInt(amount.toString()), - recipient.toBuffer() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedVaa - ), - ], - [payerSigner] - ); - - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(amount); - } - - // Save for later. - localVariables.set("signedVaa", signedVaa); - localVariables.set("recipient", recipient); - }); - - it("Cannot Invoke `transfer_fees` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - const recipient: Buffer = localVariables.get("recipient")!; - - await expectIxErr( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedVaa - ), - ], - [payerSigner], - "AlreadyInitialized" - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/006__legacyGuardianSetUpdate.ts b/solana/old-tests/02__coreBridge/006__legacyGuardianSetUpdate.ts deleted file mode 100644 index a5fb90b211..0000000000 --- a/solana/old-tests/02__coreBridge/006__legacyGuardianSetUpdate.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 0; -const GOVERNANCE_SEQUENCE = 2_006_000; - -describe("Core Bridge: Legacy Guardian Set Update (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Known Issues", async () => { - it("Invoke Core Bridge Governance Without Latest Guardian Set", async () => { - const recipient = web3.Keypair.generate().publicKey; - const amount = 42069420; - - // Post transfer fees VAA, which will be signed with a soon-to-be-expired Guardian set. - const signedTransferFeesVaa = await (async () => { - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeTransferFees( - timestamp, - chain, - BigInt(amount.toString()), - recipient.toBuffer() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - return signedVaa; - })(); - - // Update guardian set. - { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys().slice(0, 1); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - } - - // Now use previously posted VAA to transfer fees. - { - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(0); - } - - // First send lamports over to the fee collector. - const transferIx = web3.SystemProgram.transfer({ - fromPubkey: payer, - toPubkey: coreBridge.FeeCollector.address(CORE_BRIDGE_PROGRAM_ID), - lamports: amount, - }); - await expectIxOk(connection, [transferIx], [payerSigner]); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedTransferFeesVaa - ), - ], - [payerSigner] - ); - - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(amount); - } - } - }); - }); - - describe("Ok", async () => { - it("Invoke `guardian_set_update`", async () => { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys().slice(0, 2); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures(published, [0]); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - - // TODO: Verify guardian set. - - // Save for later. - localVariables.set("signedVaa", signedVaa); - }); - - it("Cannot Invoke `guardian_set_update` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - - await expectIxErr( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner], - "AlreadyInitialized" - ); - }); - - it("Invoke `guardian_set_update` Again to Original Guardian Keys", async () => { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys(); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures(published, [0, 1]); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - - // TODO: Verify guardian set. - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/008__legacyPostMessageUnreliable.ts b/solana/old-tests/02__coreBridge/008__legacyPostMessageUnreliable.ts deleted file mode 100644 index aaff75528d..0000000000 --- a/solana/old-tests/02__coreBridge/008__legacyPostMessageUnreliable.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - COMMON_EMITTER, - COMMON_UNRELIABLE_MESSAGE_SIGNER, - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - airdrop, - expectLegacyPostMessageUnreliableErr, - expectLegacyPostMessageUnreliableOk, -} from "../helpers"; - -describe("Core Bridge: Legacy Post Message Unreliable", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - let payerSequence = new BN(0); - let commonEmitterSequence = new BN(2); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Known Issues", async () => { - it("Cannot Invoke `post_message_unreliable` Without Clock Sysvar", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.alloc(803); - const finalityRepr = 0; - - // We must pay the fee collector prior publishing a message. - const preInstructions = await Promise.all([ - coreBridge.transferMessageFeeIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - payer - ), - ]); - - await expectLegacyPostMessageUnreliableErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - "Transaction too large: 1233 > 1232", - preInstructions - ); - }); - }); - - describe("Ok", async () => { - it("Invoke `post_message_unreliable` with Common Message Signer", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = COMMON_UNRELIABLE_MESSAGE_SIGNER; - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const finalityRepr = 0; - const payload = Buffer.alloc(64); - payload.write("All your base are belong to us.", 0); - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - - // Send again but with different payload and different finality. - const newFinalityRepr = 1; - payload.fill(0); - payload.write("Just kidding.", 0); - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr: newFinalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Cannot Invoke `legacy_post_message_unreliable` using Different Length Payload", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = COMMON_UNRELIABLE_MESSAGE_SIGNER; - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("Womp womp."); - const finalityRepr = 0; - - // We must pay the fee collector prior publishing a message. - const preInstructions = await Promise.all([ - coreBridge.transferMessageFeeIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - payer - ), - ]); - - await expectLegacyPostMessageUnreliableErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - "Error: Custom(18)", // InvalidPayloadLength - preInstructions - ); - }); - - it("Invoke `legacy_post_message_unreliable` using Old Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = COMMON_UNRELIABLE_MESSAGE_SIGNER; - - // Accounts. - const message = messageSigner.publicKey; - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - message, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Fetch existing message. - const [existingFinalityRepr, existingNonce, existingPayload] = - await coreBridge.PostedMessageV1Unreliable.fromAccountAddress( - connection, - message - ).then((msg): [number, number, Buffer] => [ - msg.finality, - msg.nonce, - msg.payload, - ]); - - const nonce = 69; - expect(nonce).not.equals(existingNonce); - - const finalityRepr = 0; - expect(finalityRepr).not.equals(existingFinalityRepr); - - const payload = Buffer.alloc(existingPayload.length); - payload.fill(0); - payload.write("So fresh and so clean clean."); - expect(payload.equals(existingPayload)).is.false; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message_unreliable` with New Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 69; - const payload = Buffer.from("Somebody set up us the bomb."); - const finalityRepr = 1; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message_unreliable` with Payer as Emitter", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: true, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("I'm the captain now."); - const finalityRepr = 1; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - payerSequence - ); - payerSequence.iaddn(1); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/100__newImplementation.ts b/solana/old-tests/02__coreBridge/100__newImplementation.ts deleted file mode 100644 index dc71eeab14..0000000000 --- a/solana/old-tests/02__coreBridge/100__newImplementation.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { createPostSignedVaaTransactions } from "@certusone/wormhole-sdk/lib/cjs/solana/sendAndConfirmPostVaa"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - artifactsPath, - expectIxOk, - loadProgramBpf, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 3; - -describe("Core Bridge: New Implementation", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - 2_100_000 - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Load New Implementation", async () => { - const artifactPath = `${artifactsPath()}/solana_wormhole_core_bridge.so`; - const bufferAuthority = coreBridge.upgradeAuthority(CORE_BRIDGE_PROGRAM_ID); - const implementation = loadProgramBpf(payerSigner, artifactPath, bufferAuthority); - - localVariables.set("implementation", implementation); - }); - - it("Invoke `upgrade_contract`", async () => { - const newContract: web3.PublicKey = localVariables.get("implementation")!; - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeUpgradeContract( - timestamp, - chain, - newContract.toString() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // Upgrade. - await expectIxOk( - connection, - [coreBridgeSDK.createUpgradeContractInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner] - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/101__legacyPostMessage.ts b/solana/old-tests/02__coreBridge/101__legacyPostMessage.ts deleted file mode 100644 index dbd1334e40..0000000000 --- a/solana/old-tests/02__coreBridge/101__legacyPostMessage.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - COMMON_EMITTER, - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - airdrop, - expectLegacyPostMessageErr, - expectLegacyPostMessageOk, -} from "../helpers"; - -describe("Core Bridge: Legacy Post Message", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - let payerSequence = new BN(0); - let commonEmitterSequence = new BN(6); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `legacy_post_message` With Small Payload", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("All your base are belong to us."); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message` Again With Same Emitter", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 69; - const payload = Buffer.from("Somebody set up us the bomb."); - const finalityRepr = 1; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message` With Payer as Emitter", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("I'm the captain now."); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - payerSequence - ); - payerSequence.iaddn(1); - }); - - it("Cannot Invoke `legacy_post_message` Without Fee Collector If Fee Exists", async () => { - const emitterSigner = web3.Keypair.generate(); - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: false } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - expect(accounts.feeCollector).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("All your base are belong to us."); - const finalityRepr = 0; - - await expectLegacyPostMessageErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - "AccountNotEnoughKeys" - ); - }); - - it("Cannot Invoke `legacy_post_message` Without Transferring Fee If Fee Exists", async () => { - const emitterSigner = web3.Keypair.generate(); - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("All your base are belong to us."); - const finalityRepr = 0; - - await expectLegacyPostMessageErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - "InsufficientMessageFee" - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/102__legacyPostVaa.ts b/solana/old-tests/02__coreBridge/102__legacyPostVaa.ts deleted file mode 100644 index 8f588bc59f..0000000000 --- a/solana/old-tests/02__coreBridge/102__legacyPostVaa.ts +++ /dev/null @@ -1,284 +0,0 @@ -import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; -import { ethers } from "ethers"; - -const GUARDIAN_SET_INDEX = 3; -const EMITTER_SEQUENCE = 2_102_000; - -// Only a subset of those that are invalidated. -const INVALID_SIGNATURE_SETS = [ - "18eK1799CaNMGCUnnCt1Kq2uwKkax6T2WmtrDsZuVFQ", - "2g6NCUUPaD6AxdHPQMVLpjpAvBfKMek6dDiGUe2A6T33", - "3hYV5968hNzbqUfcvnQ6v9D5h32hEwGJn19c47N3unNj", - "76eEyhaEKs4mesjiQiu8bghvwDHNxJW3EfcpbNC78y1z", - /* ... */ - "E8qKJMwzBCiHCHUmBEcL631kN5CjfsHNx24osFLfHg69", - "EtMw1nQ4AQaH53RjYz3pRk12rrqWjcYjPDETphYJzmCX", - "EVNwqfgkUnJoMqBqiHgDfa3TLZPQocX1hpcbAXbpcSLv", - "FixSiDfTxvoy5Zgjp5KdFU8U23ChwCxPWY3WTkmMW2fU", -]; - -const SIGNATURE_SET_NO_SIGNATURES: [string, number] = [ - "5Ng9FbCGL2teGdZHVU2xmJiCzofwudeey9EGWCaY4hAT", - 2_102_002, -]; - -const VALID_SIGNATURE_SETS: [string, number][] = [ - ["8uishP1LzwrNY1EUGwN9hDDyCJNq9KYYiuLdEXR7z8wp", 2_102_003], - ["FavNDP9raM38ut8GpSwPZPfaQhsHf9ssR3iuwVJq7uDY", 2_102_004], - ["9FUxeAD7bGDxKtv6oQmaWpEFAFVigRKr9PMGupoVb7vP", 2_102_005], - ["Cy4QhUDcWfHiXKwNyR9Ua4NmoQ1WkdZsBdt9vNHyvD6K", 2_102_006], - ["QhjQmkJsJNJPuk2dBdRuLsiAyUy2sVDoqRfgHheSPtd", 2_102_007], - ["HeYQadUm66isrG9mggNV4snUwMRN1MC2y7yLXCs4c4vo", 2_102_008], - ["Ap4LoByWRoyQew1VmThFB97BqxqVqaTyYmL5YAzk8YvU", 2_102_009], - ["5zH7Qq9bdrcJDsJLjZJ63s5BjnftTvHuqe8c3eErHUrW", 2_102_010], - ["DCiw4352zkToCNNBap5rz9wruLYoWbLCXMcZp2rTuF4P", 2_102_011], -]; - -const HURRDURR: [string, number][] = [ - ["5Ng9FbCGL2teGdZHVU2xmJiCzofwudeey9EGWCaY4hAT", 0], - ["8uishP1LzwrNY1EUGwN9hDDyCJNq9KYYiuLdEXR7z8wp", EMITTER_SEQUENCE + 2], - ["FavNDP9raM38ut8GpSwPZPfaQhsHf9ssR3iuwVJq7uDY", EMITTER_SEQUENCE + 3], - ["9FUxeAD7bGDxKtv6oQmaWpEFAFVigRKr9PMGupoVb7vP", EMITTER_SEQUENCE + 4], - ["Cy4QhUDcWfHiXKwNyR9Ua4NmoQ1WkdZsBdt9vNHyvD6K", EMITTER_SEQUENCE + 5], - ["QhjQmkJsJNJPuk2dBdRuLsiAyUy2sVDoqRfgHheSPtd", EMITTER_SEQUENCE + 6], - ["HeYQadUm66isrG9mggNV4snUwMRN1MC2y7yLXCs4c4vo", EMITTER_SEQUENCE + 7], - ["Ap4LoByWRoyQew1VmThFB97BqxqVqaTyYmL5YAzk8YvU", EMITTER_SEQUENCE + 8], - ["5zH7Qq9bdrcJDsJLjZJ63s5BjnftTvHuqe8c3eErHUrW", EMITTER_SEQUENCE + 9], - ["DCiw4352zkToCNNBap5rz9wruLYoWbLCXMcZp2rTuF4P", EMITTER_SEQUENCE + 10], -]; - -const EXPECTED_TIMESTAMP = 23456789; -const EXPECTED_NONCE = 12345; -const EXPECTED_PAYLOAD = Buffer.from("Are you looking at me?"); -const EXPECTED_CONSISTENCY_LEVEL = 200; - -describe("Core Bridge: Legacy Verify Signatures and Post VAA", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const foreignEmitter = new MockEmitter( - "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", - 2, - EMITTER_SEQUENCE - ); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `legacy_verify_signatures` and `legacy_post_vaa`", async () => { - const timestamp = 12345678; - const nonce = 420; - const payload = Buffer.from("Someone set us up the bomb."); - const consistencyLevel = 1; - const published = foreignEmitter.publishMessage(nonce, payload, consistencyLevel, timestamp); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // TODO: Check Posted VAA account. - }); - - it("Invoke `legacy_post_vaa` Without Unnecessary Accounts and Arguments", async () => { - const [signatureSetKey, expectedSequence] = VALID_SIGNATURE_SETS[0]; - const signatureSet = new web3.PublicKey(signatureSetKey); - - const { sigVerifySuccesses, messageHash, guardianSetIndex } = - await coreBridge.SignatureSet.fromAccountAddress(connection, signatureSet); - - // Verify that this signature set reached quorum. - const guardianSetData = await coreBridge.GuardianSet.fromPda( - connection, - CORE_BRIDGE_PROGRAM_ID, - guardianSetIndex - ); - const numVerified = sigVerifySuccesses - .map((v) => Number(v)) - .reduce((prev, curr) => prev + curr); - expect(numVerified).is.greaterThanOrEqual((2 * guardianSetData.keys.length) / 3); - - const accounts = coreBridge.LegacyPostVaaContext.new( - CORE_BRIDGE_PROGRAM_ID, - guardianSetIndex, - signatureSet, - messageHash, - payer, - { bridge: false, clock: false, rent: false } - ); - - expect(accounts._bridge).is.null; - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - const args = { - version: undefined, - guardianSetIndex: undefined, - timestamp: EXPECTED_TIMESTAMP, - nonce: EXPECTED_NONCE, - emitterChain: foreignEmitter.chain, - emitterAddress: Array.from(foreignEmitter.address), - sequence: new BN(expectedSequence), - finality: EXPECTED_CONSISTENCY_LEVEL, - payload: EXPECTED_PAYLOAD, - }; - - const ix = coreBridge.legacyPostVaaIx(CORE_BRIDGE_PROGRAM_ID, accounts, args); - - await expectIxOk(connection, [ix], [payerSigner]); - - // const signers = [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14]; - // let i = 0; - // for (const [signatureSet, _] of HURRDURR) { - // ++i; - // const published = foreignEmitter.publishMessage( - // EXPECTED_NONCE, - // EXPECTED_PAYLOAD, - // EXPECTED_CONSISTENCY_LEVEL, - // EXPECTED_TIMESTAMP - // ); - // console.log("published", signatureSet, published.toString("hex")); - // const messageHash = Buffer.from( - // ethers.utils.arrayify(ethers.utils.keccak256(published)) - // ); - // console.log("messageHash?", messageHash.toString("hex")); - // const accountData = await coreBridge.SignatureSet.fromAccountAddress( - // connection, - // new web3.PublicKey(signatureSet) - // ); - // console.log("accountData?", accountData); - // const accountData = Buffer.alloc(4 + 19 + 32 + 4); - // accountData.writeUInt32LE(19, 0); - // if (i > 1) { - // for (let i = 0; i < 19; ++i) { - // if (signers.includes(i)) { - // accountData.writeUInt8(1, 4 + i); - // } - // } - // } - // accountData.write(messageHash.toString("hex"), 4 + 19, "hex"); - // accountData.writeUInt32LE(GUARDIAN_SET_INDEX, 4 + 19 + 32); - // console.log( - // "and....?", - // accountData.subarray(23, 23 + 32).toString("hex") - // ); - // const allData = { - // pubkey: signatureSet, - // account: { - // lamports: 1301520, - // data: [accountData.toString("base64"), "base64"], - // owner: CORE_BRIDGE_PROGRAM_ID, - // executable: false, - // rentEpoch: 0, - // }, - // }; - // const fs = require("fs"); - // fs.writeFileSync( - // `${__dirname}/../test-accounts/valid_signature_set_${i - // .toString() - // .padStart(2, "0")}.json`, - // JSON.stringify(allData, null, 2) - // ); - //} - }); - - it("Cannot Invoke `legacy_post_vaa` With Invalid Signature Set Pubkeys", async () => { - // TODO - INVALID_SIGNATURE_SETS; - }); - - it("Cannot Invoke `legacy_post_vaa` Without Quorum", async () => { - const [signatureSetKey, expectedSequence] = SIGNATURE_SET_NO_SIGNATURES; - const signatureSet = new web3.PublicKey(signatureSetKey); - - const { sigVerifySuccesses, messageHash, guardianSetIndex } = - await coreBridge.SignatureSet.fromAccountAddress(connection, signatureSet); - - // Verify that this signature set reached quorum. - const guardianSetData = await coreBridge.GuardianSet.fromPda( - connection, - CORE_BRIDGE_PROGRAM_ID, - guardianSetIndex - ); - const numVerified = sigVerifySuccesses - .map((v) => Number(v)) - .reduce((prev, curr) => prev + curr); - expect(numVerified).is.lessThan((2 * guardianSetData.keys.length) / 3); - - const args = { - timestamp: EXPECTED_TIMESTAMP, - nonce: EXPECTED_NONCE, - emitterChain: foreignEmitter.chain, - emitterAddress: Array.from(foreignEmitter.address), - sequence: new BN(expectedSequence), - finality: EXPECTED_CONSISTENCY_LEVEL, - payload: EXPECTED_PAYLOAD, - }; - - const ix = coreBridge.LegacyPostVaaContext.instruction( - CORE_BRIDGE_PROGRAM_ID, - guardianSetIndex, - signatureSet, - messageHash, - payer, - args - ); - - await expectIxErr(connection, [ix], [payerSigner], "NoQuorum"); - }); - - it("Cannot Invoke `legacy_post_vaa` With Incorrect Body Data", async () => { - const [signatureSetKey, expectedSequence] = VALID_SIGNATURE_SETS[1]; - const signatureSet = new web3.PublicKey(signatureSetKey); - - const { messageHash, guardianSetIndex } = await coreBridge.SignatureSet.fromAccountAddress( - connection, - signatureSet - ); - - const incorrectSequence = expectedSequence + 1; - const args = { - timestamp: EXPECTED_TIMESTAMP, - nonce: EXPECTED_NONCE, - emitterChain: foreignEmitter.chain, - emitterAddress: Array.from(foreignEmitter.address), - sequence: new BN(incorrectSequence), - finality: EXPECTED_CONSISTENCY_LEVEL, - payload: EXPECTED_PAYLOAD, - }; - - const ix = coreBridge.LegacyPostVaaContext.instruction( - CORE_BRIDGE_PROGRAM_ID, - guardianSetIndex, - signatureSet, - messageHash, - payer, - args - ); - - await expectIxErr(connection, [ix], [payerSigner], "InvalidMessageHash"); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/103__legacySetMessageFee.ts b/solana/old-tests/02__coreBridge/103__legacySetMessageFee.ts deleted file mode 100644 index 7a9ef5419b..0000000000 --- a/solana/old-tests/02__coreBridge/103__legacySetMessageFee.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectLegacyPostMessageOk, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { coreBridge } from "wormhole-solana-sdk"; -import { expect } from "chai"; - -const GUARDIAN_SET_INDEX = 3; -const GOVERNANCE_SEQUENCE = 2_103_000; - -describe("Core Bridge: Legacy Set Message Fee (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `set_message_fee` To Set Fee == 0", async () => { - const amount = new BN(0); - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeSetMessageFee( - timestamp, - chain, - BigInt(amount.toString()) - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // Set message fee. - await expectIxOk( - connection, - [coreBridgeSDK.createSetFeesInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner] - ); - - // TODO: Check bridge program data to see if message fee was set correctly. - - localVariables.set("signedVaa", signedVaa); - }); - - it("Cannot Invoke `set_message_fee` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - - await expectIxErr( - connection, - [coreBridgeSDK.createSetFeesInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner], - "already in use" - ); - }); - - it("Invoke `legacy_post_message` Without Requiring Fee Collector Pubkey", async () => { - const emitterSigner = web3.Keypair.generate(); - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: false } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - expect(accounts.feeCollector).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("All your base are belong to us."); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - new BN(0) - ); - }); - - it("Invoke `set_message_fee` To Set Fee == 42069", async () => { - const amount = new BN(42069); - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeSetMessageFee( - timestamp, - chain, - BigInt(amount.toString()) - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - // Set message fee. - await expectIxOk( - connection, - [coreBridgeSDK.createSetFeesInstruction(CORE_BRIDGE_PROGRAM_ID, payer, signedVaa)], - [payerSigner] - ); - - // TODO: Check bridge program data to see if message fee was set correctly. - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/104__legacyTransferFees.ts b/solana/old-tests/02__coreBridge/104__legacyTransferFees.ts deleted file mode 100644 index 6e16746e9e..0000000000 --- a/solana/old-tests/02__coreBridge/104__legacyTransferFees.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 3; -const GOVERNANCE_SEQUENCE = 2_104_000; - -describe("Core Bridge: Legacy Transfer Fees (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `transfer_fees`", async () => { - const recipient = web3.Keypair.generate().publicKey; - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(0); - } - - // First send lamports over to the fee collector. - const amount = 6942069; - const transferIx = web3.SystemProgram.transfer({ - fromPubkey: payer, - toPubkey: coreBridge.FeeCollector.address(CORE_BRIDGE_PROGRAM_ID), - lamports: amount, - }); - await expectIxOk(connection, [transferIx], [payerSigner]); - - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeTransferFees( - timestamp, - chain, - BigInt(amount.toString()), - recipient.toBuffer() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedVaa - ), - ], - [payerSigner] - ); - - { - const balance = await connection.getBalance(recipient); - expect(balance).equals(amount); - } - - // Save for later. - localVariables.set("signedVaa", signedVaa); - localVariables.set("recipient", recipient); - }); - - it("Cannot Invoke `transfer_fees` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - const recipient: Buffer = localVariables.get("recipient")!; - - await expectIxErr( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedVaa - ), - ], - [payerSigner], - "already in use" - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/106__legacyGuardianSetUpdate.ts b/solana/old-tests/02__coreBridge/106__legacyGuardianSetUpdate.ts deleted file mode 100644 index efe2d7ccfe..0000000000 --- a/solana/old-tests/02__coreBridge/106__legacyGuardianSetUpdate.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as coreBridgeSDK from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GOVERNANCE_EMITTER_ADDRESS, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, - invokeVerifySignaturesAndPostVaa, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 3; -const GOVERNANCE_SEQUENCE = 2_106_000; - -describe("Core Bridge: Legacy Guardian Set Update (Governance)", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const governance = new GovernanceEmitter( - GOVERNANCE_EMITTER_ADDRESS.toBuffer().toString("hex"), - GOVERNANCE_SEQUENCE - ); - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Cannot Invoke Core Bridge Governance Without Latest Guardian Set", async () => { - const recipient = web3.Keypair.generate().publicKey; - const amount = 42069420; - - // Post transfer fees VAA, which will be signed with a soon-to-be-expired Guardian set. - const signedTransferFeesVaa = await (async () => { - const timestamp = 12345678; - const chain = 1; - const published = governance.publishWormholeTransferFees( - timestamp, - chain, - BigInt(amount.toString()), - recipient.toBuffer() - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - return signedVaa; - })(); - - // Update guardian set. - { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys().slice(0, 1); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] - ); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - } - - // Now use previously posted VAA to transfer fees. - { - // First send lamports over to the fee collector. - const transferIx = web3.SystemProgram.transfer({ - fromPubkey: payer, - toPubkey: coreBridge.FeeCollector.address(CORE_BRIDGE_PROGRAM_ID), - lamports: amount, - }); - await expectIxOk(connection, [transferIx], [payerSigner]); - - await expectIxErr( - connection, - [ - coreBridgeSDK.createTransferFeesInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - recipient, - signedTransferFeesVaa - ), - ], - [payerSigner], - "LatestGuardianSetRequired" - ); - } - }); - - it("Invoke `guardian_set_update`", async () => { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys().slice(0, 2); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures(published, [0]); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - - // TODO: Verify guardian set. - - // Save for later. - localVariables.set("signedVaa", signedVaa); - }); - - it("Cannot Invoke `guardian_set_update` with Same VAA", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - - await expectIxErr( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner], - "already in use" - ); - }); - - it("Invoke `guardian_set_update` Again to Original Guardian Keys", async () => { - const newGuardianSetIndex = guardians.setIndex + 1; - const newGuardianKeys = guardians.getPublicKeys(); - - const timestamp = 12345678; - const published = governance.publishWormholeGuardianSetUpgrade( - timestamp, - newGuardianSetIndex, - newGuardianKeys - ); - const signedVaa = guardians.addSignatures(published, [0, 1]); - - // Verify and Post - await invokeVerifySignaturesAndPostVaa(connection, payerSigner, signedVaa); - - await expectIxOk( - connection, - [ - coreBridgeSDK.createUpgradeGuardianSetInstruction( - CORE_BRIDGE_PROGRAM_ID, - payer, - signedVaa - ), - ], - [payerSigner] - ); - - // Update mock guardians. - guardians.updateGuardianSetIndex(newGuardianSetIndex); - - // TODO: Verify guardian set. - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/108__legacyPostMessageUnreliable.ts b/solana/old-tests/02__coreBridge/108__legacyPostMessageUnreliable.ts deleted file mode 100644 index e81db560f2..0000000000 --- a/solana/old-tests/02__coreBridge/108__legacyPostMessageUnreliable.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - COMMON_EMITTER, - COMMON_UNRELIABLE_MESSAGE_SIGNER, - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - airdrop, - expectLegacyPostMessageUnreliableErr, - expectLegacyPostMessageUnreliableOk, -} from "../helpers"; - -describe("Core Bridge: Legacy Post Message Unreliable", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - let payerSequence = new BN(0); - let commonEmitterSequence = new BN(8); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Cannot Invoke `legacy_post_message_unreliable` using Different Length Payload", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = COMMON_UNRELIABLE_MESSAGE_SIGNER; - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("Womp womp."); - const finalityRepr = 0; - - // We must pay the fee collector prior publishing a message. - const preInstructions = await Promise.all([ - coreBridge.transferMessageFeeIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - payer - ), - ]); - - await expectLegacyPostMessageUnreliableErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - "ConstraintSpace", - preInstructions - ); - }); - - it("Invoke `legacy_post_message_unreliable` using Old Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = COMMON_UNRELIABLE_MESSAGE_SIGNER; - - // Accounts. - const message = messageSigner.publicKey; - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - message, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Fetch existing message. - const [existingFinalityRepr, existingNonce, existingPayload] = - await coreBridge.PostedMessageV1Unreliable.fromAccountAddress( - connection, - message - ).then((msg): [number, number, Buffer] => [ - msg.finality, - msg.nonce, - msg.payload, - ]); - - const nonce = 6969; - expect(nonce).not.equals(existingNonce); - - const finalityRepr = 0; - expect(finalityRepr).not.equals(existingFinalityRepr); - - const payload = Buffer.alloc(existingPayload.length); - payload.fill(0); - payload.write("So fresh and so clean clean. AGAIN?"); - expect(payload.equals(existingPayload)).is.false; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message_unreliable` with New Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 69; - const payload = Buffer.from("Somebody set up us the bomb."); - const finalityRepr = 1; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence - ); - commonEmitterSequence.iaddn(1); - }); - - it("Invoke `legacy_post_message_unreliable` with Payer as Emitter", async () => { - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.LegacyPostMessageUnreliableContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - payer, // emitter - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.from("I'm the captain now."); - const finalityRepr = 1; - - await expectLegacyPostMessageUnreliableOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, messageSigner], - payerSequence - ); - payerSequence.iaddn(1); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/201__PostMessage.ts b/solana/old-tests/02__coreBridge/201__PostMessage.ts deleted file mode 100644 index 94579afc6b..0000000000 --- a/solana/old-tests/02__coreBridge/201__PostMessage.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { BN, web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - COMMON_EMITTER, - CORE_BRIDGE_PROGRAM_ID, - LOCALHOST, - airdrop, - expectLegacyPostMessageErr, - expectLegacyPostMessageOk, - expectIxTransactionDetails, - expectIxErr, - expectIxOk, -} from "../helpers"; -import { Transaction } from "@solana/web3.js"; - -describe("Core Bridge: New Post Message Features", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - let payerSequence = new BN(0); - let commonEmitterSequence = new BN(10); - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `init_message_v1` with Large Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.InitMessageV1Context.new( - emitterSigner.publicKey, - messageSigner.publicKey - ); - - // Data. - const expectedMsgLength = 30 * 1024; - const args = { cpiProgramId: null }; - - const dataLength = 95 + expectedMsgLength; - const createIx = await connection - .getMinimumBalanceForRentExemption(dataLength) - .then((lamports) => - web3.SystemProgram.createAccount({ - fromPubkey: payer, - newAccountPubkey: messageSigner.publicKey, - space: dataLength, - lamports, - programId: new web3.PublicKey(CORE_BRIDGE_PROGRAM_ID), - }) - ); - const initIx = await coreBridge.initMessageV1Ix( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - args - ); - - await expectIxOk( - connection, - [createIx, initIx], - [payerSigner, emitterSigner, messageSigner] - ); - - const { emitterAuthority: emitter, draftMessage } = accounts; - - const messageData = await coreBridge.PostedMessageV1.fromAccountAddress( - connection, - draftMessage - ); - expect(messageData.finality).equals(0); - expect(messageData.emitterAuthority.equals(emitter)).is.true; - expect(messageData.status).equals(coreBridge.MessageStatus.Writing); - expect(messageData._gap0.equals(Buffer.alloc(3))).is.true; - expect(messageData.postedTimestamp).equals(0); - expect(messageData.nonce).equals(0); - expect(messageData.sequence.eqn(0)).is.true; - expect(messageData.solanaChainId).equals(1); - expect(messageData.emitter.equals(emitter)).is.true; - expect(messageData.payload).has.length(expectedMsgLength); - - // Save for later. - localVariables.set("messageSigner", messageSigner); - localVariables.set("draftMessage", draftMessage); - localVariables.set("expectedMsgLength", expectedMsgLength); - }); - - it("Invoke `process_message_v1` With Large Message", async () => { - const emitterSigner = COMMON_EMITTER; - const draftMessage: web3.PublicKey = localVariables.get("draftMessage")!; - const expectedMsgLength: number = - localVariables.get("expectedMsgLength")!; - - const repeatedMessage = "All your base are belong to us. "; - const messagePayload = Buffer.alloc(expectedMsgLength, repeatedMessage); - let messageIndex = 0; - - const accounts = coreBridge.ProcessMessageV1Context.new( - emitterSigner.publicKey, - draftMessage, - null - ); - - // Break up into chunks. Max chunk size is 914 (due to transaction size). - const maxChunkSize = 914; - while (messageIndex < messagePayload.length) { - const dataLength = Math.min( - messagePayload.length - messageIndex, - maxChunkSize - ); - const data = messagePayload.subarray( - messageIndex, - messageIndex + dataLength - ); - - const ix = await coreBridge.processMessageV1Ix( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - { write: { index: messageIndex, data } } - ); - - await expectIxOk(connection, [ix], [payerSigner, emitterSigner]); - - messageIndex += dataLength; - } - - const messageData = await coreBridge.PostedMessageV1.fromAccountAddress( - connection, - draftMessage - ); - expect(messageData.status).equals(coreBridge.MessageStatus.Writing); - expect(messageData.payload.equals(messagePayload)).is.true; - - // Save for later. - localVariables.set("messagePayload", messagePayload); - }); - - it("Invoke `legacy_post_message` With Processed Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner: web3.Keypair = localVariables.get("messageSigner")!; - const messagePayload: Buffer = localVariables.get("messagePayload")!; - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.alloc(0); - const finalityRepr = 0; - - await expectLegacyPostMessageOk( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - commonEmitterSequence, - { actualPayload: messagePayload } - ); - commonEmitterSequence.iaddn(1); - }); - - it("Cannot Invoke `legacy_post_message` With Same Processed Message", async () => { - const emitterSigner = COMMON_EMITTER; - const messageSigner: web3.Keypair = localVariables.get("messageSigner")!; - - // Accounts. - const accounts = coreBridge.LegacyPostMessageContext.new( - CORE_BRIDGE_PROGRAM_ID, - messageSigner.publicKey, - emitterSigner.publicKey, - payer, - { clock: false, rent: false, feeCollector: true } - ); - - // And for the heck of it, show that we do not need these accounts. - expect(accounts._clock).is.null; - expect(accounts._rent).is.null; - - // Data. - const nonce = 420; - const payload = Buffer.alloc(0); - const finalityRepr = 0; - - await expectLegacyPostMessageErr( - connection, - accounts, - { nonce, payload, finalityRepr }, - [payerSigner, emitterSigner, messageSigner], - "RequireKeysEqViolated" - ); - }); - }); -}); diff --git a/solana/old-tests/02__coreBridge/202__ParseAndVerify.ts b/solana/old-tests/02__coreBridge/202__ParseAndVerify.ts deleted file mode 100644 index 244e921b2b..0000000000 --- a/solana/old-tests/02__coreBridge/202__ParseAndVerify.ts +++ /dev/null @@ -1,374 +0,0 @@ -import { - MockEmitter, - MockGuardians, -} from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { web3 } from "@coral-xyz/anchor"; -import { expect } from "chai"; -import { ethers } from "ethers"; -import { coreBridge } from "wormhole-solana-sdk"; -import { - CORE_BRIDGE_PROGRAM_ID, - GUARDIAN_KEYS, - LOCALHOST, - airdrop, - expectIxErr, - expectIxOk, -} from "../helpers"; - -const GUARDIAN_SET_INDEX = 6; - -describe("Core Bridge: New Parse and Verify VAA", () => { - const connection = new web3.Connection(LOCALHOST, "processed"); - - const payerSigner = web3.Keypair.generate(); - const payer = payerSigner.publicKey; - - const foreignEmitter = new MockEmitter( - "000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", - 2, - 2_202_000 - ); - foreignEmitter.sequence = 6900; - - const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); - - const localVariables = new Map(); - - before("Airdrop Payer", async () => { - await airdrop(connection, payer); - }); - - describe("Ok", async () => { - it("Invoke `init_encoded_vaa` with Large VAA Message Payload", async () => { - const encodedVaaSigner = web3.Keypair.generate(); - - // Accounts. - const accounts = coreBridge.InitEncodedVaaContext.new( - payerSigner.publicKey, - encodedVaaSigner.publicKey - ); - - // Data. - const repeatedMessage = "Someone set up us the bomb. "; - - // NOTE: The largest VAA message payload that can be encoded is 15KB - // because to generate the message hash for anything larger requires more - // than 1.4M compute units, which is the current compute limit. - const messagePayload = Buffer.alloc(15 * 1024, repeatedMessage); - - const nonce = 420; - const consistencyLevel = 1; - const timestamp = 12345678; - const published = foreignEmitter.publishMessage( - nonce, - messagePayload, - consistencyLevel, - timestamp - ); - - const signedVaa = guardians.addSignatures( - published, - [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14] - ); - - const dataLength = 46 + signedVaa.length; - const createIx = await connection - .getMinimumBalanceForRentExemption(dataLength) - .then((lamports) => - web3.SystemProgram.createAccount({ - fromPubkey: payer, - newAccountPubkey: encodedVaaSigner.publicKey, - space: dataLength, - lamports, - programId: new web3.PublicKey(CORE_BRIDGE_PROGRAM_ID), - }) - ); - const initIx = await coreBridge.initEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts - ); - - await expectIxOk( - connection, - [createIx, initIx], - [payerSigner, encodedVaaSigner] - ); - - const { writeAuthority, encodedVaa } = accounts; - - const encodedVaaData = await coreBridge.EncodedVaa.fromAccountAddress( - connection, - CORE_BRIDGE_PROGRAM_ID, - encodedVaa - ); - - expect(encodedVaaData.status).equals(1); // Writing - expect(encodedVaaData.writeAuthority.equals(writeAuthority)).is.true; - expect(encodedVaaData.version).equals(0); // Unset - expect(encodedVaaData.bytes.equals(Buffer.alloc(signedVaa.length))).is - .true; - - // Save for later. - localVariables.set("vaaPubkey", encodedVaa); - localVariables.set("messagePayload", messagePayload); - localVariables.set("signedVaa", signedVaa); - }); - - it("Invoke `process_encoded_vaa` To Write VAA", async () => { - const encodedVaa: web3.PublicKey = localVariables.get("vaaPubkey")!; - const signedVaa: Buffer = localVariables.get("signedVaa")!; - - let vaaIndex = 0; - - const accounts = coreBridge.ProcessEncodedVaaContext.new( - payerSigner.publicKey, - encodedVaa, - null - ); - - // Break up into chunks. Max chunk size is 990 (due to transaction size). - const maxChunkSize = 990; - while (vaaIndex < signedVaa.length) { - const dataLength = Math.min(signedVaa.length - vaaIndex, maxChunkSize); - const data = signedVaa.subarray(vaaIndex, vaaIndex + dataLength); - - const ix = await coreBridge.processEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - { write: { index: vaaIndex, data } } - ); - - await expectIxOk(connection, [ix], [payerSigner]); - - vaaIndex += dataLength; - } - - const encodedVaaData = await coreBridge.EncodedVaa.fromAccountAddress( - connection, - CORE_BRIDGE_PROGRAM_ID, - encodedVaa - ); - - expect(encodedVaaData.status).equals(1); // Writing - expect(encodedVaaData.bytes.equals(signedVaa)).is.true; - }); - - it("Invoke `process_encoded_vaa` To Verify Signatures", async () => { - const encodedVaa: web3.PublicKey = localVariables.get("vaaPubkey")!; - - const guardianSet = coreBridge.GuardianSet.address( - CORE_BRIDGE_PROGRAM_ID, - GUARDIAN_SET_INDEX - ); - const accounts = coreBridge.ProcessEncodedVaaContext.new( - payerSigner.publicKey, - encodedVaa, - guardianSet - ); - - // Current max compute unit limit is 1.4M. - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 375000, - }); - - const verifySigsIx = await coreBridge.processEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - { verifySignaturesV1: {} } - ); - - await expectIxOk(connection, [computeIx, verifySigsIx], [payerSigner]); - - const encodedVaaData = await coreBridge.EncodedVaa.fromAccountAddress( - connection, - CORE_BRIDGE_PROGRAM_ID, - encodedVaa - ); - - expect(encodedVaaData.status).equals(2); // Verified - expect(encodedVaaData.version).equals(1); // V1 - }); - - it("Cannot Invoke `post_vaa_v1` With Message Payload > 10,145 Bytes", async () => { - const signedVaa: Buffer = localVariables.get("signedVaa")!; - const vaa: web3.PublicKey = localVariables.get("vaaPubkey")!; - - const messageHash = Array.from( - ethers.utils.arrayify( - ethers.utils.keccak256(signedVaa.subarray(signedVaa[5] * 66 + 6)) - ) - ); - - const accounts = coreBridge.PostVaaV1Context.new( - payerSigner.publicKey, - vaa, - coreBridge.PostedVaaV1.address(CORE_BRIDGE_PROGRAM_ID, messageHash) - ); - - const ix = await coreBridge.postVaaV1Ix( - connection, - CORE_BRIDGE_PROGRAM_ID, - accounts, - { tryOnce: {} } - ); - - await expectIxErr( - connection, - [ix], - [payerSigner], - "Account data size realloc limited to 10240 in inner instructions" - ); - }); - - it("Invoke `post_vaa_v1` With Max Message Payload Size 10,145 Bytes", async () => { - const encodedVaaSigner = web3.Keypair.generate(); - - // Accounts. - const initAccounts = coreBridge.InitEncodedVaaContext.new( - payerSigner.publicKey, - encodedVaaSigner.publicKey - ); - - const { encodedVaa: vaa } = initAccounts; - - // Data. - const repeatedMessage = "Goldilocks. "; - - // NOTE: The largest VAA message payload that can be encoded is 15KB - // because to generate the message hash for anything larger requires more - // than 1.4M compute units, which is the current compute limit. - const messagePayload = Buffer.alloc(10145, repeatedMessage); - - const nonce = 420; - const consistencyLevel = 1; - const timestamp = 12345678; - const published = foreignEmitter.publishMessage( - nonce, - messagePayload, - consistencyLevel, - timestamp - ); - - const signedVaa = guardians.addSignatures( - published, - [0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 15, 16] - ); - - const dataLength = 46 + signedVaa.length; - const createIx = await connection - .getMinimumBalanceForRentExemption(dataLength) - .then((lamports) => - web3.SystemProgram.createAccount({ - fromPubkey: payer, - newAccountPubkey: encodedVaaSigner.publicKey, - space: dataLength, - lamports, - programId: new web3.PublicKey(CORE_BRIDGE_PROGRAM_ID), - }) - ); - const initIx = await coreBridge.initEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - initAccounts - ); - - let remainingNumBytes = signedVaa.length; - let vaaIndex = 0; - - const writeAccounts = coreBridge.ProcessEncodedVaaContext.new( - payerSigner.publicKey, - vaa, - null - ); - - // Because we are putting the first write instruction with the init - // instructions, we cannot write as many bytes due to other data filling - // the transaction (signature, create instruction data, etc). - const firstChunkSize = Math.min(844, remainingNumBytes); - const firstChunk = signedVaa.subarray(0, firstChunkSize); - - const firstProcessIx = await coreBridge.processEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - writeAccounts, - { write: { index: 0, data: firstChunk } } - ); - vaaIndex += firstChunkSize; - remainingNumBytes -= firstChunkSize; - - await expectIxOk( - connection, - [createIx, initIx, firstProcessIx], - [payerSigner, encodedVaaSigner] - ); - - // Break up into chunks. Max chunk size is 990 (due to transaction size). - const maxChunkSize = 990; - while (remainingNumBytes > 0) { - const dataLength = Math.min(remainingNumBytes, maxChunkSize); - const data = signedVaa.subarray(vaaIndex, vaaIndex + dataLength); - - const ix = await coreBridge.processEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - writeAccounts, - { write: { index: vaaIndex, data } } - ); - - vaaIndex += dataLength; - remainingNumBytes -= dataLength; - - await expectIxOk(connection, [ix], [payerSigner]); - } - - const guardianSet = coreBridge.GuardianSet.address( - CORE_BRIDGE_PROGRAM_ID, - GUARDIAN_SET_INDEX - ); - const verifyAccounts = coreBridge.ProcessEncodedVaaContext.new( - payerSigner.publicKey, - vaa, - guardianSet - ); - - const computeIx = web3.ComputeBudgetProgram.setComputeUnitLimit({ - units: 400000, - }); - - const verifySigsIx = await coreBridge.processEncodedVaaIx( - connection, - CORE_BRIDGE_PROGRAM_ID, - verifyAccounts, - { verifySignaturesV1: {} } - ); - - const messageHash = Array.from( - ethers.utils.arrayify( - ethers.utils.keccak256(signedVaa.subarray(signedVaa[5] * 66 + 6)) - ) - ); - - const postAccounts = coreBridge.PostVaaV1Context.new( - payerSigner.publicKey, - vaa, - coreBridge.PostedVaaV1.address(CORE_BRIDGE_PROGRAM_ID, messageHash) - ); - - const postIx = await coreBridge.postVaaV1Ix( - connection, - CORE_BRIDGE_PROGRAM_ID, - postAccounts, - { tryOnce: {} } - ); - - await expectIxOk( - connection, - [computeIx, verifySigsIx, postIx], - [payerSigner] - ); - }); - }); -}); diff --git a/solana/old-tests/helpers/consts.ts b/solana/old-tests/helpers/consts.ts deleted file mode 100644 index 5e275799dc..0000000000 --- a/solana/old-tests/helpers/consts.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Keypair, PublicKey } from "@solana/web3.js"; - -export const TEST_ROOT = `${__dirname}/..`; - -export const LOCALHOST = "http://localhost:8899"; - -export const GUARDIAN_KEYS = [ - "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", - "c3b2e45c422a1602333a64078aeb42637370b0f48fe385f9cfa6ad54a8e0c47e", - "9f790d3f08bc4b5cd910d4278f3deb406e57bb5e924906ccd52052bb078ccd47", - "b20cc49d6f2c82a5e6519015fc18aa3e562867f85f872c58f1277cfbd2a0c8e4", - "eded5a2fdcb5bbbfa5b07f2a91393813420e7ac30a72fc935b6df36f8294b855", - "00d39587c3556f289677a837c7f3c0817cb7541ce6e38a243a4bdc761d534c5e", - "da534d61a8da77b232f3a2cee55c0125e2b3e33a5cd8247f3fe9e72379445c3b", - "cdbabfc2118eb00bc62c88845f3bbd03cb67a9e18a055101588ca9b36387006c", - "c83d36423820e7350428dc4abe645cb2904459b7d7128adefe16472fdac397ba", - "1cbf4e1388b81c9020500fefc83a7a81f707091bb899074db1bfce4537428112", - "17646a6ba14a541957fc7112cc973c0b3f04fce59484a92c09bb45a0b57eb740", - "eb94ff04accbfc8195d44b45e7c7da4c6993b2fbbfc4ef166a7675a905df9891", - "053a6527124b309d914a47f5257a995e9b0ad17f14659f90ed42af5e6e262b6a", - "3fbf1e46f6da69e62aed5670f279e818889aa7d8f1beb7fd730770fd4f8ea3d7", - "53b05697596ba04067e40be8100c9194cbae59c90e7870997de57337497172e9", - "4e95cb2ff3f7d5e963631ad85c28b1b79cb370f21c67cbdd4c2ffb0bf664aa06", - "01b8c448ce2c1d43cfc5938d3a57086f88e3dc43bb8b08028ecb7a7924f4676f", - "1db31a6ba3bcd54d2e8a64f8a2415064265d291593450c6eb7e9a6a986bd9400", - "70d8f1c9534a0ab61a020366b831a494057a289441c07be67e4288c44bc6cd5d", -]; - -export const GOVERNANCE_CHAIN = 1; -export const GOVERNANCE_EMITTER_ADDRESS = new PublicKey( - "11111111111111111111111111111115" -); - -export const CORE_BRIDGE_PROGRAM_ID = - "agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC"; -export const TOKEN_BRIDGE_PROGRAM_ID = - "bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa"; - -export const COMMON_EMITTER = Keypair.fromSecretKey( - Uint8Array.from([ - 145, 34, 16, 171, 216, 143, 215, 220, 100, 17, 136, 205, 96, 178, 199, 89, - 241, 146, 194, 163, 246, 102, 245, 74, 126, 30, 25, 67, 114, 12, 115, 145, - 180, 118, 0, 230, 97, 203, 112, 115, 55, 184, 243, 155, 159, 3, 113, 180, - 145, 13, 221, 136, 65, 145, 102, 90, 48, 180, 24, 126, 243, 231, 80, 249, - ]) -); - -export const COMMON_UNRELIABLE_MESSAGE_SIGNER = Keypair.fromSecretKey( - Uint8Array.from([ - 115, 98, 163, 202, 20, 161, 74, 124, 209, 179, 18, 229, 62, 184, 182, 119, - 187, 128, 136, 8, 85, 204, 189, 101, 113, 70, 148, 207, 195, 143, 246, 216, - 112, 162, 59, 195, 10, 241, 58, 141, 135, 255, 7, 230, 238, 191, 212, 140, - 172, 76, 12, 44, 96, 229, 221, 4, 177, 185, 86, 100, 238, 143, 129, 93, - ]) -); - -export const COMMON_WRITE_AUTHORITY = Keypair.fromSecretKey( - Uint8Array.from([ - 135, 88, 63, 102, 162, 201, 52, 165, 64, 176, 48, 43, 112, 46, 187, 2, 145, - 232, 133, 217, 197, 253, 168, 75, 41, 155, 80, 222, 141, 90, 22, 84, 229, - 232, 252, 74, 184, 190, 214, 95, 72, 14, 154, 158, 134, 181, 12, 254, 11, - 247, 120, 71, 73, 102, 180, 57, 97, 147, 38, 161, 139, 254, 207, 250, - ]) -); diff --git a/solana/old-tests/helpers/index.ts b/solana/old-tests/helpers/index.ts deleted file mode 100644 index 0859ab4942..0000000000 --- a/solana/old-tests/helpers/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./consts"; -export * from "./misc"; -export * from "./transaction"; -export * from "./vaa"; diff --git a/solana/old-tests/helpers/misc.ts b/solana/old-tests/helpers/misc.ts deleted file mode 100644 index 03946a755c..0000000000 --- a/solana/old-tests/helpers/misc.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as fs from "fs"; -import { - CORE_BRIDGE_PROGRAM_ID, - TEST_ROOT, - TOKEN_BRIDGE_PROGRAM_ID, -} from "./consts"; - -export function tmpPath() { - const tmp = `${TEST_ROOT}/.tmp`; - if (!fs.existsSync(tmp)) { - fs.mkdirSync(tmp); - } - - return tmp; -} - -export function removeTmpPath() { - fs.rmSync(tmpPath(), { force: true, recursive: true }); -} - -export function artifactsPath() { - const artifacts = `${TEST_ROOT}/../target/deploy`; - if (!fs.existsSync(artifacts)) { - throw new Error("Artifacts not found. Run `anchor build` first."); - } - - return artifacts; -} - -export function coreBridgeKeyPath() { - return `${TEST_ROOT}/keys/${CORE_BRIDGE_PROGRAM_ID}.json`; -} - -export function tokenBridgeKeyPath() { - return `${TEST_ROOT}/keys/${TOKEN_BRIDGE_PROGRAM_ID}.json`; -} diff --git a/solana/old-tests/helpers/transaction.ts b/solana/old-tests/helpers/transaction.ts deleted file mode 100644 index 71814ad826..0000000000 --- a/solana/old-tests/helpers/transaction.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - ConfirmOptions, - Connection, - LAMPORTS_PER_SOL, - PublicKey, - Signer, - Transaction, - TransactionInstruction, - sendAndConfirmTransaction, -} from "@solana/web3.js"; -import { expect } from "chai"; -import { Err, Ok } from "ts-results"; - -async function confirmLatest(connection: Connection, signature: string) { - return connection - .getLatestBlockhash() - .then(({ blockhash, lastValidBlockHeight }) => - connection.confirmTransaction( - { - blockhash, - lastValidBlockHeight, - signature, - }, - "confirmed" - ) - ); -} - -export async function expectIxOk( - connection: Connection, - ixs: TransactionInstruction[], - signers: Signer[], - confirmOptions?: ConfirmOptions -) { - return debugSendAndConfirmTransaction( - connection, - new Transaction().add(...ixs), - signers, - { - logError: true, - confirmOptions, - } - ).then((result) => result.unwrap()); -} - -export async function expectIxErr( - connection: Connection, - ixs: TransactionInstruction[], - signers: Signer[], - expectedError: string, - confirmOptions?: ConfirmOptions -) { - const errorMsg = await debugSendAndConfirmTransaction( - connection, - new Transaction().add(...ixs), - signers, - { - logError: false, - confirmOptions, - } - ).then((result) => { - if (result.err) { - return result.toString(); - } else { - throw new Error("Expected transaction to fail"); - } - }); - try { - expect(errorMsg).includes(expectedError); - } catch (err) { - console.log(errorMsg); - throw err; - } -} - -export async function expectIxTransactionDetails( - connection: Connection, - ixs: TransactionInstruction[], - signers: Signer[], - confirmOptions?: ConfirmOptions -) { - const txSig = await expectIxOk(connection, ixs, signers, confirmOptions); - await confirmLatest(connection, txSig); - return connection.getTransaction(txSig, { - commitment: "confirmed", - maxSupportedTransactionVersion: 0, - }); -} - -export async function airdrop(connection: Connection, account: PublicKey) { - const lamports = 69 * LAMPORTS_PER_SOL; - await connection - .requestAirdrop(account, lamports) - .then((sig) => confirmLatest(connection, sig)); - - return lamports; -} - -async function debugSendAndConfirmTransaction( - connection: Connection, - tx: Transaction, - signers: Signer[], - options?: { - logError?: boolean; - confirmOptions?: ConfirmOptions; - } -) { - const logError = options === undefined ? true : options.logError; - const confirmOptions = - options === undefined ? undefined : options.confirmOptions; - - return sendAndConfirmTransaction(connection, tx, signers, confirmOptions) - .then((sig) => new Ok(sig)) - .catch((err) => { - if (logError) { - console.log(err); - } - if (err.logs !== undefined) { - const logs: string[] = err.logs; - return new Err(logs.join("\n")); - } else { - return new Err(err.message); - } - }); -} diff --git a/solana/old-tests/helpers/vaa.ts b/solana/old-tests/helpers/vaa.ts deleted file mode 100644 index 5d6e17b609..0000000000 --- a/solana/old-tests/helpers/vaa.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { postVaaSolana } from "@certusone/wormhole-sdk"; -import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; -import { Connection, Keypair } from "@solana/web3.js"; -import { CORE_BRIDGE_PROGRAM_ID } from "./consts"; - -export async function invokeVerifySignaturesAndPostVaa( - connection: Connection, - payer: Keypair, - signedVaa: Buffer -) { - const wallet = new NodeWallet(payer); - return postVaaSolana( - connection, - wallet.signTransaction, - CORE_BRIDGE_PROGRAM_ID, - wallet.key(), - signedVaa - ); -} diff --git a/solana/old-tests/run_mocha_tests.sh b/solana/old-tests/run_mocha_tests.sh deleted file mode 100755 index 4296dcbfd9..0000000000 --- a/solana/old-tests/run_mocha_tests.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -### maybe a validator is already running -pgrep -f solana-test-validator -if [ $? -eq 0 ]; then - echo "solana-test-validator already running" - exit 1; -fi - -TEST_ROOT=$(dirname $0) -ROOT=$TEST_ROOT/.. - -### prepare local validator -ARTIFACTS=$ROOT/target/deploy -ACCOUNTS=$TEST_ROOT/test-accounts - -MPL_TOKEN_METADATA_PUBKEY=metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -MPL_TOKEN_METADATA_BPF=$TEST_ROOT/mpl_token_metadata.so -if [ ! -f $MPL_TOKEN_METADATA_BPF ]; then - echo "> Fetching MPL Token Metadata program from mainnet-beta" - solana program dump -u m $MPL_TOKEN_METADATA_PUBKEY $MPL_TOKEN_METADATA_BPF -fi - -### Fetch Wormhole programs from main branch -EXISTING_CORE_BRIDGE_BPF=$TEST_ROOT/existing_core_bridge.so -EXISTING_TOKEN_BRIDGE_BPF=$TEST_ROOT/existing_token_bridge.so -EXISTING_NFT_BRIDGE_BPF=$TEST_ROOT/existing_nft_bridge.so - -if [ ! -f $EXISTING_CORE_BRIDGE_BPF ]; then -git clone \ - --depth 1 \ - --branch main \ - --filter=blob:none \ - https://github.com/wormhole-foundation/wormhole \ - wormhole-main > /dev/null 2>&1 - cd wormhole-main/solana - echo "> Building Wormhole Solana bridges" - DOCKER_BUILDKIT=1 docker build \ - -f Dockerfile \ - --build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC \ - -o artifacts-localnet . - cd ../.. - cp wormhole-main/solana/artifacts-localnet/bridge.so $EXISTING_CORE_BRIDGE_BPF - cp wormhole-main/solana/artifacts-localnet/token_bridge.so $EXISTING_TOKEN_BRIDGE_BPF - cp wormhole-main/solana/artifacts-localnet/nft_bridge.so $EXISTING_NFT_BRIDGE_BPF - rm -rf wormhole-main -fi - -TEST=$TEST_ROOT/.test - -VALIDATOR=$(which solana-test-validator) -echo $VALIDATOR -echo $($VALIDATOR --version) - -$VALIDATOR --reset \ - --bpf-program $MPL_TOKEN_METADATA_PUBKEY $MPL_TOKEN_METADATA_BPF \ - --account-dir $ACCOUNTS \ - --ledger $TEST > validator.log 2>&1 & -sleep 5 - -### write program logs -PROGRAM_LOGS=$TEST/program-logs -mkdir -p $PROGRAM_LOGS - -RPC=http://localhost:8899 -solana logs agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC --url $RPC > $PROGRAM_LOGS/agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC & -solana logs bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa --url $RPC > $PROGRAM_LOGS/bPPNmBhmHfkEFJmNKKCvwc1tPqBjzPDRwCw3yQYYXQa & -solana logs caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q --url $RPC > $PROGRAM_LOGS/caosnXgev6ceZQAUFk3hCjYtUwJLoKWwaoqZx9V9s9Q & -solana logs KeccakSecp256k11111111111111111111111111111 --url $RPC > $PROGRAM_LOGS/KeccakSecp256k11111111111111111111111111111 & -solana logs metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s --url $RPC > $PROGRAM_LOGS/metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s & - -### run tests -npx ts-mocha -p ./tsconfig.json -t 1000000 tests/[0-9]*/[0-9]*.ts - -### nuke -pkill -f "solana logs" -pkill -f solana-test-validator diff --git a/solana/package-lock.json b/solana/package-lock.json index eb6197ac8f..6dd996fc8b 100644 --- a/solana/package-lock.json +++ b/solana/package-lock.json @@ -11,6 +11,7 @@ "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "ethers": "^5.7.2", + "ts-node": "^10.9.1", "ts-results": "^3.3.0" }, "devDependencies": { @@ -423,6 +424,17 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@ethereumjs/common": { "version": "2.6.5", "license": "MIT", @@ -1407,6 +1419,28 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@metamask/eth-sig-util": { "version": "4.0.1", "license": "ISC", @@ -1896,6 +1930,26 @@ "pbts": "bin/pbts" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, "node_modules/@types/bn.js": { "version": "5.1.1", "license": "MIT", @@ -2053,6 +2107,14 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agentkeepalive": { "version": "4.3.0", "license": "MIT", @@ -2198,6 +2260,11 @@ "form-data": "^4.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -2728,6 +2795,11 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, "node_modules/cross-fetch": { "version": "3.1.6", "license": "MIT", @@ -3943,7 +4015,6 @@ }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, "license": "ISC" }, "node_modules/map-obj": { @@ -4961,6 +5032,67 @@ "node": ">=4" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/ts-results": { "version": "3.3.0", "license": "MIT" @@ -5015,7 +5147,6 @@ }, "node_modules/typescript": { "version": "4.9.5", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5059,6 +5190,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, "node_modules/vlq": { "version": "2.0.4", "license": "MIT" @@ -5220,6 +5356,14 @@ "node": ">=10" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, diff --git a/solana/package.json b/solana/package.json index dbbc96603d..73fa4271b2 100644 --- a/solana/package.json +++ b/solana/package.json @@ -10,6 +10,7 @@ "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "ethers": "^5.7.2", + "ts-node": "^10.9.1", "ts-results": "^3.3.0" }, "devDependencies": { diff --git a/solana/programs/core-bridge/Cargo.toml b/solana/programs/core-bridge/Cargo.toml index 177202d471..9e7e3e1b09 100644 --- a/solana/programs/core-bridge/Cargo.toml +++ b/solana/programs/core-bridge/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wormhole-core-bridge-solana" description = "Wormhole Core Bridge Program for Solana" -version = "0.0.0-alpha.4" +version = "0.0.0-alpha.6" edition = "2021" authors = ["W7"] license = "Apache-2.0" @@ -26,7 +26,7 @@ testnet = [] [dependencies] wormhole-io = "0.1.0" -wormhole-raw-vaas = { version = "=0.0.0-alpha.7", features = ["ruint", "on-chain"], default-features = false } +wormhole-raw-vaas = { version = "=0.0.0-alpha.9", features = ["ruint", "on-chain"], default-features = false } anchor-lang = { version = "0.28.0", features = ["derive", "init-if-needed"] } solana-program = "1.14" diff --git a/solana/programs/core-bridge/README.md b/solana/programs/core-bridge/README.md index dd193dda75..655f9ce8b7 100644 --- a/solana/programs/core-bridge/README.md +++ b/solana/programs/core-bridge/README.md @@ -11,11 +11,9 @@ address, there are a few traits that you the integrator will have to implement: - `PublishMessage<'info>` - Ensures that all Core Bridge accounts are included in your [account context]. - `CreateAccount<'info>` - - Requires payer and system program account infos. -- `InvokeCoreBridge<'info>` - - Requires Core Bridge program account info. + - Requires payer and System program account infos. -These traits are found in the SDK submodule of the Core Bridge program crate. +These traits are found in the [SDK] submodule of the Core Bridge program crate. ```rust,ignore use wormhole_core_bridge_solana::sdk as core_bridge_sdk; @@ -61,12 +59,14 @@ pub struct PublishHelloWorld<'info> { This account context must have all of the accounts required by the Core Bridge program in order to publish a Wormhole message: -- Core Bridge config (seeds: ["Bridge"]). -- Core Message (which in this example is just a keypair generated off-chain). -- Core Emitter Sequence (seeds: ["Sequence", your_program_id]). - - **NOTE** Your program ID is the emitter in this case, which is why the emitter sequence PDA - address is derived using this pubkey. -- Core Fee Collector (seeds ["fee_collector"]). +- `core_message` (which in this example is just a keypair generated off-chain). +- `core_emitter_authority` (seeds: ["emitter"]). + - **NOTE: Your program ID is the emitter in this case, which requires these specific seeds.** +- `core_bridge_config` (seeds: ["Bridge"]). +- `core_emitter_sequence` (seeds: ["Sequence", your_program_id]). + - **NOTE: Your program ID is the emitter in this case, which is why the emitter sequence PDA + address is derived using this pubkey.** +- `core_fee_collector` (seeds ["fee_collector"]). **You are not required to re-derive these PDA addresses in your program's account context because the Core Bridge program already does these derivations. Doing so is a waste of compute units.** @@ -75,23 +75,16 @@ The traits above would be implemented by calling `to_account_info` on the approp your context. By making sure that the `core_bridge_program` account is the correct program, your context will use -the [Program] account wrapper with the `CoreBridge` type. Implementing the `InvokeCoreBridge` trait -is required for the `PublishMessage` trait and is as simple as: - -```rust,ignore -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for PublishHelloWorld<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} -``` +the [Program] account wrapper with the `CoreBridge` type. Because publishing a Wormhole message requires creating account(s), the `PublishMessage` trait requires the `CreateAccount` trait, which defines a `payer` account, who has the lamports to send to a new account, and the `system_program`, which is used via CPI to create accounts. ```rust,ignore -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for PublishHelloWorld<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -104,23 +97,23 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'in Finally implement the `PublishMessage` trait by providing the necessary Core Bridge accounts. -**NOTE: For messages where the emitter addresses is your program ID, the `core_emitter` in this case -is `None` and `core_emitter_authority` is `Some(emitter authority)`, which is your program's PDA -address derived using `[b"emitter"]` as its seeds. This seed prefix is provided for you as `PROGRAM_EMITTER_SEED_PREFIX` and is used in your account context to validate the correct emitter -authority is provided.** +**NOTE: For messages where the emitter address is your program ID, the `core_emitter_authority` is +your program's PDA address derived using `[b"emitter"]` as its seeds. This seed prefix is provided +for you as `PROGRAM_EMITTER_SEED_PREFIX` and is used in your account context to validate that the +correct emitter authority is provided.** ```rust,ignore impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_emitter(&self) -> Option> { - None + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter_authority(&self) -> Option> { - Some(self.core_program_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_program_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -130,10 +123,6 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'i fn core_fee_collector(&self) -> Option> { Some(self.core_fee_collector.to_account_info()) } - - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() - } } ``` @@ -142,7 +131,7 @@ CPI SDK with the `PublishMessageDirective::ProgramMessage` with your program ID. program will verify that your emitter authority can be derived the same way using the provided program ID (this validates the correct emitter address will be used for your Wormhole message). -This directive with the other message arguments (nonce, Solana commitment level and message payload) +This directive with the other message arguments (`nonce`, Solana `commitment` and message `payload`) will invoke the Core Bridge to create a message account observed by the Guardians. When the Wormhole Guardians sign this message attesting to its observation, you may redeem this attested message (VAA) on any network where a Core Bridge smart contract is deployed. @@ -154,25 +143,28 @@ pub fn publish_hello_world(ctx: Context) -> Result<()> { core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::ProgramMessage { program_id: crate::ID, nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[ + Some(&[&[ core_bridge_sdk::PROGRAM_EMITTER_SEED_PREFIX, &[ctx.bumps["core_program_emitter"]], - ], - None, + ]]), ) } ``` -And that is all you need to do to emit a Wormhole message from Solana. Putting everything together -to make a simple Anchor program looks like the following: +And that is all you need to do to emit a Wormhole message from Solana. + +## Putting it All Together ```rust,ignore +#![allow(clippy::result_large_err)] + use anchor_lang::prelude::*; use wormhole_core_bridge_solana::sdk as core_bridge_sdk; @@ -211,13 +203,9 @@ pub struct PublishHelloWorld<'info> { core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for PublishHelloWorld<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} - -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for PublishHelloWorld<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -228,16 +216,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for PublishHelloWorld<'in } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_emitter(&self) -> Option> { - None + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter_authority(&self) -> Option> { - Some(self.core_program_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_program_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -247,10 +235,6 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for PublishHelloWorld<'i fn core_fee_collector(&self) -> Option> { Some(self.core_fee_collector.to_account_info()) } - - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() - } } #[program] @@ -263,17 +247,17 @@ pub mod core_bridge_hello_world { core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::ProgramMessage { program_id: crate::ID, nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[ + Some(&[&[ core_bridge_sdk::PROGRAM_EMITTER_SEED_PREFIX, &[ctx.bumps["core_program_emitter"]], - ], - None, + ]]), ) } } @@ -282,3 +266,4 @@ pub mod core_bridge_hello_world { [account context]: https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html [anchor]: https://docs.rs/anchor-lang/latest/anchor_lang/ [program]: https://docs.rs/anchor-lang/latest/anchor_lang/accounts/program/struct.Program.html +[sdk]: https://docs.rs/wormhole-core-bridge-solana/latest/wormhole_core_bridge_solana/sdk/cpi/index.html diff --git a/solana/programs/core-bridge/src/constants.rs b/solana/programs/core-bridge/src/constants.rs index 79500c3f7e..1334937e3d 100644 --- a/solana/programs/core-bridge/src/constants.rs +++ b/solana/programs/core-bridge/src/constants.rs @@ -1,5 +1,5 @@ //! Constants used by the Core Bridge Program. For integrators, necessary constants are re-exported -//! in the [sdk](mod@crate::sdk) module. +//! in the [sdk](crate::sdk) module. use anchor_lang::prelude::constant; @@ -24,6 +24,8 @@ pub const PROGRAM_EMITTER_SEED_PREFIX: &[u8] = b"emitter"; #[constant] pub const MAX_MESSAGE_PAYLOAD_SIZE: usize = 30 * 1_024; +pub(crate) const GOVERNANCE_CHAIN: u16 = 1; + pub(crate) const GOVERNANCE_EMITTER: [u8; 32] = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, ]; diff --git a/solana/programs/core-bridge/src/error.rs b/solana/programs/core-bridge/src/error.rs index 65d52cf93d..f5e4b64af2 100644 --- a/solana/programs/core-bridge/src/error.rs +++ b/solana/programs/core-bridge/src/error.rs @@ -53,7 +53,7 @@ pub enum CoreBridgeError { GovernanceForAnotherChain = 0x26, #[msg("InvalidGovernanceVaa")] - InvalidGovernanceVaa, + InvalidGovernanceVaa = 0x28, #[msg("InsufficientFees")] InsufficientFees = 0x100, @@ -97,8 +97,8 @@ pub enum CoreBridgeError { #[msg("InvalidSigVerifyInstruction")] InvalidSigVerifyInstruction = 0x704, - #[msg("PostVaaGuardianSetExpired")] - PostVaaGuardianSetExpired = 0x706, + #[msg("GuardianSetExpired")] + GuardianSetExpired = 0x706, #[msg("InvalidGuardianKeyRecovery")] InvalidGuardianKeyRecovery = 0x708, @@ -133,9 +133,6 @@ pub enum CoreBridgeError { #[msg("InvalidSignature")] InvalidSignature = 0x1080, - #[msg("GuardianSetExpired")] - GuardianSetExpired = 0x1090, - #[msg("UnverifiedVaa")] UnverifiedVaa = 0x10a0, @@ -178,6 +175,9 @@ pub enum CoreBridgeError { #[msg("WriteAuthorityMismatch")] WriteAuthorityMismatch = 0x10ba, + #[msg("PostedVaaPayloadTooLarge")] + PostedVaaPayloadTooLarge = 0x10bc, + #[msg("EmitterRequired")] EmitterRequired = 0x2000, diff --git a/solana/programs/core-bridge/src/legacy/accounts.rs b/solana/programs/core-bridge/src/legacy/accounts.rs deleted file mode 100644 index 38c9e4de8a..0000000000 --- a/solana/programs/core-bridge/src/legacy/accounts.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! A set of structs mirroring the structs deriving Accounts, where each field is a Pubkey. This is -//! useful for specifying accounts for a client. - -use anchor_lang::prelude::Pubkey; - -/// Context to post a new Core Bridge message. -pub struct PostMessage { - /// Core Bridge Program Data (mut, seeds = \["Bridge"\]). - pub config: Pubkey, - /// Core Bridge Message (mut). - pub message: Pubkey, - /// Core Bridge Emitter (optional, read-only signer). - pub emitter: Option, - /// Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\]). - pub emitter_sequence: Pubkey, - /// Transaction payer (mut signer). - pub payer: Pubkey, - /// Core Bridge Fee Collector (optional, mut, seeds = \["fee_collector"\]). - pub fee_collector: Option, - /// System Program. - pub system_program: Pubkey, -} - -/// Context to post a new or reuse an existing Core Bridge message. -pub struct PostMessageUnreliable { - /// Core Bridge Program Data (mut, seeds = \["Bridge"\]). - pub config: Pubkey, - /// Core Bridge Message (mut). - pub message: Pubkey, - /// Core Bridge Emitter (read-only signer). - pub emitter: Pubkey, - /// Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\]). - pub emitter_sequence: Pubkey, - /// Transaction payer (mut signer). - pub payer: Pubkey, - /// Core Bridge Fee Collector (optional, mut, seeds = \["fee_collector"\]). - pub fee_collector: Option, - /// System Program. - pub system_program: Pubkey, -} diff --git a/solana/programs/core-bridge/src/legacy/accounts/mod.rs b/solana/programs/core-bridge/src/legacy/accounts/mod.rs new file mode 100644 index 0000000000..e71c6db872 --- /dev/null +++ b/solana/programs/core-bridge/src/legacy/accounts/mod.rs @@ -0,0 +1,85 @@ +//! A set of structs mirroring the structs deriving [Accounts](anchor_lang::prelude::Accounts), +//! where each field is a [Pubkey]. This is useful for specifying self for a client. +//! +//! NOTE: This is similar to how [accounts](mod@crate::accounts) is generated via Anchor's +//! [program][anchor_lang::prelude::program] macro. + +use anchor_lang::{prelude::Pubkey, ToAccountMetas}; +use solana_program::instruction::AccountMeta; + +/// Context to post a new Core Bridge message. +pub struct PostMessage { + /// Core Bridge Program Data (mut, seeds = \["Bridge"\]). + pub config: Pubkey, + /// Core Bridge Message (mut). + pub message: Pubkey, + /// Core Bridge Emitter (optional, read-only signer). + pub emitter: Option, + /// Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\]). + pub emitter_sequence: Pubkey, + /// Transaction payer (mut signer). + pub payer: Pubkey, + /// Core Bridge Fee Collector (optional, read-only, seeds = \["fee_collector"\]). + pub fee_collector: Option, + /// System Program. + pub system_program: Pubkey, +} + +impl ToAccountMetas for PostMessage { + fn to_account_metas(&self, message_is_signer: Option) -> Vec { + // Using `message_is_signer` above is a hack. But because we do not want to return a result, + // we assume that the caller of this is passing in whether a message is a signer, which is + // the case when a new message account is created when someone invokes this instruction. + // Otherwise the message was already prepared so it does not have to be a signer. + + // If the emitter is None, we do not require it to be a signer. + let (emitter, emitter_is_signer) = match self.emitter { + Some(emitter) => (emitter, true), + None => (crate::ID, false), + }; + + vec![ + AccountMeta::new(self.config, false), + AccountMeta::new(self.message, message_is_signer.unwrap_or(true)), + AccountMeta::new_readonly(emitter, emitter_is_signer), + AccountMeta::new(self.emitter_sequence, false), + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(self.fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(self.system_program, false), + ] + } +} + +/// Context to post a new or reuse an existing Core Bridge message. +pub struct PostMessageUnreliable { + /// Core Bridge Program Data (mut, seeds = \["Bridge"\]). + pub config: Pubkey, + /// Core Bridge Message (mut). + pub message: Pubkey, + /// Core Bridge Emitter (read-only signer). + pub emitter: Pubkey, + /// Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\]). + pub emitter_sequence: Pubkey, + /// Transaction payer (mut signer). + pub payer: Pubkey, + /// Core Bridge Fee Collector (optional, read-only, seeds = \["fee_collector"\]). + pub fee_collector: Option, + /// System Program. + pub system_program: Pubkey, +} + +impl ToAccountMetas for PostMessageUnreliable { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.config, false), + AccountMeta::new(self.message, true), + AccountMeta::new_readonly(self.emitter, true), + AccountMeta::new(self.emitter_sequence, false), + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(self.fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(self.system_program, false), + ] + } +} diff --git a/solana/programs/core-bridge/src/legacy/instruction/initialize.rs b/solana/programs/core-bridge/src/legacy/instruction/initialize.rs deleted file mode 100644 index 80485e1796..0000000000 --- a/solana/programs/core-bridge/src/legacy/instruction/initialize.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -/// Arguments used to initialize the Core Bridge program. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct InitializeArgs { - pub guardian_set_ttl_seconds: u32, - pub fee_lamports: u64, - pub initial_guardians: Vec<[u8; 20]>, -} diff --git a/solana/programs/core-bridge/src/legacy/instruction/mod.rs b/solana/programs/core-bridge/src/legacy/instruction/mod.rs index 4135aa5fd2..a04a3ee3b5 100644 --- a/solana/programs/core-bridge/src/legacy/instruction/mod.rs +++ b/solana/programs/core-bridge/src/legacy/instruction/mod.rs @@ -2,24 +2,7 @@ //! with a struct defining the input arguments to the method. These should be used directly, when //! one wants to serialize instruction data, for example, when speciying instructions on a client. -mod initialize; -pub use initialize::InitializeArgs; - -mod post_message; -#[cfg(feature = "no-entrypoint")] -pub use post_message::post_message; -pub use post_message::PostMessageArgs; - -mod post_message_unreliable; -#[cfg(feature = "no-entrypoint")] -pub use post_message_unreliable::post_message_unreliable; - -mod post_vaa; -pub use post_vaa::PostVaaArgs; - -mod verify_signatures; -pub use verify_signatures::VerifySignaturesArgs; - +use crate::types::Commitment; use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; /// Legacy instruction selector. @@ -33,7 +16,7 @@ pub enum LegacyInstruction { Initialize, /// Publish a Wormhole message by creating a message account reflecting the message. PostMessage, - /// Write an account reflecting a validated VAA (Version 1). + /// Write an account reflecting a verified VAA (Version 1). PostVaa, /// **Governance.** Set the fee for posting a message. SetMessageFee, @@ -49,6 +32,113 @@ pub enum LegacyInstruction { PostMessageUnreliable, } +/// Arguments used to initialize the Core Bridge program. +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InitializeArgs { + pub guardian_set_ttl_seconds: u32, + pub fee_lamports: u64, + pub initial_guardians: Vec<[u8; 20]>, +} + +/// Arguments used to post a new Wormhole (Core Bridge) message either using +/// [post_message](crate::legacy::instruction::post_message) or +/// [post_message_unreliable](crate::legacy::instruction::post_message_unreliable). +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct PostMessageArgs { + /// Unique id for this message. + pub nonce: u32, + /// Encoded message. + pub payload: Vec, + /// Solana commitment level for Guardian observation. + pub commitment: Commitment, +} + +/// Arguments to post new VAA data after signature verification. +/// +/// NOTE: It is preferred to use the new process of verifying a VAA using the new Core Bridge Anchor +/// instructions. See [init_encoded_vaa](crate::wormhole_core_bridge_solana::init_encoded_vaa) and +/// [write_encoded_vaa](crate::wormhole_core_bridge_solana::write_encoded_vaa) for more info. +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct PostVaaArgs { + /// Unused data. + pub _gap_0: [u8; 5], + /// Time the message was submitted. + pub timestamp: u32, + /// Unique ID for this message. + pub nonce: u32, + /// The Wormhole chain ID denoting the origin of this message. + pub emitter_chain: u16, + /// Emitter of the message. + pub emitter_address: [u8; 32], + /// Sequence number of this message. + pub sequence: u64, + /// Level of consistency requested by the emitter. + pub consistency_level: u8, + /// Message payload. + pub payload: Vec, +} + +/// Arguments to verify specific guardian indices. +/// +/// NOTE: It is preferred to use the new process of verifying a VAA using the new Core Bridge Anchor +/// instructions. See [init_encoded_vaa](crate::wormhole_core_bridge_solana::init_encoded_vaa) and +/// [write_encoded_vaa](crate::wormhole_core_bridge_solana::write_encoded_vaa) for more info. +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct VerifySignaturesArgs { + /// Indices of verified guardian signatures, where -1 indicates a missing value. There is a + /// missing value if the guardian at this index is not expected to have its signature verfied by + /// the Sig Verify native program in the instruction invoked prior). + /// + /// NOTE: In the legacy implementation, this argument being a fixed-sized array of 19 only + /// allows the first 19 guardians of any size guardian set to be verified. Because of this, it + /// is absolutely important to use the new process of verifying a VAA. + pub signer_indices: [i8; 19], +} + /// Unit struct used to represent an empty instruction argument. #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] pub struct EmptyArgs {} + +#[cfg(feature = "no-entrypoint")] +mod __no_entrypoint { + use crate::legacy::instruction::{LegacyInstruction, PostMessageArgs}; + use anchor_lang::ToAccountMetas; + use solana_program::instruction::Instruction; + + /// Processor to post (publish) a Wormhole message by setting up the message account for + /// Guardian observation. + /// + /// A message is either created beforehand using the new Anchor instruction to process a message + /// or is created at this point. + pub fn post_message( + accounts: crate::legacy::accounts::PostMessage, + args: PostMessageArgs, + ) -> Instruction { + let message_is_signer = !args.payload.is_empty(); + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::PostMessage, args), + accounts.to_account_metas(Some(message_is_signer)), + ) + } + + /// Processor to post (publish) a Wormhole message by setting up the message account for + /// Guardian observation. This message account has either been created already or is created in + /// this call. + /// + /// If this message account already exists, the emitter must be the same as the one encoded in + /// the message and the payload must be the same size. + pub fn post_message_unreliable( + accounts: crate::legacy::accounts::PostMessageUnreliable, + args: PostMessageArgs, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::PostMessageUnreliable, args), + accounts.to_account_metas(None), + ) + } +} + +#[cfg(feature = "no-entrypoint")] +pub use __no_entrypoint::*; diff --git a/solana/programs/core-bridge/src/legacy/instruction/post_message.rs b/solana/programs/core-bridge/src/legacy/instruction/post_message.rs deleted file mode 100644 index bd1deef7d8..0000000000 --- a/solana/programs/core-bridge/src/legacy/instruction/post_message.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::types::Commitment; -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -/// Arguments used to post a new Wormhole (Core Bridge) message either using -/// [post_message](crate::legacy::instruction::post_message) or -/// [post_message_unreliable](crate::legacy::instruction::post_message_unreliable). -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct PostMessageArgs { - pub nonce: u32, - pub payload: Vec, - pub commitment: Commitment, -} - -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::LegacyInstruction; - use solana_program::instruction::{AccountMeta, Instruction}; - - use super::*; - - pub fn post_message( - accounts: crate::legacy::accounts::PostMessage, - args: PostMessageArgs, - ) -> Instruction { - let fee_collector = accounts.fee_collector.unwrap_or(crate::ID); - let (emitter, emitter_is_signer) = match accounts.emitter { - Some(emitter) => (emitter, true), - None => (crate::ID, false), - }; - - // This part is a hack. But to avoid having to return a result, we assume that if the - // payload provided is empty, then the message is already prepared (so the message does not - // have to be a signer). - let message_is_signer = !args.payload.is_empty(); - - let accounts = vec![ - AccountMeta::new(accounts.config, false), - AccountMeta::new(accounts.message, message_is_signer), - AccountMeta::new_readonly(emitter, emitter_is_signer), - AccountMeta::new(accounts.emitter_sequence, false), - AccountMeta::new(accounts.payer, true), - AccountMeta::new(fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(crate::ID, false), // _rent - ]; - - Instruction::new_with_borsh(crate::ID, &(LegacyInstruction::PostMessage, args), accounts) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/core-bridge/src/legacy/instruction/post_message_unreliable.rs b/solana/programs/core-bridge/src/legacy/instruction/post_message_unreliable.rs deleted file mode 100644 index 2ceddb1afd..0000000000 --- a/solana/programs/core-bridge/src/legacy/instruction/post_message_unreliable.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::{cpi::PostMessageArgs, instruction::LegacyInstruction}; - use solana_program::instruction::{AccountMeta, Instruction}; - - /// This instruction handler is used to post a new message to the core bridge using an existing - /// message account. - /// - /// The constraints for posting a message using this instruction handler are: - /// * Emitter must be the same as the message account's emitter. - /// * The new message must be the same size as the existing message's payload. - pub fn post_message_unreliable( - accounts: crate::legacy::accounts::PostMessageUnreliable, - args: PostMessageArgs, - ) -> Instruction { - let fee_collector = accounts.fee_collector.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.config, false), - AccountMeta::new(accounts.message, true), - AccountMeta::new_readonly(accounts.emitter, true), - AccountMeta::new(accounts.emitter_sequence, false), - AccountMeta::new(accounts.payer, true), - AccountMeta::new(fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(crate::ID, false), // _rent - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::PostMessageUnreliable, args), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/core-bridge/src/legacy/instruction/post_vaa.rs b/solana/programs/core-bridge/src/legacy/instruction/post_vaa.rs deleted file mode 100644 index e07beeb624..0000000000 --- a/solana/programs/core-bridge/src/legacy/instruction/post_vaa.rs +++ /dev/null @@ -1,22 +0,0 @@ -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -/// Arguments to post new VAA data after signature verification. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct PostVaaArgs { - /// Unused data. - pub _gap_0: [u8; 5], - /// Time the message was submitted. - pub timestamp: u32, - /// Unique ID for this message. - pub nonce: u32, - /// The Wormhole chain ID denoting the origin of this message. - pub emitter_chain: u16, - /// Emitter of the message. - pub emitter_address: [u8; 32], - /// Sequence number of this message. - pub sequence: u64, - /// Level of consistency requested by the emitter. - pub consistency_level: u8, - /// Message payload. - pub payload: Vec, -} diff --git a/solana/programs/core-bridge/src/legacy/instruction/verify_signatures.rs b/solana/programs/core-bridge/src/legacy/instruction/verify_signatures.rs deleted file mode 100644 index 6c729e836b..0000000000 --- a/solana/programs/core-bridge/src/legacy/instruction/verify_signatures.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -/// Argument to verify specific guardian indices. -/// -/// NOTE: It is preferred to use the new process of verifying a VAA using the new Core Bridge Anchor -/// instructions. See [init_encoded_vaa](crate::wormhole_core_bridge_solana::init_encoded_vaa) and -/// [process_encoded_vaa](crate::wormhole_core_bridge_solana::process_encoded_vaa) for more info. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct VerifySignaturesArgs { - pub signer_indices: [i8; 19], -} diff --git a/solana/programs/core-bridge/src/legacy/mod.rs b/solana/programs/core-bridge/src/legacy/mod.rs index 5511b0e44f..1328dfa3e6 100644 --- a/solana/programs/core-bridge/src/legacy/mod.rs +++ b/solana/programs/core-bridge/src/legacy/mod.rs @@ -13,20 +13,27 @@ pub mod state; pub mod utils; -/// **Integrators: Please use [sdk](mod@crate::sdk) instead of this module.** Methods used to -/// interact with the Core Bridge Program via CPI. +/// Collection of methods to interact with the Core Bridge program via CPI. The structs defined in +/// this module mirror the structs deriving [Accounts](anchor_lang::prelude::Accounts), where each +/// field is an [AccountInfo]. **Integrators: Please use [sdk](crate::sdk) instead of this module.** +/// +/// NOTE: This is similar to how [cpi](mod@crate::cpi) is generated via Anchor's +/// [program][anchor_lang::prelude::program] macro. #[cfg(feature = "cpi")] pub mod cpi { - pub use instruction::PostMessageArgs; - use anchor_lang::prelude::*; use solana_program::program::invoke_signed; use super::*; + /// Processor to post (publish) a Wormhole message by setting up the message account for + /// Guardian observation. + /// + /// A message is either created beforehand using the new Anchor instruction to process a message + /// or is created at this point. pub fn post_message<'info>( ctx: CpiContext<'_, '_, '_, 'info, PostMessage<'info>>, - args: PostMessageArgs, + args: instruction::PostMessageArgs, ) -> Result<()> { invoke_signed( &instruction::post_message( @@ -47,15 +54,15 @@ pub mod cpi { .map_err(Into::into) } - /// This instruction handler is used to post a new message to the core bridge using an existing - /// message account. + /// Processor to post (publish) a Wormhole message by setting up the message account for + /// Guardian observation. This message account has either been created already or is created in + /// this call. /// - /// The constraints for posting a message using this instruction handler are: - /// * Emitter must be the same as the message account's emitter. - /// * The new message must be the same size as the existing message's payload. + /// If this message account already exists, the emitter must be the same as the one encoded in + /// the message and the payload must be the same size. pub fn post_message_unreliable<'info>( ctx: CpiContext<'_, '_, '_, 'info, PostMessageUnreliable<'info>>, - args: PostMessageArgs, + args: instruction::PostMessageArgs, ) -> Result<()> { invoke_signed( &instruction::post_message_unreliable( @@ -89,7 +96,7 @@ pub mod cpi { pub emitter_sequence: AccountInfo<'info>, /// CHECK: Transaction payer (mut signer). pub payer: AccountInfo<'info>, - /// CHECK: Core Bridge Fee Collector (optional, mut, seeds = \["fee_collector"\]). + /// CHECK: Core Bridge Fee Collector (optional, read-only, seeds = \["fee_collector"\]). pub fee_collector: Option>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, @@ -108,7 +115,7 @@ pub mod cpi { pub emitter_sequence: AccountInfo<'info>, /// CHECK: Transaction payer (mut signer). pub payer: AccountInfo<'info>, - /// CHECK: Core Bridge Fee Collector (optional, mut, seeds = \["fee_collector"\]). + /// CHECK: Core Bridge Fee Collector (optional, read-only, seeds = \["fee_collector"\]). pub fee_collector: Option>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, diff --git a/solana/programs/core-bridge/src/legacy/processor/governance/guardian_set_update.rs b/solana/programs/core-bridge/src/legacy/processor/governance/guardian_set_update.rs index 645453c225..8173cae7ea 100644 --- a/solana/programs/core-bridge/src/legacy/processor/governance/guardian_set_update.rs +++ b/solana/programs/core-bridge/src/legacy/processor/governance/guardian_set_update.rs @@ -1,9 +1,9 @@ use crate::{ error::CoreBridgeError, legacy::{instruction::EmptyArgs, utils::LegacyAnchorized}, - state::{Claim, Config, GuardianSet}, + state::{Config, GuardianSet}, types::Timestamp, - zero_copy::PostedVaaV1, + zero_copy::{LoadZeroCopy, VaaAccount}, }; use anchor_lang::prelude::*; use wormhole_raw_vaas::core::CoreBridgeGovPayload; @@ -23,34 +23,22 @@ pub struct GuardianSetUpdate<'info> { config: Account<'info, LegacyAnchorized<0, Config>>, /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the - /// instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump - )] - posted_vaa: AccountInfo<'info>, + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - /// Account representing that a VAA has been consumed. - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](crate::utils::vaa::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// Existing guardian set, whose guardian set index is the same one found in the [Config]. #[account( mut, - seeds = [GuardianSet::SEED_PREFIX, &config.guardian_set_index.to_be_bytes()], + seeds = [ + GuardianSet::SEED_PREFIX, + &config.guardian_set_index.to_be_bytes() + ], bump, )] curr_guardian_set: Account<'info, LegacyAnchorized<0, GuardianSet>>, @@ -60,8 +48,11 @@ pub struct GuardianSetUpdate<'info> { #[account( init, payer = payer, - space = try_compute_size(&posted_vaa)?, - seeds = [GuardianSet::SEED_PREFIX, &(curr_guardian_set.index + 1).to_be_bytes()], + space = try_compute_size(&vaa)?, + seeds = [ + GuardianSet::SEED_PREFIX, + &curr_guardian_set.index.saturating_add(1).to_be_bytes() + ], bump, )] new_guardian_set: Account<'info, LegacyAnchorized<0, GuardianSet>>, @@ -69,6 +60,16 @@ pub struct GuardianSetUpdate<'info> { system_program: Program<'info, System>, } +impl<'info> crate::utils::cpi::CreateAccount<'info> for GuardianSetUpdate<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for GuardianSetUpdate<'info> { @@ -80,8 +81,8 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> impl<'info> GuardianSetUpdate<'info> { fn constraints(ctx: &Context) -> Result<()> { let config = &ctx.accounts.config; - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let gov_payload = super::require_valid_posted_governance_vaa(&acc_data, config)?; + let vaa = VaaAccount::load(&ctx.accounts.vaa)?; + let gov_payload = super::require_valid_governance_vaa(config, &vaa)?; // Encoded guardian set must be the next value after the current guardian set index. // @@ -103,14 +104,16 @@ impl<'info> GuardianSetUpdate<'info> { /// with the new guardians encoded in the governance VAA. #[access_control(GuardianSetUpdate::constraints(&ctx))] fn guardian_set_update(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = VaaAccount::load(&ctx.accounts.vaa).unwrap(); - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + crate::utils::vaa::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; - let gov_payload = CoreBridgeGovPayload::parse(vaa.payload()).unwrap().decree(); + let gov_payload = CoreBridgeGovPayload::try_from(vaa.try_payload().unwrap()) + .unwrap() + .decree(); let decree = gov_payload.guardian_set_update().unwrap(); // Deserialize new guardian set. @@ -128,10 +131,22 @@ fn guardian_set_update(ctx: Context, _args: EmptyArgs) -> Res } // Set new guardian set account fields. + let creation_time = match &vaa { + VaaAccount::EncodedVaa(inner) => inner + .as_vaa() + .unwrap() + .v1() + .unwrap() + .body() + .timestamp() + .into(), + VaaAccount::PostedVaaV1(inner) => inner.timestamp(), + }; + ctx.accounts.new_guardian_set.set_inner( GuardianSet { index: ctx.accounts.curr_guardian_set.index + 1, - creation_time: vaa.timestamp(), + creation_time, keys, expiration_time: Default::default(), } @@ -157,10 +172,9 @@ fn guardian_set_update(ctx: Context, _args: EmptyArgs) -> Res /// NOTE: We check the validity of the governance VAA in access control. If the posted VAA happens /// to deserialize as a guardian set update decree but anything else is invalid about this message, /// this instruction handler will revert (just not at this step when determining the account size). -fn try_compute_size(posted_vaa: &AccountInfo<'_>) -> Result { - let acc_data = posted_vaa.try_borrow_data()?; - let vaa = PostedVaaV1::parse(&acc_data)?; - let gov_payload = CoreBridgeGovPayload::parse(vaa.payload()) +fn try_compute_size(vaa: &AccountInfo) -> Result { + let vaa = VaaAccount::load(vaa)?; + let gov_payload = CoreBridgeGovPayload::try_from(vaa.try_payload()?) .map(|msg| msg.decree()) .map_err(|_| error!(CoreBridgeError::InvalidGovernanceVaa))?; diff --git a/solana/programs/core-bridge/src/legacy/processor/governance/mod.rs b/solana/programs/core-bridge/src/legacy/processor/governance/mod.rs index 0c6e43b06e..b2969a7246 100644 --- a/solana/programs/core-bridge/src/legacy/processor/governance/mod.rs +++ b/solana/programs/core-bridge/src/legacy/processor/governance/mod.rs @@ -10,35 +10,44 @@ pub use transfer_fees::*; mod upgrade_contract; pub use upgrade_contract::*; -use crate::{error::CoreBridgeError, state::Config, zero_copy::PostedVaaV1}; +use crate::{ + error::CoreBridgeError, + state::{Config, VaaVersion}, + zero_copy::VaaAccount, +}; use anchor_lang::prelude::*; use wormhole_raw_vaas::core::{CoreBridgeDecree, CoreBridgeGovPayload}; /// Validate a posted VAA as a Core Bridge governance decree. Specifically for the Core Bridge, the /// guardian set used to attest for this governance decree must be the latest guardian set (found in /// the [Config] account). -pub fn require_valid_posted_governance_vaa<'ctx>( - vaa_acc_data: &'ctx [u8], +pub fn require_valid_governance_vaa<'ctx>( config: &'ctx Config, + vaa: &'ctx VaaAccount<'ctx>, ) -> Result> { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - // Make sure the VAA was attested for by the latest guardian set. + let guardian_set_index = match vaa { + VaaAccount::EncodedVaa(inner) => match inner.as_vaa()? { + VaaVersion::V1(vaa) => vaa.guardian_set_index(), + }, + VaaAccount::PostedVaaV1(inner) => inner.guardian_set_index(), + }; require_eq!( config.guardian_set_index, - vaa.guardian_set_index(), + guardian_set_index, CoreBridgeError::LatestGuardianSetRequired ); // The emitter must be the hard-coded governance emitter. + let (emitter_address, emitter_chain, _) = vaa.try_emitter_info()?; require!( - vaa.emitter_chain() == crate::constants::SOLANA_CHAIN - && vaa.emitter_address() == crate::constants::GOVERNANCE_EMITTER, + emitter_chain == crate::constants::GOVERNANCE_CHAIN + && emitter_address == crate::constants::GOVERNANCE_EMITTER, CoreBridgeError::InvalidGovernanceEmitter ); // Finally attempt to parse the governance decree. - CoreBridgeGovPayload::parse(vaa.payload()) + CoreBridgeGovPayload::try_from(vaa.try_payload().unwrap()) .map(|msg| msg.decree()) .map_err(|_| error!(CoreBridgeError::InvalidGovernanceVaa)) } diff --git a/solana/programs/core-bridge/src/legacy/processor/governance/set_message_fee.rs b/solana/programs/core-bridge/src/legacy/processor/governance/set_message_fee.rs index ee97fdc3e0..34bdec51c9 100644 --- a/solana/programs/core-bridge/src/legacy/processor/governance/set_message_fee.rs +++ b/solana/programs/core-bridge/src/legacy/processor/governance/set_message_fee.rs @@ -2,8 +2,8 @@ use crate::{ constants::SOLANA_CHAIN, error::CoreBridgeError, legacy::{instruction::EmptyArgs, utils::LegacyAnchorized}, - state::{Claim, Config}, - zero_copy::PostedVaaV1, + state::Config, + zero_copy::{LoadZeroCopy, VaaAccount}, }; use anchor_lang::prelude::*; use ruint::aliases::U256; @@ -24,33 +24,28 @@ pub struct SetMessageFee<'info> { config: Account<'info, LegacyAnchorized<0, Config>>, /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the - /// instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump - )] - posted_vaa: AccountInfo<'info>, + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - /// Account representing that a VAA has been consumed. - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](crate::utils::vaa::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, system_program: Program<'info, System>, } +impl<'info> crate::utils::cpi::CreateAccount<'info> for SetMessageFee<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for SetMessageFee<'info> { @@ -61,9 +56,8 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> impl<'info> SetMessageFee<'info> { fn constraints(ctx: &Context) -> Result<()> { - let acc_data = ctx.accounts.posted_vaa.try_borrow_data()?; - let gov_payload = - super::require_valid_posted_governance_vaa(&acc_data, &ctx.accounts.config)?; + let vaa = VaaAccount::load(&ctx.accounts.vaa)?; + let gov_payload = super::require_valid_governance_vaa(&ctx.accounts.config, &vaa)?; let decree = gov_payload .set_message_fee() @@ -79,7 +73,7 @@ impl<'info> SetMessageFee<'info> { // Make sure that the encoded fee does not overflow since the encoded amount is u256 (and // lamports are u64). let fee = U256::from_be_bytes(decree.fee()); - require_gte!(U256::from(u64::MAX), fee, CoreBridgeError::U64Overflow); + require!(fee <= U256::from(u64::MAX), CoreBridgeError::U64Overflow); // Done. Ok(()) @@ -90,13 +84,16 @@ impl<'info> SetMessageFee<'info> { /// the message fee in the [Config] account. #[access_control(SetMessageFee::constraints(&ctx))] fn set_message_fee(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + crate::utils::vaa::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); - let gov_payload = CoreBridgeGovPayload::parse(vaa.payload()).unwrap().decree(); + let gov_payload = CoreBridgeGovPayload::try_from(vaa.try_payload().unwrap()) + .unwrap() + .decree(); // Uint encodes limbs in little endian, so we will take the first u64 value. let fee = U256::from_be_bytes(gov_payload.set_message_fee().unwrap().fee()); diff --git a/solana/programs/core-bridge/src/legacy/processor/governance/transfer_fees.rs b/solana/programs/core-bridge/src/legacy/processor/governance/transfer_fees.rs index 02af02975f..580f0e8740 100644 --- a/solana/programs/core-bridge/src/legacy/processor/governance/transfer_fees.rs +++ b/solana/programs/core-bridge/src/legacy/processor/governance/transfer_fees.rs @@ -2,8 +2,8 @@ use crate::{ constants::{FEE_COLLECTOR_SEED_PREFIX, SOLANA_CHAIN}, error::CoreBridgeError, legacy::{instruction::EmptyArgs, utils::LegacyAnchorized}, - state::{Claim, Config}, - zero_copy::PostedVaaV1, + state::Config, + zero_copy::{LoadZeroCopy, VaaAccount}, }; use anchor_lang::{ prelude::*, @@ -27,29 +27,14 @@ pub struct TransferFees<'info> { config: Account<'info, LegacyAnchorized<0, Config>>, /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the - /// instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump - )] - posted_vaa: AccountInfo<'info>, + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - /// Account representing that a VAA has been consumed. - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](crate::utils::vaa::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// CHECK: Fee collector. Fees will be collected by transferring lamports from this account to /// the recipient. @@ -70,6 +55,16 @@ pub struct TransferFees<'info> { system_program: Program<'info, System>, } +impl<'info> crate::utils::cpi::CreateAccount<'info> for TransferFees<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for TransferFees<'info> { @@ -80,9 +75,8 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> impl<'info> TransferFees<'info> { fn constraints(ctx: &Context) -> Result<()> { - let acc_data = ctx.accounts.posted_vaa.try_borrow_data()?; - let gov_payload = - super::require_valid_posted_governance_vaa(&acc_data, &ctx.accounts.config)?; + let vaa = VaaAccount::load(&ctx.accounts.vaa)?; + let gov_payload = super::require_valid_governance_vaa(&ctx.accounts.config, &vaa)?; let decree = gov_payload .transfer_fees() @@ -98,7 +92,7 @@ impl<'info> TransferFees<'info> { // Make sure that the encoded fee does not overflow since the encoded amount is u256 (and // lamports are u64). let amount = U256::from_be_bytes(decree.amount()); - require_gte!(U256::from(u64::MAX), amount, CoreBridgeError::U64Overflow); + require!(amount <= U256::from(u64::MAX), CoreBridgeError::U64Overflow); // The recipient provided in the account context must be the same as the one encoded in the // governance VAA. @@ -117,9 +111,9 @@ impl<'info> TransferFees<'info> { let fee_collector = AsRef::::as_ref(&ctx.accounts.fee_collector); (fee_collector.data_len(), fee_collector.lamports()) }; - require_gte!( - lamports.saturating_sub(to_u64_unchecked(&amount)), - Rent::get().map(|rent| rent.minimum_balance(data_len))?, + let min_required = Rent::get().map(|rent| rent.minimum_balance(data_len))?; + require!( + lamports.saturating_sub(to_u64_unchecked(&amount)) >= min_required, CoreBridgeError::NotEnoughLamports ); } @@ -131,14 +125,16 @@ impl<'info> TransferFees<'info> { #[access_control(TransferFees::constraints(&ctx))] fn transfer_fees(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = VaaAccount::load(&ctx.accounts.vaa).unwrap(); - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + crate::utils::vaa::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; - let gov_payload = CoreBridgeGovPayload::parse(vaa.payload()).unwrap().decree(); + let gov_payload = CoreBridgeGovPayload::try_from(vaa.try_payload().unwrap()) + .unwrap() + .decree(); let decree = gov_payload.transfer_fees().unwrap(); let fee_collector = AsRef::::as_ref(&ctx.accounts.fee_collector); diff --git a/solana/programs/core-bridge/src/legacy/processor/governance/upgrade_contract.rs b/solana/programs/core-bridge/src/legacy/processor/governance/upgrade_contract.rs index 787617c31b..9e3f274e8a 100644 --- a/solana/programs/core-bridge/src/legacy/processor/governance/upgrade_contract.rs +++ b/solana/programs/core-bridge/src/legacy/processor/governance/upgrade_contract.rs @@ -2,8 +2,8 @@ use crate::{ constants::{SOLANA_CHAIN, UPGRADE_SEED_PREFIX}, error::CoreBridgeError, legacy::{instruction::EmptyArgs, utils::LegacyAnchorized}, - state::{Claim, Config}, - zero_copy::PostedVaaV1, + state::Config, + zero_copy::{LoadZeroCopy, VaaAccount}, }; use anchor_lang::prelude::*; use solana_program::{bpf_loader_upgradeable, program::invoke_signed}; @@ -23,29 +23,14 @@ pub struct UpgradeContract<'info> { config: Account<'info, LegacyAnchorized<0, Config>>, /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the - /// instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump - )] - posted_vaa: AccountInfo<'info>, + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - /// Account representing that a VAA has been consumed. - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](crate::utils::vaa::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// CHECK: We need this upgrade authority to invoke the BPF Loader Upgradeable program to /// upgrade this program's executable. We verify this PDA address here out of convenience to get @@ -83,6 +68,16 @@ pub struct UpgradeContract<'info> { system_program: Program<'info, System>, } +impl<'info> crate::utils::cpi::CreateAccount<'info> for UpgradeContract<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for UpgradeContract<'info> { @@ -93,9 +88,8 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> impl<'info> UpgradeContract<'info> { fn constraints(ctx: &Context) -> Result<()> { - let acc_data = ctx.accounts.posted_vaa.try_borrow_data()?; - let gov_payload = - super::require_valid_posted_governance_vaa(&acc_data, &ctx.accounts.config)?; + let vaa = VaaAccount::load(&ctx.accounts.vaa)?; + let gov_payload = super::require_valid_governance_vaa(&ctx.accounts.config, &vaa)?; let decree = gov_payload .contract_upgrade() @@ -124,9 +118,12 @@ impl<'info> UpgradeContract<'info> { /// Loader Upgradeable program to upgrade this program's executable to the provided buffer. #[access_control(UpgradeContract::constraints(&ctx))] fn upgrade_contract(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + crate::utils::vaa::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; // Finally upgrade. invoke_signed( diff --git a/solana/programs/core-bridge/src/legacy/processor/post_message/mod.rs b/solana/programs/core-bridge/src/legacy/processor/post_message/mod.rs index f98090b0e0..37fc274873 100644 --- a/solana/programs/core-bridge/src/legacy/processor/post_message/mod.rs +++ b/solana/programs/core-bridge/src/legacy/processor/post_message/mod.rs @@ -3,13 +3,19 @@ pub use unreliable::*; use crate::{ error::CoreBridgeError, - legacy::{instruction::PostMessageArgs, utils::LegacyAnchorized}, + legacy::{ + instruction::PostMessageArgs, + utils::{LegacyAccount, LegacyAnchorized}, + }, state::{ Config, EmitterSequence, MessageStatus, PostedMessageV1, PostedMessageV1Data, PostedMessageV1Info, }, + utils, + zero_copy::LoadZeroCopy, }; use anchor_lang::prelude::*; + #[derive(Accounts)] #[instruction(args: PostMessageArgs)] pub struct PostMessage<'info> { @@ -59,7 +65,6 @@ pub struct PostMessage<'info> { /// CHECK: Fee collector, which is used to update the [Config] account with the most up-to-date /// last lamports on this account. #[account( - mut, seeds = [crate::constants::FEE_COLLECTOR_SEED_PREFIX], bump, )] @@ -69,9 +74,16 @@ pub struct PostMessage<'info> { _clock: UncheckedAccount<'info>, system_program: Program<'info, System>, +} - /// CHECK: Previously needed sysvar. - _rent: UncheckedAccount<'info>, +impl<'info> crate::utils::cpi::CreateAccount<'info> for PostMessage<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } } impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, PostMessageArgs> @@ -80,21 +92,65 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, PostMessageArg const LOG_IX_NAME: &'static str = "LegacyPostMessage"; const ANCHOR_IX_FN: fn(Context, PostMessageArgs) -> Result<()> = post_message; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + order_post_message_account_infos(account_infos) + } +} + +/// The Anchor context orders the accounts as: +/// +/// 1. `config` +/// 2. `message` +/// 3. `emitter` +/// 4. `emitter_sequence` +/// 5. `payer` +/// 6. `fee_collector` +/// 7. `clock` +/// 8. `system_program` +/// +/// Because the legacy implementation did not require specifying where the System program should be, +/// we ensure that it is account #8 because the Anchor account context requires it to be in this +/// position. +pub(super) fn order_post_message_account_infos<'info>( + account_infos: &[AccountInfo<'info>], +) -> Result>> { + const NUM_ACCOUNTS: usize = 8; + const SYSTEM_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + + let mut infos = account_infos.to_vec(); + + // We only need to order the account infos if there are more than 8 accounts. + if infos.len() > NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + } + + Ok(infos) } /// This method is used by both `post_message` and `post_message_unreliable` instruction handlers. /// It handles the message fee check on the fee collector, upticks the emitter sequence number and /// returns the posted message data, which will be serialized to either `PostedMessageV1` or /// `PostedMessageV1Unreliable` depending on which instruction handler called this method. -pub(super) fn new_posted_message_data( +pub(super) fn new_posted_message_info( config: &mut Account>, fee_collector: &Option, emitter_sequence: &mut Account>, consistency_level: u8, nonce: u32, emitter: &Pubkey, - payload: Vec, -) -> Result { +) -> Result { // Determine whether fee has been paid. Update core bridge config account if so. // // NOTE: This is inconsistent with other Core Bridge implementations, where we would check that @@ -105,33 +161,30 @@ pub(super) fn new_posted_message_data( let sequence = emitter_sequence.value; // Finally set the `message` account with posted data. - let data = PostedMessageV1Data { - info: PostedMessageV1Info { - consistency_level, - emitter_authority: Default::default(), - status: MessageStatus::Unset, - _gap_0: Default::default(), - posted_timestamp: Clock::get().map(Into::into)?, - nonce, - sequence, - solana_chain_id: Default::default(), - emitter: *emitter, - }, - payload, + let info = PostedMessageV1Info { + consistency_level, + emitter_authority: Default::default(), + status: MessageStatus::Unset, + _gap_0: Default::default(), + posted_timestamp: Clock::get().map(Into::into)?, + nonce, + sequence, + solana_chain_id: Default::default(), + emitter: *emitter, }; // Increment emitter sequence value. emitter_sequence.value += 1; // Done. - Ok(data) + Ok(info) } -/// Processor to post (publish) a Wormhole message by setting up the message account for Guardian -/// observation. +/// Processor to post (publish) a Wormhole message by setting up the message account for +/// Guardian observation. /// -/// A message is either created beforehand using the new Anchor instructions `init_message_v1` and -/// `process_message_v1` or is created at this point. +/// A message is either created beforehand using the new Anchor instructions `init_message_v1` +/// and `process_message_v1` or is created at this point. fn post_message(ctx: Context, args: PostMessageArgs) -> Result<()> { if ctx.accounts.message.data_is_empty() { handle_post_new_message(ctx, args) @@ -157,40 +210,34 @@ fn handle_post_new_message(ctx: Context, args: PostMessageArgs) -> // Create the account. { - let data_len = PostedMessageV1::compute_size(payload.len()); - anchor_lang::system_program::create_account( - CpiContext::new( - ctx.accounts.system_program.to_account_info(), - anchor_lang::system_program::CreateAccount { - from: ctx.accounts.payer.to_account_info(), - to: ctx.accounts.message.to_account_info(), - }, - ), - Rent::get().map(|rent| rent.minimum_balance(data_len))?, - data_len.try_into().unwrap(), + utils::cpi::create_account( + ctx.accounts, + &ctx.accounts.message, + PostedMessageV1::compute_size(payload.len()), &crate::ID, + None, )?; } - let data = new_posted_message_data( + let info = new_posted_message_info( &mut ctx.accounts.config, &ctx.accounts.fee_collector, &mut ctx.accounts.emitter_sequence, commitment.into(), nonce, &ctx.accounts.emitter.as_ref().unwrap().key(), - payload, )?; // NOTE: The legacy instruction had the note "DO NOT REMOVE - CRITICAL OUTPUT". But we may be // able to remove this to save on compute units. - msg!("Sequence: {}", data.sequence); + msg!("Sequence: {}", info.sequence); - let msg_acc_data: &mut [u8] = &mut ctx.accounts.message.data.borrow_mut(); + let msg_acc_data: &mut [_] = &mut ctx.accounts.message.data.borrow_mut(); let mut writer = std::io::Cursor::new(msg_acc_data); // Finally set the `message` account with posted data. - LegacyAnchorized::from(PostedMessageV1 { data }).try_serialize(&mut writer)?; + LegacyAnchorized::from(PostedMessageV1::from(PostedMessageV1Data { info, payload })) + .try_serialize(&mut writer)?; // Done. Ok(()) @@ -215,33 +262,25 @@ fn handle_post_prepared_message(ctx: Context, args: PostMessageArgs CoreBridgeError::InvalidInstructionArgument ); - let (consistency_level, nonce, emitter, payload) = { - let acc_data = ctx.accounts.message.data.borrow(); - let msg = crate::zero_copy::PostedMessageV1::parse(&acc_data).unwrap(); + let (consistency_level, nonce, emitter) = { + let msg = crate::zero_copy::PostedMessageV1::load(&ctx.accounts.message).unwrap(); - ( - msg.consistency_level(), - msg.nonce(), - msg.emitter(), - msg.payload().to_vec(), - ) + (msg.consistency_level(), msg.nonce(), msg.emitter()) }; - let data = new_posted_message_data( + let info = new_posted_message_info( &mut ctx.accounts.config, &ctx.accounts.fee_collector, &mut ctx.accounts.emitter_sequence, consistency_level, nonce, &emitter, - payload, )?; - let msg_acc_data: &mut [u8] = &mut ctx.accounts.message.data.borrow_mut(); + let msg_acc_data: &mut [_] = &mut ctx.accounts.message.data.borrow_mut(); let mut writer = std::io::Cursor::new(msg_acc_data); - // Finally set the `message` account with posted data. - LegacyAnchorized::from(PostedMessageV1 { data }).try_serialize(&mut writer)?; + (PostedMessageV1::DISCRIMINATOR, info).serialize(&mut writer)?; // Done. Ok(()) @@ -252,24 +291,24 @@ fn handle_message_fee( config: &mut Account>, fee_collector: &Option, ) -> Result<()> { - match (config.fee_lamports, fee_collector) { - (0, _) => Ok(()), // Nothing to do. - (lamports, Some(fee_collector)) => { - let collector_lamports = fee_collector.to_account_info().lamports(); - require_eq!( - collector_lamports, - config.last_lamports.saturating_add(lamports), - CoreBridgeError::InsufficientFees - ); - - // Update core bridge config to reflect paid fees. - config.last_lamports = collector_lamports; - - // Done. - Ok(()) - } - _ => err!(ErrorCode::AccountNotEnoughKeys), + if config.fee_lamports > 0 { + let fee_collector = fee_collector + .as_ref() + .ok_or(error!(ErrorCode::AccountNotEnoughKeys))?; + + let collector_lamports = fee_collector.lamports(); + require_eq!( + collector_lamports, + config.last_lamports.saturating_add(config.fee_lamports), + CoreBridgeError::InsufficientFees + ); + + // Update core bridge config to reflect paid fees. + config.last_lamports = collector_lamports; } + + // Done. + Ok(()) } /// For posting a message, either a message has been prepared beforehand or this account is created @@ -282,10 +321,13 @@ fn handle_message_fee( /// PDA address is derived using the emitter, is assigned to the emitter signer (now called the /// emitter authority). Whereas with the new prepared message, this emitter can be taken from the /// message account to re-derive the emitter sequence PDA address. -fn find_emitter_for_sequence(emitter: &Option, msg: &AccountInfo) -> Result { - if msg.data_is_empty() { +fn find_emitter_for_sequence( + emitter: &Option, + msg_acc_info: &AccountInfo, +) -> Result { + if msg_acc_info.data_is_empty() { // Message must be a signer in order to be created. - require!(msg.is_signer, ErrorCode::AccountNotSigner); + require!(msg_acc_info.is_signer, ErrorCode::AccountNotSigner); // Because this message will be newly created in this instruction, the emitter is required // and must be a signer to authorize posting this message. @@ -296,8 +338,7 @@ fn find_emitter_for_sequence(emitter: &Option, msg: &AccountInfo) - Ok(emitter.key()) } else { - let msg_acc_data = msg.data.borrow(); - let msg = crate::zero_copy::PostedMessageV1::parse(&msg_acc_data)?; + let msg = crate::zero_copy::PostedMessageV1::load(msg_acc_info)?; match msg.status() { MessageStatus::Unset => err!(CoreBridgeError::MessageAlreadyPublished), diff --git a/solana/programs/core-bridge/src/legacy/processor/post_message/unreliable.rs b/solana/programs/core-bridge/src/legacy/processor/post_message/unreliable.rs index 6cc47ed7cc..abd92c9c0f 100644 --- a/solana/programs/core-bridge/src/legacy/processor/post_message/unreliable.rs +++ b/solana/programs/core-bridge/src/legacy/processor/post_message/unreliable.rs @@ -1,7 +1,8 @@ use crate::{ error::CoreBridgeError, legacy::{instruction::PostMessageArgs, utils::LegacyAnchorized}, - state::{Config, EmitterSequence, PostedMessageV1Unreliable}, + state::{Config, EmitterSequence, PostedMessageV1Data, PostedMessageV1Unreliable}, + zero_copy::LoadZeroCopy, }; use anchor_lang::prelude::*; @@ -51,7 +52,6 @@ pub struct PostMessageUnreliable<'info> { /// CHECK: Fee collector, which is used to update the [Config] account with the most up-to-date /// last lamports on this account. #[account( - mut, seeds = [crate::constants::FEE_COLLECTOR_SEED_PREFIX], bump, )] @@ -61,9 +61,6 @@ pub struct PostMessageUnreliable<'info> { _clock: UncheckedAccount<'info>, system_program: Program<'info, System>, - - /// CHECK: Previously needed sysvar. - _rent: UncheckedAccount<'info>, } impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, PostMessageArgs> @@ -72,7 +69,14 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, PostMessageArg const LOG_IX_NAME: &'static str = "LegacyPostMessageUnreliable"; const ANCHOR_IX_FN: fn(Context, PostMessageArgs) -> Result<()> = post_message_unreliable; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_post_message_account_infos(account_infos) + } } + impl<'info> PostMessageUnreliable<'info> { fn constraints(ctx: &Context) -> Result<()> { let msg = &ctx.accounts.message; @@ -92,10 +96,12 @@ impl<'info> PostMessageUnreliable<'info> { } } -/// Processor to post (publish) a Wormhole message by setting up the message account for Guardian -/// observation. This message account has either been created already or is created in this call. If -/// this message was already created, the emitter must be the same as the one encoded in the message -/// and the payload must be the same size. +/// Processor to post (publish) a Wormhole message by setting up the message account for +/// Guardian observation. This message account has either been created already or is created in +/// this call. +/// +/// If this message account already exists, the emitter must be the same as the one encoded in +/// the message and the payload must be the same size. #[access_control(PostMessageUnreliable::constraints(&ctx))] fn post_message_unreliable( ctx: Context, @@ -113,24 +119,23 @@ fn post_message_unreliable( CoreBridgeError::InvalidInstructionArgument ); - let data = super::new_posted_message_data( + let info = super::new_posted_message_info( &mut ctx.accounts.config, &ctx.accounts.fee_collector, &mut ctx.accounts.emitter_sequence, commitment.into(), nonce, &ctx.accounts.emitter.key(), - payload, )?; // NOTE: The legacy instruction had the note "DO NOT REMOVE - CRITICAL OUTPUT". But we may be // able to remove this to save on compute units. - msg!("Sequence: {}", data.sequence); + msg!("Sequence: {}", info.sequence); // Finally set the `message` account with posted data. ctx.accounts .message - .set_inner(PostedMessageV1Unreliable { data }.into()); + .set_inner(PostedMessageV1Unreliable::from(PostedMessageV1Data { info, payload }).into()); // Done. Ok(()) @@ -142,18 +147,22 @@ fn post_message_unreliable( /// payload. Instead of reverting with `ConstraintSpace`, we revert with a custom Core Bridge error /// saying that the payload size does not match the existing one (which is a requirement to reuse /// this message account). -fn try_compute_size(message: &AccountInfo, payload_size: u32) -> Result { +fn try_compute_size(msg_acc_info: &AccountInfo, payload_size: u32) -> Result { let payload_size = usize::try_from(payload_size).unwrap(); - if !message.data_is_empty() { - let expected_size = - crate::zero_copy::PostedMessageV1Unreliable::parse(&message.data.borrow())? - .payload_size(); - require_eq!( - payload_size, - expected_size, - CoreBridgeError::PayloadSizeMismatch - ); + if !msg_acc_info.data_is_empty() { + let msg = crate::zero_copy::MessageAccount::load(msg_acc_info)?; + + match msg.v1_unreliable() { + Some(inner) => { + require_eq!( + payload_size, + inner.payload_size(), + CoreBridgeError::PayloadSizeMismatch + ) + } + _ => return err!(ErrorCode::AccountDidNotDeserialize), + } } Ok(PostedMessageV1Unreliable::compute_size(payload_size)) diff --git a/solana/programs/core-bridge/src/legacy/processor/post_vaa.rs b/solana/programs/core-bridge/src/legacy/processor/post_vaa.rs index fdb3876e99..aebd01b86f 100644 --- a/solana/programs/core-bridge/src/legacy/processor/post_vaa.rs +++ b/solana/programs/core-bridge/src/legacy/processor/post_vaa.rs @@ -54,7 +54,10 @@ pub struct PostVaa<'info> { init, payer = payer, space = PostedVaaV1::compute_size(args.payload.len()), - seeds = [PostedVaaV1::SEED_PREFIX, signature_set.message_hash.as_ref()], + seeds = [ + PostedVaaV1::SEED_PREFIX, + signature_set.message_hash.as_ref() + ], bump, )] posted_vaa: Account<'info, LegacyAnchorized<4, PostedVaaV1>>, @@ -79,6 +82,13 @@ impl<'info> crate::legacy::utils::ProcessLegacyInstruction<'info, PostVaaArgs> f impl<'info> PostVaa<'info> { pub fn constraints(ctx: &Context, args: &PostVaaArgs) -> Result<()> { + // Check that the guardian set is still active. + let timestamp = Clock::get().map(Into::into)?; + require!( + ctx.accounts.guardian_set.is_active(×tamp), + CoreBridgeError::GuardianSetExpired + ); + let signature_set = &ctx.accounts.signature_set; require!( !INVALID_SIGNATURE_SET_KEYS.contains(&signature_set.key().to_string().as_str()), @@ -87,9 +97,8 @@ impl<'info> PostVaa<'info> { // Number of verified signatures in the signature set account must be at least quorum with // the guardian set. - require_gte!( - signature_set.num_verified(), - utils::quorum(ctx.accounts.guardian_set.keys.len()), + require!( + signature_set.num_verified() >= utils::quorum(ctx.accounts.guardian_set.keys.len()), CoreBridgeError::NoQuorum ); @@ -114,7 +123,7 @@ impl<'info> PostVaa<'info> { } } -/// Processor to write a validated VAA to a [PostedVaaV1] account. This instruction handler requires +/// Processor to write a verified VAA to a `PostedVaaV1` account. This instruction handler requires /// that the number of verified signers in the [SignatureSet] account is at least the quorum using /// the guardian set, whose index is encoded in this account. And the message hash in this account /// must agree with the recomputed one using this instruction handler's arguments. diff --git a/solana/programs/core-bridge/src/legacy/processor/verify_signatures.rs b/solana/programs/core-bridge/src/legacy/processor/verify_signatures.rs index a1806d6b69..4d238202b1 100644 --- a/solana/programs/core-bridge/src/legacy/processor/verify_signatures.rs +++ b/solana/programs/core-bridge/src/legacy/processor/verify_signatures.rs @@ -83,7 +83,7 @@ impl<'info> VerifySignatures<'info> { let timestamp = Clock::get().map(Into::into)?; require!( ctx.accounts.guardian_set.is_active(×tamp), - CoreBridgeError::PostVaaGuardianSetExpired + CoreBridgeError::GuardianSetExpired ); // Done. @@ -164,7 +164,7 @@ fn verify_signatures(ctx: Context, args: VerifySignaturesArgs) let signature_set = &mut ctx.accounts.signature_set; // If the signature set account has not been initialized yet, establish the expected account - // data (guardian set index used, hash and which indices have been validated). + // data (guardian set index used, hash and which indices have been verified). if signature_set.is_initialized() { // Otherwise, verify that the guardian set index is what we expect from // the last time we wrote to the signature set account. diff --git a/solana/programs/core-bridge/src/legacy/state/claim.rs b/solana/programs/core-bridge/src/legacy/state/claim.rs deleted file mode 100644 index a0de877041..0000000000 --- a/solana/programs/core-bridge/src/legacy/state/claim.rs +++ /dev/null @@ -1,25 +0,0 @@ -use anchor_lang::prelude::*; - -/// Account used to reflect a consumed VAA. This account is intended to be created once per VAA so -/// it can provide a protection against replay attacks for instructions that redeem VAAs that are -/// meant to only be consumed once. -/// -/// NOTE: This account's PDA seeds are inconsistent with how other Core Bridges save consumed VAAs. -/// This account uses a tuple of (emitter_chain, emitter_address, sequence) whereas other Core -/// Bridge implementations use the message digest (double keccak). - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] -pub struct Claim { - /// This member is not necessary, but we must preserve it since the legacy bridge assumes this - /// serialization for consumed VAAs (it is set to true when a VAA has been claimed). The fact - /// that this account exists at all should be enough to protect against a replay attack. - pub is_complete: bool, -} - -impl crate::legacy::utils::LegacyAccount<0> for Claim { - const DISCRIMINATOR: [u8; 0] = []; - - fn program_id() -> Pubkey { - crate::ID - } -} diff --git a/solana/programs/core-bridge/src/legacy/state/guardian_set.rs b/solana/programs/core-bridge/src/legacy/state/guardian_set.rs index e57e334313..a9b9dc807f 100644 --- a/solana/programs/core-bridge/src/legacy/state/guardian_set.rs +++ b/solana/programs/core-bridge/src/legacy/state/guardian_set.rs @@ -3,7 +3,8 @@ use anchor_lang::prelude::*; /// Account used to store a guardian set. The keys encoded in this account are Ethereum pubkeys. /// Its expiration time is determined at the time a guardian set is updated to a new set, where the -/// current network clock time is used with the Core Bridge's config `guardian_set_ttl`. +/// current network clock time is used with +/// [guardian_set_ttl](crate::state::Config::guardian_set_ttl). #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] pub struct GuardianSet { /// Index representing an incrementing version number for this guardian set. diff --git a/solana/programs/core-bridge/src/legacy/state/mod.rs b/solana/programs/core-bridge/src/legacy/state/mod.rs index 8961d61e39..482d7efbe5 100644 --- a/solana/programs/core-bridge/src/legacy/state/mod.rs +++ b/solana/programs/core-bridge/src/legacy/state/mod.rs @@ -1,8 +1,5 @@ //! Account schemas for the Core Bridge Program. -mod claim; -pub use claim::*; - mod config; pub use config::*; @@ -15,9 +12,6 @@ pub use guardian_set::*; mod posted_message_v1; pub use posted_message_v1::*; -mod posted_message_v1_unreliable; -pub use posted_message_v1_unreliable::*; - mod posted_vaa_v1; pub use posted_vaa_v1::*; diff --git a/solana/programs/core-bridge/src/legacy/state/posted_message_v1.rs b/solana/programs/core-bridge/src/legacy/state/posted_message_v1/mod.rs similarity index 94% rename from solana/programs/core-bridge/src/legacy/state/posted_message_v1.rs rename to solana/programs/core-bridge/src/legacy/state/posted_message_v1/mod.rs index 35ca0460ae..cb3d8abca1 100644 --- a/solana/programs/core-bridge/src/legacy/state/posted_message_v1.rs +++ b/solana/programs/core-bridge/src/legacy/state/posted_message_v1/mod.rs @@ -1,3 +1,6 @@ +mod unreliable; +pub use unreliable::*; + use std::ops::{Deref, DerefMut}; use crate::types::{ChainIdSolanaOnly, Timestamp}; @@ -5,7 +8,7 @@ use anchor_lang::prelude::*; pub const POSTED_MESSAGE_V1_DISCRIMINATOR: [u8; 4] = *b"msg\x00"; -/// Status of a message. When a message is posetd, its status is `Unset`. +/// Status of a message. When a message is posetd, its status is [Unset](MessageStatus::Unset). #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] pub enum MessageStatus { Unset, @@ -104,6 +107,12 @@ impl PostedMessageV1 { } } +impl From for PostedMessageV1 { + fn from(value: PostedMessageV1Data) -> Self { + Self { data: value } + } +} + impl crate::legacy::utils::LegacyAccount<4> for PostedMessageV1 { const DISCRIMINATOR: [u8; 4] = POSTED_MESSAGE_V1_DISCRIMINATOR; diff --git a/solana/programs/core-bridge/src/legacy/state/posted_message_v1_unreliable.rs b/solana/programs/core-bridge/src/legacy/state/posted_message_v1/unreliable.rs similarity index 84% rename from solana/programs/core-bridge/src/legacy/state/posted_message_v1_unreliable.rs rename to solana/programs/core-bridge/src/legacy/state/posted_message_v1/unreliable.rs index 68d07ecfb9..f8f2a2f6f7 100644 --- a/solana/programs/core-bridge/src/legacy/state/posted_message_v1_unreliable.rs +++ b/solana/programs/core-bridge/src/legacy/state/posted_message_v1/unreliable.rs @@ -1,8 +1,9 @@ use std::ops::{Deref, DerefMut}; -use crate::state::PostedMessageV1Data; use anchor_lang::prelude::*; +use super::PostedMessageV1Data; + pub const POSTED_MESSAGE_V1_UNRELIABLE_DISCRIMINATOR: [u8; 4] = *b"msu\x00"; /// Account used to store a published (reusable) Wormhole message. @@ -25,6 +26,12 @@ impl PostedMessageV1Unreliable { } } +impl From for PostedMessageV1Unreliable { + fn from(value: PostedMessageV1Data) -> Self { + Self { data: value } + } +} + impl Deref for PostedMessageV1Unreliable { type Target = PostedMessageV1Data; diff --git a/solana/programs/core-bridge/src/legacy/state/posted_vaa_v1.rs b/solana/programs/core-bridge/src/legacy/state/posted_vaa_v1.rs index 2d5bd82c42..75155496d0 100644 --- a/solana/programs/core-bridge/src/legacy/state/posted_vaa_v1.rs +++ b/solana/programs/core-bridge/src/legacy/state/posted_vaa_v1.rs @@ -16,12 +16,13 @@ pub struct PostedVaaV1Info { /// Time the message was submitted. pub timestamp: Timestamp, - /// Pubkey of `SignatureSet` account that represent this VAA's signature verification. + /// Pubkey of [SignatureSet](crate::state::SignatureSet) account that represents this VAA's + /// signature verification. pub signature_set: Pubkey, - /// Guardian set index used to verify signatures for `SignatureSet`. + /// Guardian set index used to verify signatures for [SignatureSet](crate::state::SignatureSet). /// - /// NOTE: In the previous implementation, this member was referred to as the `posted_timestamp`, + /// NOTE: In the previous implementation, this member was referred to as the "posted timestamp", /// which is zero for VAA data (posted messages and VAAs resemble the same account schema). By /// changing this to the guardian set index, we patch a bug with verifying governance VAAs for /// the Core Bridge (other Core Bridge implementations require that the guardian set that diff --git a/solana/programs/core-bridge/src/legacy/utils/mod.rs b/solana/programs/core-bridge/src/legacy/utils/mod.rs index 5b9eb39557..9b46713239 100644 --- a/solana/programs/core-bridge/src/legacy/utils/mod.rs +++ b/solana/programs/core-bridge/src/legacy/utils/mod.rs @@ -72,14 +72,10 @@ where T: LegacyAccount, { fn try_serialize(&self, writer: &mut W) -> Result<()> { - if writer.write_all(&T::DISCRIMINATOR).is_err() { - return err!(ErrorCode::AccountDidNotSerialize); - } - - if AnchorSerialize::serialize(&self.0, writer).is_err() { - return err!(ErrorCode::AccountDidNotSerialize); - } - Ok(()) + writer + .write_all(&T::DISCRIMINATOR) + .and_then(|_| self.0.serialize(writer)) + .map_err(|_| error!(ErrorCode::AccountDidNotSerialize)) } } @@ -99,7 +95,7 @@ where } fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { - let mut data: &[u8] = &buf[N..]; + let mut data = &buf[N..]; Ok(Self(T::deserialize(&mut data)?)) } } @@ -117,13 +113,54 @@ pub trait ProcessLegacyInstruction<'info, T: AnchorDeserialize>: /// program. This method gets invoked in the process instruction method. const ANCHOR_IX_FN: fn(Context, T) -> Result<()>; + /// This method is used to order the accounts in the same order as the Anchorized account + /// contexts. In the legacy implementation, some accounts were not required to be defined in any + /// context, and were passed in sort of like how remaining accounts work in Anchor. + /// + /// For example, in the post message instruction, the Anchor context orders the accounts as: + /// + /// 1. `config` + /// 2. `message` + /// 3. `emitter` + /// 4. `emitter_sequence` + /// 5. `payer` + /// 6. `fee_collector` + /// 7. `clock` + /// 8. `system_program` + /// + /// In the legacy implementation, accounts only up through the `clock` sysvar were defined in + /// an account context (meaning that the accounts relevant to the business logic were defined + /// with a specific order). + /// + /// There were actually two accounts that were required with the legacy post message + /// instruction: + /// + /// 8. System program + /// 9. Rent sysvar. + /// + /// These two accounts could have been passed into an instruction in any order (so the System + /// program can either be #8 or #9 in the instruction's account metas). Because integrators + /// composing with these legacy implementations may be passing in these accounts in any sort of + /// order, this method will make sure that any account after the last ordered account. So in + /// this example, making sure the System program is #9 (and not caring about where Rent ends up + /// because it is not needed anymore). + /// + /// Ordering matters because Anchor requires that all the necessary accounts are defined in its + /// account contexts. So this includes the System program (whereas with the legacy + /// implementation did not require this to be defined in its context). + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + Ok(account_infos.to_vec()) + } + /// This method implements the same procedure Anchor performs in its codegen, where it creates /// a Context using the account context and invokes the instruction handler with the handler's /// arguments. It then performs clean up at the end by writing the account data back into the /// borrowed account data via exit. fn process_instruction( program_id: &Pubkey, - mut account_infos: &[AccountInfo<'info>], + account_infos: &[AccountInfo<'info>], mut ix_data: &[u8], ) -> Result<()> { #[cfg(not(feature = "no-log-ix-name"))] @@ -131,6 +168,8 @@ pub trait ProcessLegacyInstruction<'info, T: AnchorDeserialize>: let mut bumps = std::collections::BTreeMap::new(); + let mut account_infos: &[_] = &Self::order_account_infos(account_infos)?; + // Generate accounts struct. This checks account constraints, including PDAs. let mut accounts = Self::try_accounts( program_id, @@ -141,7 +180,7 @@ pub trait ProcessLegacyInstruction<'info, T: AnchorDeserialize>: )?; // Create new context of these accounts. - let ctx = Context::new(program_id, &mut accounts, &[], bumps); + let ctx = Context::new(program_id, &mut accounts, account_infos, bumps); // Execute method that takes this context with specified instruction arguments. Self::ANCHOR_IX_FN(ctx, T::deserialize(&mut ix_data)?)?; diff --git a/solana/programs/core-bridge/src/lib.rs b/solana/programs/core-bridge/src/lib.rs index 814c8edb83..b74024a5ef 100644 --- a/solana/programs/core-bridge/src/lib.rs +++ b/solana/programs/core-bridge/src/lib.rs @@ -19,10 +19,6 @@ pub mod legacy; mod processor; pub(crate) use processor::*; -pub use processor::{ - ClosePostedVaaV1Directive, InitMessageV1Args, PostVaaV1Directive, ProcessEncodedVaaDirective, - ProcessMessageV1Directive, -}; pub mod sdk; @@ -30,9 +26,9 @@ pub mod state; pub mod types; -pub mod utils; +pub(crate) mod utils; -pub mod zero_copy; +pub(crate) mod zero_copy; use anchor_lang::prelude::*; @@ -44,8 +40,9 @@ pub mod wormhole_core_bridge_solana { /// account for writing. The emitter authority is established at this point and the payload size /// is inferred from the size of the created account. This instruction handler also allows an /// integrator to publish Wormhole messages using his program's ID as the emitter address - /// (by passing `Some(crate::ID)` to the `cpi_program_id` argument). **Be aware that the emitter - /// authority's seeds must only be [b"emitter"] in this case.** + /// (by passing `Some(crate::ID)` to the [cpi_program_id](InitMessageV1Args::cpi_program_id) + /// argument). **Be aware that the emitter authority's seeds must only be \[b"emitter"\] in this + /// case.** /// /// This instruction should be followed up with `process_message_v1` to write and finalize the /// message account (to prepare it for publishing via the @@ -58,14 +55,27 @@ pub mod wormhole_core_bridge_solana { processor::init_message_v1(ctx, args) } + /// Processor used to write to a draft [PostedMessageV1](crate::state::PostedMessageV1) account. + /// This instruction requires an authority (the emitter authority) to interact with the message + /// account. + pub fn write_message_v1(ctx: Context, args: WriteMessageV1Args) -> Result<()> { + processor::write_message_v1(ctx, args) + } + + /// Processor used to finalize a draft [PostedMessageV1](crate::state::PostedMessageV1) account. + /// Once finalized, this message account cannot be written to again. A finalized message is the + /// only state the legacy post message instruction can accept before publishing. This + /// instruction requires an authority (the emitter authority) to interact with the message + /// account. + pub fn finalize_message_v1(ctx: Context) -> Result<()> { + processor::finalize_message_v1(ctx) + } + /// Processor used to process a draft [PostedMessageV1](crate::state::PostedMessageV1) account. /// This instruction requires an authority (the emitter authority) to interact with the message /// account. - pub fn process_message_v1( - ctx: Context, - directive: ProcessMessageV1Directive, - ) -> Result<()> { - processor::process_message_v1(ctx, directive) + pub fn close_message_v1(ctx: Context) -> Result<()> { + processor::close_message_v1(ctx) } /// Processor used to intialize a created account as [EncodedVaa](crate::state::EncodedVaa). An @@ -74,14 +84,27 @@ pub mod wormhole_core_bridge_solana { processor::init_encoded_vaa(ctx) } - /// Processor used to process an [EncodedVaa](crate::state::EncodedVaa) account. This + /// Processor used to close an [EncodedVaa](crate::state::EncodedVaa). This instruction requires + /// an authority (the write authority) to interact witht he encoded VAA account. + pub fn close_encoded_vaa(ctx: Context) -> Result<()> { + processor::close_encoded_vaa(ctx) + } + + /// Processor used to write to an [EncodedVaa](crate::state::EncodedVaa) account. This /// instruction requires an authority (the write authority) to interact with the encoded VAA /// account. - pub fn process_encoded_vaa( - ctx: Context, - directive: ProcessEncodedVaaDirective, + pub fn write_encoded_vaa( + ctx: Context, + args: WriteEncodedVaaArgs, ) -> Result<()> { - processor::process_encoded_vaa(ctx, directive) + processor::write_encoded_vaa(ctx, args) + } + + /// Processor used to verify an [EncodedVaa](crate::state::EncodedVaa) account as a version 1 + /// VAA (guardian signatures attesting to this observation). This instruction requires an + /// authority (the write authority) to interact with the encoded VAA account. + pub fn verify_encoded_vaa_v1(ctx: Context) -> Result<()> { + processor::verify_encoded_vaa_v1(ctx) } /// Processor used to close an [EncodedVaa](crate::state::EncodedVaa) account to create a @@ -90,18 +113,15 @@ pub mod wormhole_core_bridge_solana { /// NOTE: Because the legacy verify signatures instruction was not required for the Posted VAA /// account to exist, the encoded [SignatureSet](crate::state::SignatureSet) is the default /// [Pubkey]. - pub fn post_vaa_v1(ctx: Context, directive: PostVaaV1Directive) -> Result<()> { - processor::post_vaa_v1(ctx, directive) + pub fn post_vaa_v1(ctx: Context) -> Result<()> { + processor::post_vaa_v1(ctx) } /// Processor used to close a [PostedMessageV1](crate::state::PostedMessageV1) account. If a /// [SignatureSet](crate::state::SignatureSet) were used to verify the VAA, that account will be /// closed, too. - pub fn close_posted_vaa_v1( - ctx: Context, - directive: ClosePostedVaaV1Directive, - ) -> Result<()> { - processor::close_posted_vaa_v1(ctx, directive) + pub fn close_posted_vaa_v1(ctx: Context) -> Result<()> { + processor::close_posted_vaa_v1(ctx) } /// Process legacy Core Bridge instructions. See [legacy](crate::legacy) for more info. diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_encoded_vaa.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_encoded_vaa.rs new file mode 100644 index 0000000000..6d9b02f324 --- /dev/null +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_encoded_vaa.rs @@ -0,0 +1,36 @@ +use crate::{error::CoreBridgeError, zero_copy::EncodedVaa}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseEncodedVaa<'info> { + /// This account is only required to be mutable for the `CloseVaaAccount` directive. This + /// authority is the same signer that originally created the VAA accounts, so he is the one that + /// will receive the lamports back for the closed accounts. + #[account(mut)] + write_authority: Signer<'info>, + + /// CHECK: The encoded VAA account, which stores the VAA buffer. This buffer must first be + /// written to and then verified. + #[account(mut)] + encoded_vaa: AccountInfo<'info>, +} + +impl<'info> CloseEncodedVaa<'info> { + fn constraints(ctx: &Context) -> Result<()> { + // Check write authority. + let vaa = EncodedVaa::parse_unverified(&ctx.accounts.encoded_vaa)?; + require_keys_eq!( + ctx.accounts.write_authority.key(), + vaa.write_authority(), + CoreBridgeError::WriteAuthorityMismatch + ); + + // Done. + Ok(()) + } +} + +#[access_control(CloseEncodedVaa::constraints(&ctx))] +pub fn close_encoded_vaa(ctx: Context) -> Result<()> { + crate::utils::close_account(&ctx.accounts.encoded_vaa, &ctx.accounts.write_authority) +} diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_posted_vaa_v1.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_posted_vaa_v1.rs index bcc51f0313..b6dcc4e649 100644 --- a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_posted_vaa_v1.rs +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/close_posted_vaa_v1.rs @@ -10,11 +10,12 @@ pub struct ClosePostedVaaV1<'info> { #[account(mut)] sol_destination: Signer<'info>, + /// Posted VAA. + /// + /// NOTE: Account will attempt to deserialize discriminator so there is no need to check seeds. #[account( mut, close = sol_destination, - seeds = [PostedVaaV1::SEED_PREFIX, posted_vaa.message_hash().as_ref()], - bump )] posted_vaa: Account<'info, LegacyAnchorized<4, PostedVaaV1>>, @@ -25,24 +26,7 @@ pub struct ClosePostedVaaV1<'info> { signature_set: Option>>, } -/// This directive acts as a placeholder in case we want to expand how posted VAAs are closed. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum ClosePostedVaaV1Directive { - TryOnce, -} - -pub fn close_posted_vaa_v1( - ctx: Context, - directive: ClosePostedVaaV1Directive, -) -> Result<()> { - match directive { - ClosePostedVaaV1Directive::TryOnce => try_once(ctx), - } -} - -fn try_once(ctx: Context) -> Result<()> { - msg!("Directive: TryOnce"); - +pub fn close_posted_vaa_v1(ctx: Context) -> Result<()> { let verified_signature_set = ctx.accounts.posted_vaa.signature_set; match &ctx.accounts.signature_set { Some(signature_set) => { diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/init_encoded_vaa.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/init_encoded_vaa.rs index fd60930999..ec0b965c77 100644 --- a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/init_encoded_vaa.rs +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/init_encoded_vaa.rs @@ -1,10 +1,9 @@ -use std::io::Read; - use crate::{ error::CoreBridgeError, - state::{EncodedVaa, Header, ProcessingStatus}, + state::{Header, ProcessingStatus}, + zero_copy::EncodedVaa, }; -use anchor_lang::{prelude::*, Discriminator}; +use anchor_lang::prelude::*; #[derive(Accounts)] pub struct InitEncodedVaa<'info> { @@ -22,26 +21,20 @@ pub struct InitEncodedVaa<'info> { impl<'info> InitEncodedVaa<'info> { fn constraints(ctx: &Context) -> Result<()> { - // Checking that the message account is completely zeroed out. By doing this, we make the - // assumption that no other Core Bridge account that is currently used will have all zeros. - // Ideally all of the Core Bridge accounts should have a discriminator so we do not have to - // mess around like this. But here we are. - let msg_acc_data: &[u8] = &ctx.accounts.encoded_vaa.try_borrow_data()?; - let mut reader = std::io::Cursor::new(msg_acc_data); - // The size of the created account must be more than the size of discriminator and header // (some VAA buffer > 0 bytes). - require_gt!( - ctx.accounts.encoded_vaa.data_len(), - EncodedVaa::BYTES_START, + require!( + ctx.accounts.encoded_vaa.data_len() > EncodedVaa::VAA_START, CoreBridgeError::InvalidCreatedAccountSize ); - // All of the discriminator + header bytes + the 4-byte payload length should be zero. - let mut zeros = [0; EncodedVaa::BYTES_START]; - reader.read_exact(&mut zeros)?; + // Checking that the message account is completely zeroed out. By doing this, we make the + // assumption that no other Core Bridge account that is currently used will have all zeros. + // Ideally all of the Core Bridge accounts should have a discriminator so we do not have to + // mess around like this. But here we are. + let msg_acc_data: &[_] = &ctx.accounts.encoded_vaa.try_borrow_data()?; require!( - zeros == [0; EncodedVaa::BYTES_START], + msg_acc_data[..EncodedVaa::VAA_START] == [0; EncodedVaa::VAA_START], CoreBridgeError::AccountNotZeroed ); @@ -52,27 +45,22 @@ impl<'info> InitEncodedVaa<'info> { #[access_control(InitEncodedVaa::constraints(&ctx))] pub fn init_encoded_vaa(ctx: Context) -> Result<()> { - let vaa_len = ctx.accounts.encoded_vaa.data_len() - EncodedVaa::BYTES_START; + let vaa_len = ctx.accounts.encoded_vaa.data_len() - EncodedVaa::VAA_START; - let acc_data: &mut [u8] = &mut ctx.accounts.encoded_vaa.data.borrow_mut(); + let acc_data: &mut [_] = &mut ctx.accounts.encoded_vaa.data.borrow_mut(); let mut writer = std::io::Cursor::new(acc_data); // Finally initialize the encoded VAA account by serializing the discriminator, header and // expected VAA length. - // - // NOTE: This account layout does not match any account found in the state directory. Only the - // discriminator and header will match the `VaaV1` account (which is how this account will be - // serialized once the encoded VAA has finished processing). - EncodedVaa::DISCRIMINATOR.serialize(&mut writer)?; - Header { - status: ProcessingStatus::Writing, - write_authority: ctx.accounts.write_authority.key(), - version: Default::default(), - } - .serialize(&mut writer)?; - - u32::try_from(vaa_len) - .unwrap() + ( + EncodedVaa::DISC, + Header { + status: ProcessingStatus::Writing, + write_authority: ctx.accounts.write_authority.key(), + version: Default::default(), + }, + u32::try_from(vaa_len).unwrap(), + ) .serialize(&mut writer) .map_err(Into::into) } diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/mod.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/mod.rs index 1b0e93ac71..34c2b0edee 100644 --- a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/mod.rs +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/mod.rs @@ -1,9 +1,17 @@ -mod close_posted_vaa_v1; -mod init_encoded_vaa; -mod post_vaa_v1; -mod process_encoded_vaa; +mod close_encoded_vaa; +pub use close_encoded_vaa::*; +mod close_posted_vaa_v1; pub use close_posted_vaa_v1::*; + +mod init_encoded_vaa; pub use init_encoded_vaa::*; + +mod post_vaa_v1; pub use post_vaa_v1::*; -pub use process_encoded_vaa::*; + +mod verify_encoded_vaa_v1; +pub use verify_encoded_vaa_v1::*; + +mod write_encoded_vaa; +pub use write_encoded_vaa::*; diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/post_vaa_v1.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/post_vaa_v1.rs index cbccb63e3e..9dc2ed5c3c 100644 --- a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/post_vaa_v1.rs +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/post_vaa_v1.rs @@ -1,31 +1,26 @@ use crate::{ error::CoreBridgeError, legacy::utils::LegacyAnchorized, - state::{EncodedVaa, PostedVaaV1, PostedVaaV1Info, ProcessingStatus}, - types::VaaVersion, + state::{PostedVaaV1, PostedVaaV1Info}, + zero_copy::{self, LoadZeroCopy}, }; use anchor_lang::prelude::*; -use wormhole_raw_vaas::Vaa; #[derive(Accounts)] pub struct PostVaaV1<'info> { #[account(mut)] write_authority: Signer<'info>, - #[account( - mut, - has_one = write_authority, - close = write_authority, - )] - vaa: Account<'info, EncodedVaa>, + /// CHECK: Encoded VAA, whose body will be serialized into the posted VAA account. + encoded_vaa: AccountInfo<'info>, #[account( init, payer = write_authority, - space = PostedVaaV1::compute_size(vaa.v1()?.body().payload().as_ref().len()), + space = try_compute_size(&encoded_vaa)?, seeds = [ PostedVaaV1::SEED_PREFIX, - solana_program::keccak::hash(vaa.v1()?.body().as_ref()).as_ref(), + try_message_hash(&encoded_vaa)?.as_ref(), ], bump, )] @@ -34,52 +29,13 @@ pub struct PostVaaV1<'info> { system_program: Program<'info, System>, } -impl<'info> PostVaaV1<'info> { - fn constraints(ctx: &Context) -> Result<()> { - // We can only create a legacy VAA account if the VAA account was verified. - // - // NOTE: We are keeping this here as a fail safe. We should never reach this point because - // if the VAA is unverified, its version will not be set (so the account context zero-copy - // getters before this access control will fail). - require!( - ctx.accounts.vaa.status == ProcessingStatus::Verified, - CoreBridgeError::UnverifiedVaa - ); - - // Done. - Ok(()) - } -} - -/// This directive acts as a placeholder in case we want to expand how VAAs are posted. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum PostVaaV1Directive { - TryOnce, -} - -#[access_control(PostVaaV1::constraints(&ctx))] -pub fn post_vaa_v1(ctx: Context, directive: PostVaaV1Directive) -> Result<()> { - match directive { - PostVaaV1Directive::TryOnce => try_once(ctx), - } -} - -fn try_once(ctx: Context) -> Result<()> { - msg!("Directive: TryOnce"); - - let encoded_vaa = &ctx.accounts.vaa; - - // This is safe because the VAA integrity has already been verified. - let vaa = Vaa::parse(&encoded_vaa.buf).unwrap(); +pub fn post_vaa_v1(ctx: Context) -> Result<()> { + // This is safe because we checked that the VAA version in the encoded VAA account is V1. + let encoded_vaa = zero_copy::EncodedVaa::load(&ctx.accounts.encoded_vaa).unwrap(); + let vaa = encoded_vaa.as_vaa().unwrap(); + let v1 = vaa.v1().unwrap(); - // Verify version. - require_eq!( - vaa.version(), - u8::from(VaaVersion::V1), - CoreBridgeError::InvalidVaaVersion - ); - - let body = vaa.body(); + let body = v1.body(); ctx.accounts.posted_vaa.set_inner( PostedVaaV1 { @@ -87,7 +43,7 @@ fn try_once(ctx: Context) -> Result<()> { consistency_level: body.consistency_level(), timestamp: body.timestamp().into(), signature_set: Default::default(), - guardian_set_index: vaa.guardian_set_index(), + guardian_set_index: v1.guardian_set_index(), nonce: body.nonce(), sequence: body.sequence(), emitter_chain: body.emitter_chain(), @@ -101,3 +57,36 @@ fn try_once(ctx: Context) -> Result<()> { // Done. Ok(()) } + +fn try_compute_size(vaa: &AccountInfo) -> Result { + let encoded_vaa = zero_copy::EncodedVaa::load(vaa)?; + let payload_size = encoded_vaa + .as_vaa()? + .v1() + .ok_or(error!(CoreBridgeError::InvalidVaaVersion))? + .body() + .payload() + .as_ref() + .len(); + + let acc_size = PostedVaaV1::compute_size(payload_size); + + // CPI to create the posted VAA account will fail if the size of the VAA payload is too large. + require!( + acc_size <= 10_240, + CoreBridgeError::PostedVaaPayloadTooLarge + ); + + Ok(acc_size) +} + +fn try_message_hash(vaa: &AccountInfo) -> Result { + let encoded_vaa = zero_copy::EncodedVaa::load(vaa)?; + let vaa = encoded_vaa.as_vaa()?; + let body = vaa + .v1() + .ok_or(error!(CoreBridgeError::InvalidVaaVersion))? + .body(); + + Ok(solana_program::keccak::hash(body.as_ref())) +} diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/process_encoded_vaa.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/process_encoded_vaa.rs deleted file mode 100644 index 3c2bf74914..0000000000 --- a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/process_encoded_vaa.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::{ - error::CoreBridgeError, - legacy::utils::LegacyAnchorized, - state::{GuardianSet, Header, ProcessingStatus}, - types::VaaVersion, - zero_copy::EncodedVaa, -}; -use anchor_lang::prelude::*; -use solana_program::{keccak, secp256k1_recover::secp256k1_recover}; -use wormhole_raw_vaas::GuardianSetSig; - -#[derive(Accounts)] -pub struct ProcessEncodedVaa<'info> { - /// This account is only required to be mutable for the `CloseVaaAccount` directive. This - /// authority is the same signer that originally created the VAA accounts, so he is the one that - /// will receive the lamports back for the closed accounts. - #[account(mut)] - write_authority: Signer<'info>, - - /// CHECK: We do not deserialize this account as `VaaV1` because allocating heap memory in its - /// deserialization uses significant compute units with every call to this instruction handler. - /// For large VAAs, this can be a significant cost. - /// - /// This instruction handler performs the same checks Anchor performs: - /// - Discriminator check (found in `AccountDeserialize`). - /// - Write authority check (via `has_one`). - #[account( - mut, - owner = crate::ID - )] - encoded_vaa: AccountInfo<'info>, - - /// The guardian set account is optional because it is only needed for the signature verification - /// instruction handler directive. - /// - /// NOTE: Because the vaa account is not deserialized as an Anchor Account, we cannot use the - /// guardian_set_index in `VaaV1` here easily. Instead we check that the guardian set index - /// matches once the VAA is verified via the `VerifySignaturesV1` directive. - #[account( - seeds = [GuardianSet::SEED_PREFIX, &guardian_set.index.to_be_bytes()], - bump, - )] - guardian_set: Option>>, -} - -impl<'info> ProcessEncodedVaa<'info> { - fn constraints(ctx: &Context) -> Result<()> { - if let Some(guardian_set) = &ctx.accounts.guardian_set { - // Guardian set must be active. - let timestamp = Clock::get().map(Into::into)?; - require!( - guardian_set.is_active(×tamp), - CoreBridgeError::GuardianSetExpired - ); - } - - // Check write authority. - let acc_data = ctx.accounts.encoded_vaa.try_borrow_data()?; - let vaa = EncodedVaa::parse_unverified(&acc_data)?; - require_keys_eq!( - ctx.accounts.write_authority.key(), - vaa.write_authority(), - CoreBridgeError::WriteAuthorityMismatch - ); - - // Done. - Ok(()) - } -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum ProcessEncodedVaaDirective { - /// Close the VAA processing accounts. Either someone decides to close the VAA account before it - /// has been verified, or the VAA has been verified by an integrating app and is no longer - /// needed. - CloseVaaAccount, - /// Write input data from VAA, indicated by the index of the encoded VAA. - Write { - /// Index of encoded VAA. - index: u32, - /// Data representing the encoded VAA starting at specified index. - data: Vec, - }, - /// When the whole VAA is written to the vaa account, its message hash is computed and guardian - /// signatures are verified. Invoking this directive will mark the VAA as verified. - /// - /// NOTE: The guardian set is a required account in order to perform this method. - VerifySignaturesV1, -} - -#[access_control(ProcessEncodedVaa::constraints(&ctx))] -pub fn process_encoded_vaa( - ctx: Context, - directive: ProcessEncodedVaaDirective, -) -> Result<()> { - match directive { - ProcessEncodedVaaDirective::CloseVaaAccount => close_vaa_account(ctx), - ProcessEncodedVaaDirective::Write { index, data } => write(ctx, index, data), - ProcessEncodedVaaDirective::VerifySignaturesV1 => verify_signatures_v1(ctx), - } -} - -fn close_vaa_account(ctx: Context) -> Result<()> { - msg!("Directive: CloseVaaAccount"); - - crate::utils::close_account( - ctx.accounts.encoded_vaa.to_account_info(), - ctx.accounts.write_authority.to_account_info(), - ) -} - -fn write(ctx: Context, index: u32, data: Vec) -> Result<()> { - require!( - !data.is_empty(), - CoreBridgeError::InvalidInstructionArgument - ); - - let vaa_size: usize = { - let acc_data = ctx.accounts.encoded_vaa.data.borrow(); - let vaa = EncodedVaa::parse_unverified(&acc_data)?; - require!( - vaa.status() == ProcessingStatus::Writing, - CoreBridgeError::NotInWritingStatus - ); - - vaa.vaa_size() - }; - - let index = usize::try_from(index).unwrap(); - let end = index.saturating_add(data.len()); - require_gte!(vaa_size, end, CoreBridgeError::DataOverflow); - - const START: usize = 8 + EncodedVaa::VAA_START; - let acc_data: &mut [u8] = &mut ctx.accounts.encoded_vaa.data.borrow_mut(); - acc_data[(START + index)..(START + end)].copy_from_slice(&data); - - // Done. - Ok(()) -} - -fn verify_signatures_v1(ctx: Context) -> Result<()> { - msg!("Directive: VerifySignaturesV1"); - - require!( - ctx.accounts.guardian_set.is_some(), - ErrorCode::AccountNotEnoughKeys - ); - - let guardian_set = ctx.accounts.guardian_set.as_ref().unwrap(); - - let write_authority = { - let acc_data = ctx.accounts.encoded_vaa.data.borrow(); - let encoded_vaa = EncodedVaa::parse_unverified(&acc_data)?; - require!( - encoded_vaa.status() == ProcessingStatus::Writing, - CoreBridgeError::VaaAlreadyVerified - ); - - // Parse and verify. - let vaa = encoded_vaa.v1_unverified()?; - - // Must be V1. - require_eq!( - vaa.version(), - u8::from(VaaVersion::V1), - CoreBridgeError::InvalidVaaVersion - ); - - // Make sure the encoded guardian set index agrees with the guardian set account's index. - require_eq!( - vaa.guardian_set_index(), - guardian_set.index, - CoreBridgeError::GuardianSetMismatch - ); - - // Do we have enough signatures for quorum? - let guardian_keys = &guardian_set.keys; - let quorum = crate::utils::quorum(guardian_keys.len()); - require_gte!( - usize::from(vaa.signature_count()), - quorum, - CoreBridgeError::NoQuorum - ); - - // Generate the same message hash (using keccak) that the Guardians used to generate their - // signatures. This message hash will be hashed again to produce the digest for - // `secp256k1_recover`. - let digest = keccak::hash(keccak::hash(vaa.body().as_ref()).as_ref()); - - // Only verify as many as we need (up to quorum). - let mut last_guardian_index = None; - let mut num_verified = 0; - for sig in vaa.signatures() { - // We do not allow for non-increasing guardian signature indices. - let index = usize::from(sig.guardian_index()); - if let Some(last_index) = last_guardian_index { - require_gt!(index, last_index, CoreBridgeError::InvalidGuardianIndex); - } - - // Does this guardian index exist in this guardian set? - let guardian_pubkey = guardian_keys - .get(index) - .ok_or_else(|| error!(CoreBridgeError::InvalidGuardianIndex))?; - - // Now verify that the signature agrees with the expected Guardian's pubkey. - verify_guardian_signature(&sig, guardian_pubkey, digest.as_ref())?; - num_verified += 1; - - // If we have reached quorum, no need to spend compute units to verify other signatures. - if num_verified == quorum { - break; - } - - last_guardian_index = Some(index); - } - - encoded_vaa.write_authority() - }; - - // Skip discriminator. - let acc_data: &mut [u8] = &mut ctx.accounts.encoded_vaa.data.borrow_mut()[8..]; - let mut writer = std::io::Cursor::new(acc_data); - Header { - status: ProcessingStatus::Verified, - write_authority, - version: VaaVersion::V1, - } - .serialize(&mut writer) - .map_err(Into::into) -} - -fn verify_guardian_signature( - sig: &GuardianSetSig, - guardian_pubkey: &[u8; 20], - digest: &[u8], -) -> Result<()> { - // Recover using `solana_program::secp256k1_recover`. Public key recovery costs 25k compute - // units. And hashing this public key to recover the Ethereum public key costs about 13k. - let recovered = { - // Recover EC public key (64 bytes). - let pubkey = secp256k1_recover(digest, sig.recovery_id(), &sig.rs()) - .map_err(|_| CoreBridgeError::InvalidSignature)?; - - // The Ethereum public key is the last 20 bytes of keccak hashed public key above. - let hashed = keccak::hash(&pubkey.to_bytes()); - - let mut eth_pubkey = [0; 20]; - eth_pubkey.copy_from_slice(&hashed.0[12..]); - - eth_pubkey - }; - - // The recovered public key should agree with the Guardian's public key at this index. - require!( - recovered == *guardian_pubkey, - CoreBridgeError::InvalidGuardianKeyRecovery - ); - - // Done. - Ok(()) -} diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs new file mode 100644 index 0000000000..442b92d499 --- /dev/null +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs @@ -0,0 +1,161 @@ +use crate::{ + error::CoreBridgeError, + legacy::utils::LegacyAnchorized, + state::{GuardianSet, Header, ProcessingStatus}, + zero_copy::EncodedVaa, +}; +use anchor_lang::prelude::*; +use solana_program::{keccak, secp256k1_recover::secp256k1_recover}; +use wormhole_raw_vaas::{GuardianSetSig, Vaa}; + +#[derive(Accounts)] +pub struct VerifyEncodedVaaV1<'info> { + write_authority: Signer<'info>, + + /// CHECK: The encoded VAA account, which stores the VAA buffer. This buffer must first be + /// written to and then verified. + #[account(mut)] + encoded_vaa: AccountInfo<'info>, + + /// Guardian set account, which is only needed for signature verification. + #[account( + seeds = [GuardianSet::SEED_PREFIX, &guardian_set.index.to_be_bytes()], + bump, + )] + guardian_set: Account<'info, LegacyAnchorized<0, GuardianSet>>, +} + +impl<'info> VerifyEncodedVaaV1<'info> { + fn constraints(ctx: &Context) -> Result<()> { + // Guardian set must be active. + let timestamp = Clock::get().map(Into::into)?; + require!( + ctx.accounts.guardian_set.is_active(×tamp), + CoreBridgeError::GuardianSetExpired + ); + + // Check write authority. + let vaa = EncodedVaa::parse_unverified(&ctx.accounts.encoded_vaa)?; + require_keys_eq!( + ctx.accounts.write_authority.key(), + vaa.write_authority(), + CoreBridgeError::WriteAuthorityMismatch + ); + + // Done. + Ok(()) + } +} + +#[access_control(VerifyEncodedVaaV1::constraints(&ctx))] +pub fn verify_encoded_vaa_v1(ctx: Context) -> Result<()> { + let guardian_set = &ctx.accounts.guardian_set; + + let write_authority = { + let encoded_vaa = EncodedVaa::parse_unverified(&ctx.accounts.encoded_vaa).unwrap(); + require!( + encoded_vaa.status() == ProcessingStatus::Writing, + CoreBridgeError::VaaAlreadyVerified + ); + + // Parse and verify. + let vaa = + Vaa::parse(encoded_vaa.buf()).map_err(|_| error!(CoreBridgeError::CannotParseVaa))?; + + // Must be V1. + require_eq!(vaa.version(), 1, CoreBridgeError::InvalidVaaVersion); + + // Make sure the encoded guardian set index agrees with the guardian set account's index. + require_eq!( + vaa.guardian_set_index(), + guardian_set.index, + CoreBridgeError::GuardianSetMismatch + ); + + // Do we have enough signatures for quorum? + let guardian_keys = &guardian_set.keys; + let quorum = crate::utils::quorum(guardian_keys.len()); + require!( + usize::from(vaa.signature_count()) >= quorum, + CoreBridgeError::NoQuorum + ); + + // Generate the same message hash (using keccak) that the Guardians used to generate their + // signatures. This message hash will be hashed again to produce the digest for + // `secp256k1_recover`. + let digest = keccak::hash(keccak::hash(vaa.body().as_ref()).as_ref()); + + // Only verify as many as we need (up to quorum). + let mut last_guardian_index = None; + let mut num_verified = 0; + for sig in vaa.signatures() { + // We do not allow for non-increasing guardian signature indices. + let index = usize::from(sig.guardian_index()); + if let Some(last_index) = last_guardian_index { + require!(index > last_index, CoreBridgeError::InvalidGuardianIndex); + } + + // Does this guardian index exist in this guardian set? + let guardian_pubkey = guardian_keys + .get(index) + .ok_or_else(|| error!(CoreBridgeError::InvalidGuardianIndex))?; + + // Now verify that the signature agrees with the expected Guardian's pubkey. + verify_guardian_signature(&sig, guardian_pubkey, digest.as_ref())?; + num_verified += 1; + + // If we have reached quorum, no need to spend compute units to verify other signatures. + if num_verified == quorum { + break; + } + + last_guardian_index = Some(index); + } + + encoded_vaa.write_authority() + }; + + let acc_data: &mut [_] = &mut ctx.accounts.encoded_vaa.data.borrow_mut(); + let mut writer = std::io::Cursor::new(acc_data); + ( + EncodedVaa::DISC, + Header { + status: ProcessingStatus::Verified, + write_authority, + version: 1, + }, + ) + .serialize(&mut writer) + .map_err(Into::into) +} + +fn verify_guardian_signature( + sig: &GuardianSetSig, + guardian_pubkey: &[u8; 20], + digest: &[u8], +) -> Result<()> { + // Recover using `solana_program::secp256k1_recover`. Public key recovery costs 25k compute + // units. And hashing this public key to recover the Ethereum public key costs about 13k. + let recovered = { + // Recover EC public key (64 bytes). + let pubkey = secp256k1_recover(digest, sig.recovery_id(), &sig.rs()) + .map_err(|_| CoreBridgeError::InvalidSignature)?; + + // The Ethereum public key is the last 20 bytes of keccak hashed public key above. + let hashed = keccak::hash(&pubkey.to_bytes()); + + let mut eth_pubkey = [0; 20]; + eth_pubkey.copy_from_slice(&hashed.0[12..]); + + eth_pubkey + }; + + // The recovered public key should agree with the Guardian's public key at this index. + require!( + recovered == *guardian_pubkey, + CoreBridgeError::InvalidGuardianKeyRecovery + ); + + // Done. + Ok(()) +} diff --git a/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/write_encoded_vaa.rs b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/write_encoded_vaa.rs new file mode 100644 index 0000000000..551a40131e --- /dev/null +++ b/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/write_encoded_vaa.rs @@ -0,0 +1,68 @@ +use crate::{error::CoreBridgeError, state::ProcessingStatus, zero_copy::EncodedVaa}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct WriteEncodedVaa<'info> { + /// The only authority that can write to the encoded VAA account. + write_authority: Signer<'info>, + + /// CHECK: The encoded VAA account, which stores the VAA buffer. This buffer must first be + /// written to and then verified. + #[account(mut)] + encoded_vaa: AccountInfo<'info>, +} + +impl<'info> WriteEncodedVaa<'info> { + fn constraints(ctx: &Context) -> Result<()> { + // Check write authority. + let vaa = EncodedVaa::parse_unverified(&ctx.accounts.encoded_vaa)?; + require_keys_eq!( + ctx.accounts.write_authority.key(), + vaa.write_authority(), + CoreBridgeError::WriteAuthorityMismatch + ); + + // Done. + Ok(()) + } +} + +/// Arguments for the [write_encoded_vaa](crate::wormhole_core_bridge_solana::write_encoded_vaa) +/// instruction. +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct WriteEncodedVaaArgs { + /// Index of VAA buffer. + pub index: u32, + /// Data representing subset of VAA buffer starting at specified index. + pub data: Vec, +} + +#[access_control(WriteEncodedVaa::constraints(&ctx))] +pub fn write_encoded_vaa(ctx: Context, args: WriteEncodedVaaArgs) -> Result<()> { + let WriteEncodedVaaArgs { index, data } = args; + + require!( + !data.is_empty(), + CoreBridgeError::InvalidInstructionArgument + ); + + let vaa_size: usize = { + let vaa = EncodedVaa::parse_unverified(&ctx.accounts.encoded_vaa).unwrap(); + require!( + vaa.status() == ProcessingStatus::Writing, + CoreBridgeError::NotInWritingStatus + ); + + vaa.vaa_size() + }; + + let index = usize::try_from(index).unwrap(); + let end = index.saturating_add(data.len()); + require!(end <= vaa_size, CoreBridgeError::DataOverflow); + + let acc_data: &mut [_] = &mut ctx.accounts.encoded_vaa.data.borrow_mut(); + acc_data[(EncodedVaa::VAA_START + index)..(EncodedVaa::VAA_START + end)].copy_from_slice(&data); + + // Done. + Ok(()) +} diff --git a/solana/programs/core-bridge/src/processor/post_message/close_message_v1.rs b/solana/programs/core-bridge/src/processor/post_message/close_message_v1.rs new file mode 100644 index 0000000000..bccd7c032e --- /dev/null +++ b/solana/programs/core-bridge/src/processor/post_message/close_message_v1.rs @@ -0,0 +1,47 @@ +use crate::{ + error::CoreBridgeError, + state::MessageStatus, + zero_copy::{LoadZeroCopy, PostedMessageV1}, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct CloseMessageV1<'info> { + emitter_authority: Signer<'info>, + + /// CHECK: Message account. The payload will be written to and then finalized. This message can + /// only be published when the message is finalized. + #[account(mut)] + draft_message: AccountInfo<'info>, + + /// CHECK: Destination for lamports if the draft message account is closed. + #[account(mut)] + close_account_destination: AccountInfo<'info>, +} + +impl<'info> CloseMessageV1<'info> { + fn constraints(ctx: &Context) -> Result<()> { + let message = PostedMessageV1::load(&ctx.accounts.draft_message)?; + + require!( + message.status() != MessageStatus::Unset, + CoreBridgeError::MessageAlreadyPublished + ); + + require_keys_eq!( + ctx.accounts.emitter_authority.key(), + message.emitter_authority(), + CoreBridgeError::EmitterAuthorityMismatch + ); + + // Done. + Ok(()) + } +} +#[access_control(CloseMessageV1::constraints(&ctx))] +pub fn close_message_v1(ctx: Context) -> Result<()> { + crate::utils::close_account( + &ctx.accounts.draft_message, + &ctx.accounts.close_account_destination, + ) +} diff --git a/solana/programs/core-bridge/src/processor/post_message/finalize_message_v1.rs b/solana/programs/core-bridge/src/processor/post_message/finalize_message_v1.rs new file mode 100644 index 0000000000..cae9faf407 --- /dev/null +++ b/solana/programs/core-bridge/src/processor/post_message/finalize_message_v1.rs @@ -0,0 +1,70 @@ +use crate::{ + error::CoreBridgeError, + state::{MessageStatus, PostedMessageV1Info}, + zero_copy::{LoadZeroCopy, PostedMessageV1}, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct FinalizeMessageV1<'info> { + emitter_authority: Signer<'info>, + + /// CHECK: Message account. The payload will be written to and then finalized. This message can + /// only be published when the message is finalized. + #[account(mut)] + draft_message: AccountInfo<'info>, +} + +impl<'info> FinalizeMessageV1<'info> { + fn constraints(ctx: &Context) -> Result<()> { + let message = PostedMessageV1::load(&ctx.accounts.draft_message)?; + + require_keys_eq!( + ctx.accounts.emitter_authority.key(), + message.emitter_authority(), + CoreBridgeError::EmitterAuthorityMismatch + ); + + // Done. + Ok(()) + } +} + +#[access_control(FinalizeMessageV1::constraints(&ctx))] +pub fn finalize_message_v1(ctx: Context) -> Result<()> { + let (nonce, consistency_level, emitter) = { + let message = PostedMessageV1::load(&ctx.accounts.draft_message).unwrap(); + + require!( + message.status() == MessageStatus::Writing, + CoreBridgeError::NotInWritingStatus + ); + + ( + message.nonce(), + message.consistency_level(), + message.emitter(), + ) + }; + + let acc_data: &mut [_] = &mut ctx.accounts.draft_message.data.borrow_mut(); + let mut writer = std::io::Cursor::new(acc_data); + + // Serialize all info for simplicity. + ( + PostedMessageV1::DISC, + PostedMessageV1Info { + consistency_level, + emitter_authority: ctx.accounts.emitter_authority.key(), + status: MessageStatus::Finalized, + _gap_0: Default::default(), + posted_timestamp: Default::default(), + nonce, + sequence: Default::default(), + solana_chain_id: Default::default(), + emitter, + }, + ) + .serialize(&mut writer) + .map_err(Into::into) +} diff --git a/solana/programs/core-bridge/src/processor/post_message/init_message_v1.rs b/solana/programs/core-bridge/src/processor/post_message/init_message_v1.rs index 8657bef0de..4512159167 100644 --- a/solana/programs/core-bridge/src/processor/post_message/init_message_v1.rs +++ b/solana/programs/core-bridge/src/processor/post_message/init_message_v1.rs @@ -1,11 +1,9 @@ -use std::io::{Read, Write}; - use crate::{ constants::MAX_MESSAGE_PAYLOAD_SIZE, error::CoreBridgeError, - legacy::utils::LegacyAccount, - state::{MessageStatus, PostedMessageV1, PostedMessageV1Info}, + state::{MessageStatus, PostedMessageV1Info}, types::Commitment, + zero_copy::PostedMessageV1, }; use anchor_lang::prelude::*; @@ -27,33 +25,26 @@ pub struct InitMessageV1<'info> { impl<'info> InitMessageV1<'info> { fn constraints(ctx: &Context) -> Result<()> { - // Checking that the message account is completely zeroed out. By doing this, we make the - // assumption that no other Core Bridge account that is currently used will have all zeros. - // Ideally all of the Core Bridge accounts should have a discriminator so we do not have to - // mess around like this. But here we are. - let msg_acc_data: &[u8] = &ctx.accounts.draft_message.try_borrow_data()?; - let mut reader = std::io::Cursor::new(msg_acc_data); - // Infer the expected message length given the size of the created account. let data_len = ctx.accounts.draft_message.data_len(); - require_gt!( - data_len, - PostedMessageV1::BYTES_START, + require!( + data_len > PostedMessageV1::PAYLOAD_START, CoreBridgeError::InvalidCreatedAccountSize ); // This message length cannot exceed the maximum message length. - require_gte!( - MAX_MESSAGE_PAYLOAD_SIZE, - data_len - PostedMessageV1::BYTES_START, + require!( + data_len - PostedMessageV1::PAYLOAD_START <= MAX_MESSAGE_PAYLOAD_SIZE, CoreBridgeError::ExceedsMaxPayloadSize ); - // All of the discriminator + header bytes + the 4-byte payload length should be zero. - let mut zeros = [0; PostedMessageV1::BYTES_START]; - reader.read_exact(&mut zeros).unwrap(); + // Checking that the message account is completely zeroed out. By doing this, we make the + // assumption that no other Core Bridge account that is currently used will have all zeros. + // Ideally all of the Core Bridge accounts should have a discriminator so we do not have to + // mess around like this. But here we are. + let msg_acc_data: &[_] = &ctx.accounts.draft_message.try_borrow_data()?; require!( - zeros == [0; PostedMessageV1::BYTES_START], + msg_acc_data[..PostedMessageV1::PAYLOAD_START] == [0; PostedMessageV1::PAYLOAD_START], CoreBridgeError::AccountNotZeroed ); @@ -62,25 +53,24 @@ impl<'info> InitMessageV1<'info> { } } -/// Arguments to initialize a new [PostedMessageV1](crate::state::PostedMessageV1) account for -/// writing. +/// Arguments for the [init_message_v1](crate::wormhole_core_bridge_solana::init_message_v1) +/// instruction. #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] pub struct InitMessageV1Args { + /// Unique id for this message. pub nonce: u32, + /// Solana commitment level for Guardian observation. pub commitment: Commitment, + /// Optional program ID if the emitter address will be your program ID. + /// + /// NOTE: If `Some(program_id)`, your emitter authority seeds to be \[b"emitter\]. pub cpi_program_id: Option, } #[access_control(InitMessageV1::constraints(&ctx))] pub fn init_message_v1(ctx: Context, args: InitMessageV1Args) -> Result<()> { - let expected_msg_length = ctx.accounts.draft_message.data_len() - PostedMessageV1::BYTES_START; - - // This message length cannot exceed the maximum message length. - require_gte!( - MAX_MESSAGE_PAYLOAD_SIZE, - expected_msg_length, - CoreBridgeError::ExceedsMaxPayloadSize - ); + let expected_msg_length = + ctx.accounts.draft_message.data_len() - PostedMessageV1::PAYLOAD_START; let InitMessageV1Args { nonce, @@ -93,26 +83,26 @@ pub fn init_message_v1(ctx: Context, args: InitMessageV1Args) -> // want to manage two separate addresses (program ID and emitter address) cross chain. let emitter = new_emitter(&ctx.accounts.emitter_authority, cpi_program_id)?; - let acc_data: &mut [u8] = &mut ctx.accounts.draft_message.data.borrow_mut(); + let acc_data: &mut [_] = &mut ctx.accounts.draft_message.data.borrow_mut(); let mut writer = std::io::Cursor::new(acc_data); // Finally initialize the draft message account by serializing the discriminator, header and // payload length. - writer.write_all(&PostedMessageV1::DISCRIMINATOR)?; - PostedMessageV1Info { - consistency_level: commitment.into(), - emitter_authority: ctx.accounts.emitter_authority.key(), - status: MessageStatus::Writing, - _gap_0: Default::default(), - posted_timestamp: Default::default(), - nonce, - sequence: Default::default(), - solana_chain_id: Default::default(), - emitter, - } - .serialize(&mut writer)?; - u32::try_from(expected_msg_length) - .unwrap() + ( + PostedMessageV1::DISC, + PostedMessageV1Info { + consistency_level: commitment.into(), + emitter_authority: ctx.accounts.emitter_authority.key(), + status: MessageStatus::Writing, + _gap_0: Default::default(), + posted_timestamp: Default::default(), + nonce, + sequence: Default::default(), + solana_chain_id: Default::default(), + emitter, + }, + u32::try_from(expected_msg_length).unwrap(), + ) .serialize(&mut writer) .map_err(Into::into) } diff --git a/solana/programs/core-bridge/src/processor/post_message/mod.rs b/solana/programs/core-bridge/src/processor/post_message/mod.rs index 8eff46b69e..1e90c43688 100644 --- a/solana/programs/core-bridge/src/processor/post_message/mod.rs +++ b/solana/programs/core-bridge/src/processor/post_message/mod.rs @@ -1,8 +1,14 @@ +mod finalize_message_v1; +pub use finalize_message_v1::*; + mod init_message_v1; pub use init_message_v1::*; -mod process_message_v1; -pub use process_message_v1::*; +mod close_message_v1; +pub use close_message_v1::*; + +mod write_message_v1; +pub use write_message_v1::*; use crate::error::CoreBridgeError; use anchor_lang::prelude::*; diff --git a/solana/programs/core-bridge/src/processor/post_message/process_message_v1.rs b/solana/programs/core-bridge/src/processor/post_message/process_message_v1.rs deleted file mode 100644 index 1296203707..0000000000 --- a/solana/programs/core-bridge/src/processor/post_message/process_message_v1.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::io::Write; - -use crate::{ - error::CoreBridgeError, - state::{MessageStatus, PostedMessageV1Info}, - zero_copy::PostedMessageV1, -}; -use anchor_lang::prelude::*; - -#[derive(Accounts)] -pub struct ProcessMessageV1<'info> { - emitter_authority: Signer<'info>, - - /// CHECK: We do not deserialize this account as `PostedMessageV1` because allocating heap - /// memory in its deserialization uses significant compute units with every call to this - /// instruction handler. For large messages, this can be a significant cost. - #[account( - mut, - owner = crate::ID - )] - draft_message: AccountInfo<'info>, - - /// CHECK: Destination for lamports if the draft message account is closed. - #[account(mut)] - close_account_destination: Option>, -} - -impl<'info> ProcessMessageV1<'info> { - fn constraints(ctx: &Context) -> Result<()> { - let acc_data = ctx.accounts.draft_message.try_borrow_data()?; - let message = PostedMessageV1::parse(&acc_data)?; - - // require!( - // info.status == MessageStatus::Writing, - // CoreBridgeError::MessageAlreadyPublished - // ); - require_keys_eq!( - ctx.accounts.emitter_authority.key(), - message.emitter_authority(), - CoreBridgeError::EmitterAuthorityMismatch - ); - - // Done. - Ok(()) - } -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum ProcessMessageV1Directive { - CloseMessageAccount, - Write { index: u32, data: Vec }, - Finalize, -} - -#[access_control(ProcessMessageV1::constraints(&ctx))] -pub fn process_message_v1( - ctx: Context, - directive: ProcessMessageV1Directive, -) -> Result<()> { - match directive { - ProcessMessageV1Directive::CloseMessageAccount => close_message_account(ctx), - ProcessMessageV1Directive::Write { index, data } => write(ctx, index, data), - ProcessMessageV1Directive::Finalize => finalize(ctx), - } -} - -fn close_message_account(ctx: Context) -> Result<()> { - msg!("Directive: CloseMessageAccount"); - - match &ctx.accounts.close_account_destination { - Some(sol_destination) => { - { - let acc_data = ctx.accounts.draft_message.data.borrow(); - let message = PostedMessageV1::parse(&acc_data)?; - - require!( - message.status() != MessageStatus::Unset, - CoreBridgeError::MessageAlreadyPublished - ); - } - - crate::utils::close_account( - ctx.accounts.draft_message.to_account_info(), - sol_destination.to_account_info(), - ) - } - None => err!(ErrorCode::AccountNotEnoughKeys), - } -} - -fn write(ctx: Context, index: u32, data: Vec) -> Result<()> { - msg!("Directive: Write"); - - require!( - !data.is_empty(), - CoreBridgeError::InvalidInstructionArgument - ); - - let msg_length = { - let acc_data = ctx.accounts.draft_message.data.borrow(); - let message = PostedMessageV1::parse(&acc_data)?; - - require!( - message.status() == MessageStatus::Writing, - CoreBridgeError::NotInWritingStatus - ); - - message.payload_size() - }; - - let index = usize::try_from(index).unwrap(); - let end = index.saturating_add(data.len()); - require_gte!(msg_length, end, CoreBridgeError::DataOverflow); - - const START: usize = 4 + PostedMessageV1::PAYLOAD_START; - let acc_data = &mut ctx.accounts.draft_message.data.borrow_mut(); - acc_data[(START + index)..(START + end)].copy_from_slice(&data); - - // Done. - Ok(()) -} - -fn finalize(ctx: Context) -> Result<()> { - msg!("Directive: Finalize"); - - let (nonce, consistency_level, emitter) = { - let acc_data = ctx.accounts.draft_message.data.borrow(); - let message = PostedMessageV1::parse(&acc_data)?; - - require!( - message.status() == MessageStatus::Writing, - CoreBridgeError::NotInWritingStatus - ); - - ( - message.nonce(), - message.consistency_level(), - message.emitter(), - ) - }; - - // Skip the discriminator. - let acc_data: &mut [u8] = &mut ctx.accounts.draft_message.data.borrow_mut(); - let mut writer = std::io::Cursor::new(acc_data); - - // Serialize all info for simplicity. - writer.write_all(&PostedMessageV1::DISCRIMINATOR)?; - PostedMessageV1Info { - consistency_level, - emitter_authority: ctx.accounts.emitter_authority.key(), - status: MessageStatus::Finalized, - _gap_0: Default::default(), - posted_timestamp: Default::default(), - nonce, - sequence: Default::default(), - solana_chain_id: Default::default(), - emitter, - } - .serialize(&mut writer) - .map_err(Into::into) -} diff --git a/solana/programs/core-bridge/src/processor/post_message/write_message_v1.rs b/solana/programs/core-bridge/src/processor/post_message/write_message_v1.rs new file mode 100644 index 0000000000..da2b713761 --- /dev/null +++ b/solana/programs/core-bridge/src/processor/post_message/write_message_v1.rs @@ -0,0 +1,73 @@ +use crate::{ + error::CoreBridgeError, + state::MessageStatus, + zero_copy::{LoadZeroCopy, PostedMessageV1}, +}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct WriteMessageV1<'info> { + emitter_authority: Signer<'info>, + + /// CHECK: Message account. The payload will be written to and then finalized. This message can + /// only be published when the message is finalized. + #[account(mut)] + draft_message: AccountInfo<'info>, +} + +impl<'info> WriteMessageV1<'info> { + fn constraints(ctx: &Context) -> Result<()> { + let message = PostedMessageV1::load(&ctx.accounts.draft_message)?; + + require_keys_eq!( + ctx.accounts.emitter_authority.key(), + message.emitter_authority(), + CoreBridgeError::EmitterAuthorityMismatch + ); + + // Done. + Ok(()) + } +} + +/// Arguments for the [write_message_v1](crate::wormhole_core_bridge_solana::write_message_v1) +/// instruction. +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct WriteMessageV1Args { + /// Index of message buffer. + pub index: u32, + /// Data representing subset of message buffer starting at specified index. + pub data: Vec, +} + +#[access_control(WriteMessageV1::constraints(&ctx))] +pub fn write_message_v1(ctx: Context, args: WriteMessageV1Args) -> Result<()> { + let WriteMessageV1Args { index, data } = args; + + require!( + !data.is_empty(), + CoreBridgeError::InvalidInstructionArgument + ); + + let msg_length = { + let message = PostedMessageV1::load(&ctx.accounts.draft_message).unwrap(); + + require!( + message.status() == MessageStatus::Writing, + CoreBridgeError::NotInWritingStatus + ); + + message.payload_size() + }; + + let index = usize::try_from(index).unwrap(); + let end = index.saturating_add(data.len()); + require!(end <= msg_length, CoreBridgeError::DataOverflow); + + let acc_data = &mut ctx.accounts.draft_message.data.borrow_mut(); + acc_data[(PostedMessageV1::PAYLOAD_START + index)..(PostedMessageV1::PAYLOAD_START + end)] + .copy_from_slice(&data); + + // Done. + Ok(()) +} diff --git a/solana/programs/core-bridge/src/sdk/cpi/close_encoded_vaa.rs b/solana/programs/core-bridge/src/sdk/cpi/close_encoded_vaa.rs new file mode 100644 index 0000000000..7e49a58526 --- /dev/null +++ b/solana/programs/core-bridge/src/sdk/cpi/close_encoded_vaa.rs @@ -0,0 +1,24 @@ +use anchor_lang::prelude::*; + +pub trait CloseEncodedVaa<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info>; + + fn write_authority(&self) -> AccountInfo<'info>; + + fn encoded_vaa(&self) -> AccountInfo<'info>; +} + +/// SDK method to close an EncodedVaa account. This method will fail if the write authority is not +/// the same one found on the [EncodedVaa](crate::state::EncodedVaa) account. +pub fn close_encoded_vaa<'info, A>(accounts: &A) -> Result<()> +where + A: CloseEncodedVaa<'info>, +{ + crate::cpi::close_encoded_vaa(CpiContext::new( + accounts.core_bridge_program(), + crate::cpi::accounts::CloseEncodedVaa { + write_authority: accounts.write_authority(), + encoded_vaa: accounts.encoded_vaa(), + }, + )) +} diff --git a/solana/programs/core-bridge/src/sdk/cpi/mod.rs b/solana/programs/core-bridge/src/sdk/cpi/mod.rs index 02b9424653..0ad019243f 100644 --- a/solana/programs/core-bridge/src/sdk/cpi/mod.rs +++ b/solana/programs/core-bridge/src/sdk/cpi/mod.rs @@ -1,23 +1,20 @@ -mod publish_message; -pub use publish_message::*; +//! CPI builders. Methods useful for interacting with the Core Bridge program from another program. -mod prepare_message_v1; -pub use prepare_message_v1::*; +/// Sub-module for System program interaction. +pub mod system_program { + pub use crate::utils::cpi::{create_account, CreateAccount}; +} -use anchor_lang::prelude::*; +pub use crate::utils::vaa::claim_vaa; -/// Trait for invoking any Core Bridge instruction via CPI. This trait is used for preparing and -/// posting Core Bridge messages specifically. -pub trait InvokeCoreBridge<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info>; -} +mod close_encoded_vaa; +pub use close_encoded_vaa::*; -/// Trait for invoking any program instruction that requires account creation. -pub trait CreateAccount<'info> { - fn payer(&self) -> AccountInfo<'info>; +mod publish_message; +pub use publish_message::*; - fn system_program(&self) -> AccountInfo<'info>; -} +mod prepare_message; +pub use prepare_message::*; /// Wormhole Core Bridge Program. pub type CoreBridge = crate::program::WormholeCoreBridgeSolana; diff --git a/solana/programs/core-bridge/src/sdk/cpi/prepare_message.rs b/solana/programs/core-bridge/src/sdk/cpi/prepare_message.rs new file mode 100644 index 0000000000..61105a99ed --- /dev/null +++ b/solana/programs/core-bridge/src/sdk/cpi/prepare_message.rs @@ -0,0 +1,204 @@ +pub use crate::processor::InitMessageV1Args; + +use anchor_lang::prelude::*; + +/// Trait for invoking the Core Bridge program's [init_message_v1](crate::cpi::init_message_v1), +/// [write_message_v1](crate::cpi::write_message_v1) and +/// [finalize_message_v1](crate::cpi::finalize_message_v1) instructions. These instructions are used +/// in concert with each other to prepare a message, which can be posted either within a program via +/// CPI or within the same transaction block as an instruction following your program's instruction. +pub trait PrepareMessage<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info>; + + /// Core Bridge Emitter Authority (read-only signer). This emitter authority acts as the signer + /// for preparing a message before it is posted. + fn core_emitter_authority(&self) -> AccountInfo<'info>; + + /// Core Bridge Message (mut). + fn core_message(&self) -> AccountInfo<'info>; +} + +/// SDK method for preparing a new Core Bridge message. It is assumed that the emitter authority is +/// your program's PDA, so emitter authority seeds are required to sign for these Core Bridge +/// Program instructions. +/// +/// NOTE: When using this SDK method, be aware that the message account is not created yet. You must +/// either invoke [create_account](crate::sdk::cpi::system_program::create_account) or use Anchor's +/// `init` macro directive before calling this method. +pub fn prepare_message<'info, A>( + accounts: &A, + init_args: InitMessageV1Args, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PrepareMessage<'info>, +{ + handle_prepare_message_v1( + accounts.core_bridge_program(), + accounts.core_message(), + accounts.core_emitter_authority(), + init_args, + data, + signer_seeds, + ) +} + +/// SDK method for initializing a new Core Bridge message by starting to write data to a message +/// account. If the message requires multiple calls, using this method may be convenient to begin +/// writing and then following this call with a subsequent [write_message] or +/// [write_and_finalize_message] call. +pub fn init_and_write_message<'info, A>( + accounts: &A, + init_args: InitMessageV1Args, + index: u32, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PrepareMessage<'info>, +{ + handle_init_message_v1( + accounts.core_bridge_program(), + accounts.core_message(), + accounts.core_emitter_authority(), + init_args, + signer_seeds, + )?; + + write_message(accounts, index, data, signer_seeds) +} + +/// SDK method for writing to an existing Core Bridge message if it is still in +/// [Writing](crate::state::MessageStatus::Writing) status. +pub fn write_message<'info, A>( + accounts: &A, + index: u32, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PrepareMessage<'info>, +{ + handle_write_message_v1( + accounts.core_bridge_program(), + accounts.core_message(), + accounts.core_emitter_authority(), + index, + data, + signer_seeds, + ) +} + +/// SDK method for writing and then finalizing an existing Core Bridge message if it is still in +/// [Writing](crate::state::MessageStatus::Writing) status. This method may be convenient to wrap up +/// writing data when it follows either [init_and_write_message] or [write_message]. +pub fn write_and_finalize_message<'info, A>( + accounts: &A, + index: u32, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PrepareMessage<'info>, +{ + write_message(accounts, index, data, signer_seeds)?; + + handle_finalize_message_v1( + accounts.core_bridge_program(), + accounts.core_message(), + accounts.core_emitter_authority(), + signer_seeds, + ) +} + +pub(in crate::sdk::cpi) fn handle_prepare_message_v1<'info>( + core_bridge_program: AccountInfo<'info>, + core_message: AccountInfo<'info>, + core_emitter_authority: AccountInfo<'info>, + init_args: InitMessageV1Args, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> { + // Initialize message with created account. + handle_init_message_v1( + core_bridge_program.clone(), + core_message.clone(), + core_emitter_authority.clone(), + init_args, + signer_seeds, + )?; + + handle_write_message_v1( + core_bridge_program.clone(), + core_message.clone(), + core_emitter_authority.clone(), + 0, + data, + signer_seeds, + )?; + + handle_finalize_message_v1( + core_bridge_program.clone(), + core_message.clone(), + core_emitter_authority.clone(), + signer_seeds, + ) +} + +fn handle_init_message_v1<'info>( + core_bridge_program: AccountInfo<'info>, + core_message: AccountInfo<'info>, + core_emitter_authority: AccountInfo<'info>, + init_args: InitMessageV1Args, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> { + crate::cpi::init_message_v1( + CpiContext::new_with_signer( + core_bridge_program.clone(), + crate::cpi::accounts::InitMessageV1 { + emitter_authority: core_emitter_authority.clone(), + draft_message: core_message.clone(), + }, + signer_seeds.unwrap_or_default(), + ), + init_args, + ) +} + +fn handle_write_message_v1<'info>( + core_bridge_program: AccountInfo<'info>, + core_message: AccountInfo<'info>, + core_emitter_authority: AccountInfo<'info>, + index: u32, + data: Vec, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> { + crate::cpi::write_message_v1( + CpiContext::new_with_signer( + core_bridge_program.clone(), + crate::cpi::accounts::WriteMessageV1 { + emitter_authority: core_emitter_authority.clone(), + draft_message: core_message.clone(), + }, + signer_seeds.unwrap_or_default(), + ), + crate::processor::WriteMessageV1Args { index, data }, + ) +} + +fn handle_finalize_message_v1<'info>( + core_bridge_program: AccountInfo<'info>, + core_message: AccountInfo<'info>, + core_emitter_authority: AccountInfo<'info>, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> { + crate::cpi::finalize_message_v1(CpiContext::new_with_signer( + core_bridge_program.clone(), + crate::cpi::accounts::FinalizeMessageV1 { + emitter_authority: core_emitter_authority.clone(), + draft_message: core_message.clone(), + }, + signer_seeds.unwrap_or_default(), + )) +} diff --git a/solana/programs/core-bridge/src/sdk/cpi/prepare_message_v1.rs b/solana/programs/core-bridge/src/sdk/cpi/prepare_message_v1.rs deleted file mode 100644 index 5dfc08a35b..0000000000 --- a/solana/programs/core-bridge/src/sdk/cpi/prepare_message_v1.rs +++ /dev/null @@ -1,91 +0,0 @@ -pub use crate::processor::InitMessageV1Args; - -use anchor_lang::prelude::*; - -use super::InvokeCoreBridge; - -/// Trait for invoking the Core Bridge program's `init_message_v1` and `process_message_v1` -/// instructions. These instructions are used in concert with each other to prepare a message, which -/// can be posted either within a program via CPI or within the same transaction block as an -/// instruction following your program's instruction. -pub trait PrepareMessageV1<'info>: InvokeCoreBridge<'info> { - /// Core Bridge Emitter Authority (read-only signer). This emitter authority acts as the signer - /// for preparing a message before it is posted. - fn core_emitter_authority(&self) -> AccountInfo<'info>; - - /// Core Bridge Message (mut). - fn core_message(&self) -> AccountInfo<'info>; -} - -/// SDK method for preparing a new Core Bridge message. It is assumed that the emitter authority is -/// your program's PDA, so emitter authority seeds are required to sign for these Core Bridge -/// Program instructions. -/// -/// NOTE: When using this SDK method, be aware that the message account is not created yet. You must -/// invoke `system_program::create_account` before calling this method either using Anchor's `init` -/// macro directive or via System Program CPI. -pub fn prepare_message_v1<'info, A: PrepareMessageV1<'info>>( - accounts: &A, - init_args: InitMessageV1Args, - data: Vec, - emitter_authority_seeds: &[&[u8]], -) -> Result<()> { - handle_prepare_message_v1( - accounts.core_bridge_program(), - accounts.core_message(), - accounts.core_emitter_authority(), - init_args, - data, - emitter_authority_seeds, - ) -} - -pub(in crate::sdk) fn handle_prepare_message_v1<'info>( - core_bridge_program: AccountInfo<'info>, - core_message: AccountInfo<'info>, - core_emitter_authority: AccountInfo<'info>, - init_args: InitMessageV1Args, - data: Vec, - emitter_authority_seeds: &[&[u8]], -) -> Result<()> { - // Initialize message with created account. - crate::cpi::init_message_v1( - CpiContext::new_with_signer( - core_bridge_program.clone(), - crate::cpi::accounts::InitMessageV1 { - emitter_authority: core_emitter_authority.clone(), - draft_message: core_message.clone(), - }, - &[emitter_authority_seeds], - ), - init_args, - )?; - - // Write message. - crate::cpi::process_message_v1( - CpiContext::new_with_signer( - core_bridge_program.clone(), - crate::cpi::accounts::ProcessMessageV1 { - emitter_authority: core_emitter_authority.clone(), - draft_message: core_message.clone(), - close_account_destination: None, - }, - &[emitter_authority_seeds], - ), - crate::processor::ProcessMessageV1Directive::Write { index: 0, data }, - )?; - - // Finalize. - crate::cpi::process_message_v1( - CpiContext::new_with_signer( - core_bridge_program.clone(), - crate::cpi::accounts::ProcessMessageV1 { - emitter_authority: core_emitter_authority.clone(), - draft_message: core_message.clone(), - close_account_destination: None, - }, - &[emitter_authority_seeds], - ), - crate::processor::ProcessMessageV1Directive::Finalize, - ) -} diff --git a/solana/programs/core-bridge/src/sdk/cpi/publish_message.rs b/solana/programs/core-bridge/src/sdk/cpi/publish_message.rs index a641902e27..805ee18763 100644 --- a/solana/programs/core-bridge/src/sdk/cpi/publish_message.rs +++ b/solana/programs/core-bridge/src/sdk/cpi/publish_message.rs @@ -1,56 +1,38 @@ -pub use crate::legacy::cpi::PostMessageArgs; +use crate::{legacy::instruction::PostMessageArgs, types::Commitment}; +use anchor_lang::prelude::*; -use crate::{error::CoreBridgeError, types::Commitment}; -use anchor_lang::{prelude::*, system_program}; +/// Trait for invoking one of the ways you can publish a Wormhole message using the Core Bridge +/// program. +/// +/// NOTE: A message's emitter address can either be a program's ID or a custom address (determined +/// by either a keypair or program's PDA). If the emitter address is a program ID, then +/// the seeds for the emitter authority must be \["emitter"\]. +pub trait PublishMessage<'info>: super::system_program::CreateAccount<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info>; -use super::{CreateAccount, InvokeCoreBridge}; + /// Core Bridge Emitter Authority (read-only signer). This account should return either the + /// account that will act as the emitter address or the signer for a program emitter, where the + /// emitter address is a program ID. This emitter also acts as the authority for preparing a + /// message before it is posted. + fn core_emitter_authority(&self) -> AccountInfo<'info>; -/// Trait for invoking the Core Bridge program's `post_message` instruction. Using this trait will -/// make posting (publishing) a Wormhole (Core Bridge) message easier. -/// -/// A message's emitter address can either based on a program's ID or a custom address (determined -/// by either a keypair or program's PDA). Depending on which emitter address is used, the -/// `core_emitter` or `core_emitter_authority` account must be provided. -/// -/// When the emitter itself is a signer for the post message instruction, you must specify Some for -/// `core_emitter`. Otherwise, if the emitter is a program ID, you must specify Some for -/// `core_emitter_authority`, which is the program's authority to draft a new message to prepare it -/// for posting. By default, `core_emitter_authority` returns None, so you must override it if the -/// emitter address is a program ID. -pub trait PublishMessage<'info>: InvokeCoreBridge<'info> + CreateAccount<'info> { /// Core Bridge Program Data (mut, seeds = \["Bridge"\]). fn core_bridge_config(&self) -> AccountInfo<'info>; - /// Core Bridge Message (mut). - fn core_message(&self) -> AccountInfo<'info>; - - /// Core Bridge Emitter (read-only signer). - /// - /// NOTE: This account isn't checked if the message's emitter address is a program ID, so - /// in this case it can return None. - fn core_emitter(&self) -> Option>; - /// Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\]). fn core_emitter_sequence(&self) -> AccountInfo<'info>; /// Core Bridge Fee Collector (mut, seeds = \["fee_collector"\]). + /// + /// NOTE: This account is mutable because the SDK method that publishes messages pays the + /// Wormhole fee, which requires a lamport transfer from the payer to the fee collector. fn core_fee_collector(&self) -> Option>; - - /// Core Bridge Emitter Authority (read-only signer). This account should return Some if the - /// emitter address is a program ID. This emitter authority acts as the signer for preparing a - /// message before it is posted. - fn core_emitter_authority(&self) -> Option> { - None - } } /// Directive used to determine how to post a Core Bridge message. pub enum PublishMessageDirective { /// Ordinary message, which creates a new account for the Core Bridge message. The emitter /// address is the pubkey of the emitter signer. - /// - /// NOTE: The core_emitter in PublishMessage must return Some, which will be the account - /// info for the emitter signer. See legacy `post_message` for more info. Message { nonce: u32, payload: Vec, @@ -59,9 +41,8 @@ pub enum PublishMessageDirective { /// Ordinary message, which creates a new account for the Core Bridge message. The emitter /// address is the program ID specified in this directive. /// - /// NOTE: The core_emitter_authority in PublishMessage must return Some, which will be the - /// account info for the authority used to prepare a new draft message. See `init_message_v1` - /// and `process_message_v1` for more details. + /// NOTE: [core_emitter_authority](PublishMessage::core_emitter_authority) must use seeds = + /// \["emitter"\]. ProgramMessage { program_id: Pubkey, nonce: u32, @@ -71,42 +52,49 @@ pub enum PublishMessageDirective { /// Unreliable (reusable) message, which will either create a new account or reuse an existing /// Core Bridge message account. The emitter address is the pubkey of the emitter signer. If a /// message account is reused, the payload length must be the same as the existing message's. - /// - /// NOTE: The core_emitter in PublishMessage must return Some, which will be the account - /// info for the emitter signer. See legacy `post_message` for more info. UnreliableMessage { nonce: u32, payload: Vec, commitment: Commitment, }, + /// Prepared message, which was already written to a message account. Usually this operation + /// would be executed within a transaction block following a program's instruction(s) preparing + /// a Wormhole message. But if there are other operations your program needs to perform in + /// concert with publishing a prepared message, use this directive. + PreparedMessage, } -/// SDK method for posting a new message with the Core Bridge program. This method will handle any -/// of the following directives: -/// * Post a new message with an emitter address determined by either a keypair or program PDA. +/// SDK method for posting a new message with the Core Bridge program. +/// +/// This method will handle any of the following directives: +/// +/// * Post a new message with an emitter address determined by either a keypair pubkey or PDA +/// address. /// * Post a new message with an emitter address that is a program ID. /// * Post an unreliable message, which can reuse a message account with a new payload. /// -/// The accounts must implement `InvokePublishMessage`. +/// The accounts must implement [PublishMessage]. /// /// Emitter seeds are needed to act as a signer for the post message instructions. These seeds are -/// either the seeds of a program's PDA or specifically seeds = \["emitter"\] if the program ID is the +/// either the seeds of a PDA or specifically seeds = \["emitter"\] if the program ID is the /// emitter address. /// /// Message seeds are optional and are only needed if the integrating program is using a PDA for -/// this account. Otherwise, a keypair can be used and message seeds can be None. -pub fn publish_message<'info, A: PublishMessage<'info>>( +/// this account. Otherwise, a keypair can be used. +pub fn publish_message<'info, A>( accounts: &A, + new_message: &AccountInfo<'info>, directive: PublishMessageDirective, - emitter_seeds: &[&[u8]], - message_seeds: Option<&[&[u8]]>, -) -> Result<()> { + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PublishMessage<'info>, +{ // If there is a fee, transfer it. But only try if the fee collector is provided because the // post message instruction will fail if there is actually a fee but no fee collector. if let Some(fee_collector) = accounts.core_fee_collector() { - let fee_lamports = - crate::zero_copy::Config::parse(&accounts.core_bridge_config().try_borrow_data()?)? - .fee_lamports(); + let fee_lamports = crate::zero_copy::Config::parse(&accounts.core_bridge_config()) + .map(|config| config.fee_lamports())?; if fee_lamports > 0 { anchor_lang::system_program::transfer( @@ -129,13 +117,13 @@ pub fn publish_message<'info, A: PublishMessage<'info>>( commitment, } => handle_post_message_v1( accounts, + new_message, PostMessageArgs { nonce, payload, commitment, }, - emitter_seeds, - message_seeds, + signer_seeds, ), PublishMessageDirective::ProgramMessage { program_id, @@ -144,12 +132,12 @@ pub fn publish_message<'info, A: PublishMessage<'info>>( commitment, } => handle_post_program_message_v1( accounts, + new_message, program_id, nonce, payload, commitment, - emitter_seeds, - message_seeds, + signer_seeds, ), PublishMessageDirective::UnreliableMessage { nonce, @@ -157,118 +145,80 @@ pub fn publish_message<'info, A: PublishMessage<'info>>( commitment, } => handle_post_unreliable_message_v1( accounts, + new_message, PostMessageArgs { nonce, payload, commitment, }, - emitter_seeds, - message_seeds, + signer_seeds, ), + PublishMessageDirective::PreparedMessage => { + handle_prepared_message_v1(accounts, new_message, signer_seeds) + } } } -fn handle_post_message_v1<'info, A: PublishMessage<'info>>( +fn handle_post_message_v1<'info, A>( accounts: &A, + new_message: &AccountInfo<'info>, args: PostMessageArgs, - emitter_seeds: &[&[u8]], - message_seeds: Option<&[&[u8]]>, -) -> Result<()> { - match message_seeds { - Some(message_seeds) => crate::legacy::cpi::post_message( - CpiContext::new_with_signer( - accounts.core_bridge_program(), - crate::legacy::cpi::PostMessage { - config: accounts.core_bridge_config(), - message: accounts.core_message(), - emitter: accounts.core_emitter(), - emitter_sequence: accounts.core_emitter_sequence(), - payer: accounts.payer(), - fee_collector: accounts.core_fee_collector(), - system_program: accounts.system_program(), - }, - &[emitter_seeds, message_seeds], - ), - args, - ), - None => crate::legacy::cpi::post_message( - CpiContext::new_with_signer( - accounts.core_bridge_program(), - crate::legacy::cpi::PostMessage { - config: accounts.core_bridge_config(), - message: accounts.core_message(), - emitter: accounts.core_emitter(), - emitter_sequence: accounts.core_emitter_sequence(), - payer: accounts.payer(), - fee_collector: accounts.core_fee_collector(), - system_program: accounts.system_program(), - }, - &[emitter_seeds], - ), - args, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PublishMessage<'info>, +{ + crate::legacy::cpi::post_message( + CpiContext::new_with_signer( + accounts.core_bridge_program(), + crate::legacy::cpi::PostMessage { + config: accounts.core_bridge_config(), + message: new_message.to_account_info(), + emitter: Some(accounts.core_emitter_authority()), + emitter_sequence: accounts.core_emitter_sequence(), + payer: accounts.payer(), + fee_collector: accounts.core_fee_collector(), + system_program: accounts.system_program(), + }, + signer_seeds.unwrap_or_default(), ), - } + args, + ) } -fn handle_post_program_message_v1<'info, A: PublishMessage<'info>>( +fn handle_post_program_message_v1<'info, A>( accounts: &A, + new_message: &AccountInfo<'info>, program_id: Pubkey, nonce: u32, payload: Vec, commitment: Commitment, - emitter_authority_seeds: &[&[u8]], - message_seeds: Option<&[&[u8]]>, -) -> Result<()> { - let emitter_authority = accounts - .core_emitter_authority() - .ok_or(CoreBridgeError::EmitterAuthorityRequired)?; - + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PublishMessage<'info>, +{ // Create message account. - { - let data_len = crate::sdk::compute_init_message_v1_space(payload.len()); - let lamports = Rent::get().map(|rent| rent.minimum_balance(data_len))?; - - match message_seeds { - Some(message_seeds) => system_program::create_account( - CpiContext::new_with_signer( - accounts.system_program(), - system_program::CreateAccount { - from: accounts.payer(), - to: accounts.core_message(), - }, - &[message_seeds], - ), - lamports, - data_len.try_into().unwrap(), - &crate::ID, - ), - None => system_program::create_account( - CpiContext::new( - accounts.system_program(), - system_program::CreateAccount { - from: accounts.payer(), - to: accounts.core_message(), - }, - ), - lamports, - data_len.try_into().unwrap(), - &crate::ID, - ), - }?; - } + crate::utils::cpi::create_account( + accounts, + new_message, + crate::sdk::compute_prepared_message_space(payload.len()), + &crate::ID, + signer_seeds, + )?; // Prepare (calling init and process instructions). crate::sdk::cpi::handle_prepare_message_v1( accounts.core_bridge_program(), - accounts.core_message(), - emitter_authority, + new_message.to_account_info(), + accounts.core_emitter_authority(), crate::sdk::cpi::InitMessageV1Args { nonce, cpi_program_id: Some(program_id), commitment, }, payload, - emitter_authority_seeds, + signer_seeds, )?; // Finally post. @@ -277,7 +227,7 @@ fn handle_post_program_message_v1<'info, A: PublishMessage<'info>>( accounts.core_bridge_program(), crate::legacy::cpi::PostMessage { config: accounts.core_bridge_config(), - message: accounts.core_message(), + message: new_message.to_account_info(), emitter: None, emitter_sequence: accounts.core_emitter_sequence(), payer: accounts.payer(), @@ -293,48 +243,59 @@ fn handle_post_program_message_v1<'info, A: PublishMessage<'info>>( ) } -fn handle_post_unreliable_message_v1<'info, A: PublishMessage<'info>>( +fn handle_post_unreliable_message_v1<'info, A>( accounts: &A, + new_message: &AccountInfo<'info>, args: PostMessageArgs, - emitter_seeds: &[&[u8]], - message_seeds: Option<&[&[u8]]>, -) -> Result<()> { - let emitter = accounts - .core_emitter() - .ok_or(CoreBridgeError::EmitterRequired)?; - - match message_seeds { - Some(message_seeds) => crate::legacy::cpi::post_message_unreliable( - CpiContext::new_with_signer( - accounts.core_bridge_program(), - crate::legacy::cpi::PostMessageUnreliable { - config: accounts.core_bridge_config(), - message: accounts.core_message(), - emitter, - emitter_sequence: accounts.core_emitter_sequence(), - payer: accounts.payer(), - fee_collector: accounts.core_fee_collector(), - system_program: accounts.system_program(), - }, - &[emitter_seeds, message_seeds], - ), - args, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PublishMessage<'info>, +{ + crate::legacy::cpi::post_message_unreliable( + CpiContext::new_with_signer( + accounts.core_bridge_program(), + crate::legacy::cpi::PostMessageUnreliable { + config: accounts.core_bridge_config(), + message: new_message.to_account_info(), + emitter: accounts.core_emitter_authority(), + emitter_sequence: accounts.core_emitter_sequence(), + payer: accounts.payer(), + fee_collector: accounts.core_fee_collector(), + system_program: accounts.system_program(), + }, + signer_seeds.unwrap_or_default(), ), - None => crate::legacy::cpi::post_message_unreliable( - CpiContext::new_with_signer( - accounts.core_bridge_program(), - crate::legacy::cpi::PostMessageUnreliable { - config: accounts.core_bridge_config(), - message: accounts.core_message(), - emitter, - emitter_sequence: accounts.core_emitter_sequence(), - payer: accounts.payer(), - fee_collector: accounts.core_fee_collector(), - system_program: accounts.system_program(), - }, - &[emitter_seeds], - ), - args, + args, + ) +} + +fn handle_prepared_message_v1<'info, A>( + accounts: &A, + new_message: &AccountInfo<'info>, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: PublishMessage<'info>, +{ + crate::legacy::cpi::post_message( + CpiContext::new_with_signer( + accounts.core_bridge_program(), + crate::legacy::cpi::PostMessage { + config: accounts.core_bridge_config(), + message: new_message.to_account_info(), + emitter: None, + emitter_sequence: accounts.core_emitter_sequence(), + payer: accounts.payer(), + fee_collector: accounts.core_fee_collector(), + system_program: accounts.system_program(), + }, + signer_seeds.unwrap_or_default(), ), - } + PostMessageArgs { + nonce: 420, // not checked + payload: Vec::new(), + commitment: Commitment::Finalized, // not checked + }, + ) } diff --git a/solana/programs/core-bridge/src/sdk/mod.rs b/solana/programs/core-bridge/src/sdk/mod.rs index eee5253ff6..290582025f 100644 --- a/solana/programs/core-bridge/src/sdk/mod.rs +++ b/solana/programs/core-bridge/src/sdk/mod.rs @@ -1,23 +1,35 @@ //! **ATTENTION INTEGRATORS!** Core Bridge Program developer kit. It is recommended to use -//! [sdk::cpi](mod@crate::sdk::cpi) for invoking Core Bridge instructions as opposed to the -//! code-generated Anchor CPI (found in [cpi](mod@crate::cpi)) and legacy CPI (found in -//! [legacy::cpi](mod@crate::legacy::cpi)). +//! [sdk::cpi](crate::sdk::cpi) for invoking Core Bridge instructions as opposed to the +//! code-generated Anchor CPI (found in [cpi](crate::cpi)) and legacy CPI (found in +//! [legacy::cpi](crate::legacy::cpi)). +#[doc(inline)] pub use crate::{ constants::{PROGRAM_EMITTER_SEED_PREFIX, SOLANA_CHAIN}, - state, types, + legacy::instruction::PostMessageArgs, + processor::{InitMessageV1Args, WriteEncodedVaaArgs, WriteMessageV1Args}, + types, + zero_copy::{Config, LoadZeroCopy, MessageAccount, PostedMessageV1, VaaAccount}, }; -/// Methods useful for interacting with the Core Bridge program via CPI if your program composes -/// with this program. +/// Set of structs mirroring the structs deriving Accounts, where each field is a Pubkey. This +/// is useful for specifying accounts for a client. +pub mod accounts { + pub use crate::{accounts::*, legacy::accounts::*}; +} + #[cfg(feature = "cpi")] pub mod cpi; -/// The program ID of the Core Bridge program. -pub static PROGRAM_ID: anchor_lang::prelude::Pubkey = crate::ID; +/// Instruction builders. These should be used directly when one wants to serialize instruction +/// data when speciying instructions on a client. +pub mod instruction { + pub use crate::{instruction::*, legacy::instruction as legacy}; +} -/// Convenient method to determine the space required for a `PostedMessageV1` account when it is -/// being prepared via `init_message_v1` and `process_message_v1`. -pub fn compute_init_message_v1_space(payload_size: usize) -> usize { +/// Convenient method to determine the space required for a [PostedMessageV1] account before the +/// account is initialized via +/// [init_message_v1](crate::wormhole_core_bridge_solana::init_message_v1). +pub fn compute_prepared_message_space(payload_size: usize) -> usize { crate::state::PostedMessageV1::BYTES_START + payload_size } diff --git a/solana/programs/core-bridge/src/state/encoded_vaa.rs b/solana/programs/core-bridge/src/state/encoded_vaa.rs index e59873984f..7a62518ed2 100644 --- a/solana/programs/core-bridge/src/state/encoded_vaa.rs +++ b/solana/programs/core-bridge/src/state/encoded_vaa.rs @@ -1,9 +1,10 @@ use std::ops::Deref; -use crate::{error::CoreBridgeError, types::VaaVersion}; use anchor_lang::prelude::*; use wormhole_raw_vaas::Vaa; +use crate::error::CoreBridgeError; + /// Encoded VAA's processing status. #[derive( Default, Copy, Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace, @@ -26,16 +27,36 @@ pub struct Header { pub status: ProcessingStatus, /// The authority that has write privilege to this account. pub write_authority: Pubkey, - /// VAA version. Only when the VAA is verified is this version set to something that is not - /// [Unset](VaaVersion::Unset). - pub version: VaaVersion, + /// VAA version. Only when the VAA is verified is this version set to a value. + pub version: u8, +} + +/// Representation of VAA versions. +#[non_exhaustive] +pub enum VaaVersion<'a> { + V1(Vaa<'a>), +} + +impl<'a> VaaVersion<'a> { + pub fn v1(&'a self) -> Option<&'a Vaa<'a>> { + match self { + Self::V1(inner) => Some(inner), + } + } +} + +impl<'a> AsRef<[u8]> for VaaVersion<'a> { + fn as_ref(&self) -> &[u8] { + match self { + Self::V1(inner) => inner.as_ref(), + } + } } /// Account used to warehouse VAA buffer. /// /// NOTE: This account should not be used by an external application unless the header's status is -/// `Verified`. It is encouraged to use the `EncodedVaa` zero-copy account struct instead. See -/// [zero_copy](mod@crate::zero_copy) for more info. +/// `Verified`. It is encouraged to use the `EncodedVaa` zero-copy account struct instead. #[account] #[derive(Debug, PartialEq, Eq)] pub struct EncodedVaa { @@ -47,18 +68,17 @@ pub struct EncodedVaa { impl EncodedVaa { /// Index of the first byte of the VAA buffer. - pub(crate) const BYTES_START: usize = 8 // DISCRIMINATOR + pub(crate) const VAA_START: usize = 8 // DISCRIMINATOR + crate::state::Header::INIT_SPACE + 4 // bytes.len() ; - /// Return VAA as zero-copy reader. - pub fn v1(&self) -> Result { - require!( - self.header.version == VaaVersion::V1, - CoreBridgeError::InvalidVaaVersion - ); - Ok(Vaa::parse(&self.buf).unwrap()) + /// Return as [VaaVersion] if the version number is valid. + pub fn as_vaa(&self) -> Result { + match self.version { + 1 => Ok(VaaVersion::V1(Vaa::parse(&self.buf).unwrap())), + _ => err!(CoreBridgeError::InvalidVaaVersion), + } } } diff --git a/solana/programs/core-bridge/src/types.rs b/solana/programs/core-bridge/src/types.rs index a8e7ab8ea7..93b1f9bd43 100644 --- a/solana/programs/core-bridge/src/types.rs +++ b/solana/programs/core-bridge/src/types.rs @@ -6,60 +6,6 @@ use anchor_lang::prelude::*; use solana_program::keccak; use wormhole_io::{Readable, Writeable}; -/// Representation of VAA versions numbers, where [Unset](VaaVersion::Unset) represents unverified -/// VAAs in various VAA accounts. -#[derive( - Default, Copy, Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace, -)] -#[non_exhaustive] -pub enum VaaVersion { - #[default] - Unset, - V1, -} - -impl VaaVersion { - const UNSET: u8 = 0; - const V_1: u8 = 1; -} - -impl From for u8 { - fn from(value: VaaVersion) -> Self { - match value { - VaaVersion::Unset => VaaVersion::UNSET, - VaaVersion::V1 => VaaVersion::V_1, - } - } -} - -impl Readable for VaaVersion { - const SIZE: Option = Some(VaaVersion::INIT_SPACE); - - fn read(reader: &mut R) -> std::io::Result { - match u8::read(reader)? { - VaaVersion::UNSET => Ok(VaaVersion::Unset), - VaaVersion::V_1 => Ok(VaaVersion::V1), - _ => Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid vaa version", - )), - } - } -} - -impl Writeable for VaaVersion { - fn write(&self, writer: &mut W) -> std::io::Result<()> { - match self { - VaaVersion::Unset => VaaVersion::UNSET.write(writer), - VaaVersion::V1 => VaaVersion::V_1.write(writer), - } - } - - fn written_size(&self) -> usize { - VaaVersion::INIT_SPACE - } -} - /// Representation of Solana's commitment levels. This enum is not exhaustive because Wormhole only /// considers these two commitment levels in its Guardian observation. /// @@ -240,9 +186,9 @@ impl fmt::Display for MessageHash { } } -/// This type is kind of silly. But because `PostedMessageV1` has the emitter chain ID as a field, -/// which is unnecessary since it is always Solana's chain ID, we use this type to guarantee that -/// the encoded chain ID is always `1`. +/// This type is kind of silly. But because [PostedMessageV1](crate::state::PostedMessageV1) has the +/// emitter chain ID as a field, which is unnecessary since it is always Solana's chain ID, we use +/// this type to guarantee that the encoded chain ID is always `1`. #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] pub struct ChainIdSolanaOnly { chain_id: u16, diff --git a/solana/programs/core-bridge/src/utils/cpi.rs b/solana/programs/core-bridge/src/utils/cpi.rs new file mode 100644 index 0000000000..6ea4c4a8ca --- /dev/null +++ b/solana/programs/core-bridge/src/utils/cpi.rs @@ -0,0 +1,39 @@ +use anchor_lang::{prelude::*, system_program}; + +/// Trait for invoking the System program's create account instruction. +pub trait CreateAccount<'info> { + fn system_program(&self) -> AccountInfo<'info>; + + /// Signer that has the lamports to transfer to new account. + fn payer(&self) -> AccountInfo<'info>; +} + +/// Method for invoking the System program's create account instruction. This method may be useful +/// if it is inconvenient to use Anchor's `init` account macro directive. +/// +/// NOTE: This method does not serialize any data into your new account. You will need to serialize +/// this data by borrowing mutable data and writing to it. +pub fn create_account<'info, A>( + accounts: &A, + new_account: &AccountInfo<'info>, + data_len: usize, + owner: &Pubkey, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: CreateAccount<'info>, +{ + system_program::create_account( + CpiContext::new_with_signer( + accounts.system_program(), + system_program::CreateAccount { + from: accounts.payer(), + to: new_account.to_account_info(), + }, + signer_seeds.unwrap_or_default(), + ), + Rent::get().map(|rent| rent.minimum_balance(data_len))?, + data_len.try_into().unwrap(), + owner, + ) +} diff --git a/solana/programs/core-bridge/src/utils/mod.rs b/solana/programs/core-bridge/src/utils/mod.rs index c22c3171fd..a32bd04f18 100644 --- a/solana/programs/core-bridge/src/utils/mod.rs +++ b/solana/programs/core-bridge/src/utils/mod.rs @@ -1,6 +1,9 @@ //! Utilities for the Core Bridge Program. +pub mod cpi; -use anchor_lang::{prelude::AccountInfo, Result}; +pub mod vaa; + +use anchor_lang::prelude::*; /// Compute quorum based on the number of guardians in a guardian set. #[inline] @@ -9,10 +12,7 @@ pub fn quorum(num_guardians: usize) -> usize { } /// Close an account by transferring all its lamports to another account. -pub fn close_account<'info>( - info: AccountInfo<'info>, - sol_destination: AccountInfo<'info>, -) -> Result<()> { +pub fn close_account(info: &AccountInfo, sol_destination: &AccountInfo) -> Result<()> { // Transfer tokens from the account to the sol_destination. let dest_starting_lamports = sol_destination.lamports(); **sol_destination.lamports.borrow_mut() = diff --git a/solana/programs/core-bridge/src/utils/vaa.rs b/solana/programs/core-bridge/src/utils/vaa.rs index 81345b6ae5..93c3762c33 100644 --- a/solana/programs/core-bridge/src/utils/vaa.rs +++ b/solana/programs/core-bridge/src/utils/vaa.rs @@ -1,30 +1,56 @@ -use crate::types::Timestamp; -use anchor_lang::solana_program::keccak; +use crate::zero_copy::VaaAccount; +use anchor_lang::prelude::*; -/// Compute quorum based on the number of guardians in a guardian set. -#[inline] -pub fn quorum(num_guardians: usize) -> usize { - (2 * num_guardians) / 3 + 1 -} +/// This method provides a way to prevent replay attacks on VAAs. It creates a PDA for your program +/// using seeds \[emitter_address, emitter_chain, sequence\]. By calling this method, it creates an +/// account of one byte (storing the bump of this PDA address). If your instruction handler is +/// called again, this step will fail because the account will already exist. +pub fn claim_vaa<'info, A>( + accounts: &A, + claim_acc_info: &AccountInfo<'info>, + program_id: &Pubkey, + vaa: &VaaAccount, +) -> Result<()> +where + A: super::cpi::CreateAccount<'info>, +{ + let (emitter_address, emitter_chain, sequence) = { + let (addr, chain, seq) = vaa.try_emitter_info()?; + (addr, chain.to_be_bytes(), seq.to_be_bytes()) + }; + + // First make sure the claim address is derived as what we expect. + let (expected_addr, claim_bump) = Pubkey::find_program_address( + &[ + emitter_address.as_ref(), + emitter_chain.as_ref(), + sequence.as_ref(), + ], + program_id, + ); + require_keys_eq!( + claim_acc_info.key(), + expected_addr, + ErrorCode::ConstraintSeeds + ); + + // In the legacy implementation, claim accounts stored a boolean (1 byte). Instead, we repurpose + // this account to store something a little more useful: the bump of the PDA address. + super::cpi::create_account( + accounts, + claim_acc_info, + 1, + program_id, + Some(&[&[ + emitter_address.as_ref(), + emitter_chain.as_ref(), + sequence.as_ref(), + &[claim_bump], + ]]), + )?; -#[inline] -pub fn compute_message_hash( - timestamp: Timestamp, - nonce: u32, - emitter_chain: u16, - emitter_address: &[u8; 32], - sequence: u64, - consistency_level: u8, - payload: &[u8], -) -> keccak::Hash { - let mut body = Vec::with_capacity(51 + payload.len()); - body.extend_from_slice(×tamp.to_be_bytes()); - body.extend_from_slice(&nonce.to_be_bytes()); - body.extend_from_slice(&emitter_chain.to_be_bytes()); - body.extend_from_slice(emitter_address.as_ref()); - body.extend_from_slice(&sequence.to_be_bytes()); - body.push(consistency_level); - body.extend_from_slice(payload); + claim_acc_info.data.borrow_mut()[0] = claim_bump; - keccak::hash(&body) + // Done. + Ok(()) } diff --git a/solana/programs/core-bridge/src/zero_copy/config.rs b/solana/programs/core-bridge/src/zero_copy/config.rs index 3dee781e21..ba4679db64 100644 --- a/solana/programs/core-bridge/src/zero_copy/config.rs +++ b/solana/programs/core-bridge/src/zero_copy/config.rs @@ -1,13 +1,15 @@ +use std::cell::Ref; + use crate::state; use anchor_lang::{ - prelude::{error, require_eq, ErrorCode, Result}, + prelude::{error, require_eq, require_keys_eq, AccountInfo, ErrorCode, Result}, Space, }; /// Account used to store the current configuration of the bridge, including tracking Wormhole fee /// payments. For governance decrees, the guardian set index is used to determine whether a decree /// was attested for using the latest guardian set. -pub struct Config<'a>(&'a [u8]); +pub struct Config<'a>(Ref<'a, &'a mut [u8]>); impl<'a> Config<'a> { /// The current guardian set index, used to decide which signature sets to accept. @@ -32,12 +34,15 @@ impl<'a> Config<'a> { u64::from_le_bytes(self.0[16..24].try_into().unwrap()) } - pub fn parse(span: &'a [u8]) -> Result { + pub fn parse(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let data = acc_info.try_borrow_data()?; require_eq!( - span.len(), + data.len(), state::Config::INIT_SPACE, ErrorCode::AccountDidNotDeserialize ); - Ok(Self(span)) + Ok(Self(data)) } } diff --git a/solana/programs/core-bridge/src/zero_copy/encoded_vaa.rs b/solana/programs/core-bridge/src/zero_copy/encoded_vaa.rs deleted file mode 100644 index 28e021fce6..0000000000 --- a/solana/programs/core-bridge/src/zero_copy/encoded_vaa.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::{error::CoreBridgeError, state, types::VaaVersion}; -use anchor_lang::{ - prelude::{error, require, AnchorDeserialize, ErrorCode, Pubkey, Result}, - Discriminator, -}; -use wormhole_raw_vaas::Vaa; - -/// Account used to warehouse VAA buffer. -pub struct EncodedVaa<'a>(&'a [u8]); - -impl<'a> EncodedVaa<'a> { - pub const DISCRIMINATOR: [u8; 8] = state::EncodedVaa::DISCRIMINATOR; - pub const VAA_START: usize = state::EncodedVaa::BYTES_START - Self::DISC_LEN; - - const DISC_LEN: usize = Self::DISCRIMINATOR.len(); - - /// Processing status. **This encoded VAA is only considered usable when this status is set - /// to [Verified](state::ProcessingStatus::Verified).** - pub fn status(&self) -> state::ProcessingStatus { - AnchorDeserialize::deserialize(&mut &self.0[..1]).unwrap() - } - - /// The authority that has write privilege to this account. - pub fn write_authority(&self) -> Pubkey { - Pubkey::try_from(&self.0[1..33]).unwrap() - } - - /// VAA version. Only when the VAA is verified is this version set to something that is not - /// [Unset](VaaVersion::Unset). - pub fn version(&self) -> VaaVersion { - AnchorDeserialize::deserialize(&mut &self.0[33..34]).unwrap() - } - - pub fn vaa_size(&self) -> usize { - let mut buf = &self.0[34..Self::VAA_START]; - u32::deserialize(&mut buf).unwrap().try_into().unwrap() - } - - /// VAA (Version 1). - pub fn v1(&self) -> Result> { - require!( - self.version() == VaaVersion::V1, - CoreBridgeError::InvalidVaaVersion - ); - Ok(Vaa::parse(&self.0[Self::VAA_START..]).unwrap()) - } - - pub(crate) fn v1_unverified(&self) -> Result> { - Vaa::parse(&self.0[Self::VAA_START..]).map_err(|_| error!(CoreBridgeError::CannotParseVaa)) - } - - /// Parse account data assumed to match the [EncodedVaa](state::EncodedVaa) schema. - /// - /// NOTE: There is no ownership check because [AccountInfo](anchor_lang::prelude::AccountInfo) - /// is not passed into this method. - pub fn parse(span: &'a [u8]) -> Result { - let vaa = Self::parse_unverified(span)?; - - // We only allow verified VAAs to be read. - require!( - vaa.status() == state::ProcessingStatus::Verified, - CoreBridgeError::UnverifiedVaa - ); - - Ok(vaa) - } - - pub(crate) fn parse_unverified(span: &'a [u8]) -> Result { - require!( - span.len() > Self::DISC_LEN, - ErrorCode::AccountDiscriminatorNotFound - ); - require!( - span[..Self::DISC_LEN] == Self::DISCRIMINATOR, - ErrorCode::AccountDiscriminatorMismatch - ); - - Ok(Self(&span[Self::DISC_LEN..])) - } - - /// Method to try to deserialize account data as VAA (Version 1). - pub fn try_v1(span: &'a [u8]) -> Result> { - Self::parse(span)?.v1() - } -} diff --git a/solana/programs/core-bridge/src/zero_copy/message/mod.rs b/solana/programs/core-bridge/src/zero_copy/message/mod.rs new file mode 100644 index 0000000000..5771cd37f8 --- /dev/null +++ b/solana/programs/core-bridge/src/zero_copy/message/mod.rs @@ -0,0 +1,49 @@ +mod posted_message_v1; +pub use posted_message_v1::*; + +use anchor_lang::prelude::{err, error, require, require_keys_eq, AccountInfo, ErrorCode, Result}; + +use super::LoadZeroCopy; + +#[non_exhaustive] +pub enum MessageAccount<'a> { + V1(PostedMessageV1<'a>), + UnreliableV1(PostedMessageV1<'a>), +} + +impl<'a> MessageAccount<'a> { + pub fn v1(&'a self) -> Option<&'a PostedMessageV1<'a>> { + match self { + Self::V1(inner) => Some(inner), + _ => None, + } + } + + pub fn v1_unreliable(&'a self) -> Option<&'a PostedMessageV1<'a>> { + match self { + Self::UnreliableV1(inner) => Some(inner), + _ => None, + } + } +} + +impl<'a> LoadZeroCopy<'a> for MessageAccount<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let discriminator = { + let data = acc_info.try_borrow_data()?; + require!(data.len() > 4, ErrorCode::AccountDidNotDeserialize); + + data[..4].try_into().unwrap() + }; + + match discriminator { + PostedMessageV1::DISC => Ok(Self::V1(PostedMessageV1::new(acc_info)?)), + PostedMessageV1::UNRELIABLE_DISC => { + Ok(Self::UnreliableV1(PostedMessageV1::new(acc_info)?)) + } + _ => err!(ErrorCode::AccountDidNotDeserialize), + } + } +} diff --git a/solana/programs/core-bridge/src/zero_copy/message/posted_message_v1.rs b/solana/programs/core-bridge/src/zero_copy/message/posted_message_v1.rs new file mode 100644 index 0000000000..1c43ceaa49 --- /dev/null +++ b/solana/programs/core-bridge/src/zero_copy/message/posted_message_v1.rs @@ -0,0 +1,98 @@ +use std::cell::Ref; + +use crate::{state, types::Timestamp}; +use anchor_lang::prelude::{ + error, require, require_eq, require_keys_eq, AccountInfo, ErrorCode, Pubkey, Result, +}; + +/// Account used to store a published Wormhole message. There are two types of message accounts, +/// one for reliable messages (discriminator == "msg\0") and unreliable messages (discriminator == +/// "msu\0"). +pub struct PostedMessageV1<'a>(Ref<'a, &'a mut [u8]>); + +impl<'a> PostedMessageV1<'a> { + pub const DISC: [u8; 4] = state::POSTED_MESSAGE_V1_DISCRIMINATOR; + pub const UNRELIABLE_DISC: [u8; 4] = state::POSTED_MESSAGE_V1_UNRELIABLE_DISCRIMINATOR; + pub const PAYLOAD_START: usize = 95; + + pub fn discriminator(&self) -> [u8; 4] { + self.0[..4].try_into().unwrap() + } + + /// Level of consistency requested by the emitter. + pub fn consistency_level(&self) -> u8 { + self.0[4] + } + + /// Authority used to write the message. This field is set to default when the message is + /// posted. + pub fn emitter_authority(&self) -> Pubkey { + Pubkey::try_from(&self.0[5..37]).unwrap() + } + + /// If a large message is been written, this is the expected length of the message. When this + /// message is posted, this value will be overwritten as zero. + pub fn status(&self) -> state::MessageStatus { + anchor_lang::AnchorDeserialize::deserialize(&mut &self.0[37..38]).unwrap() + } + + /// Time the posted message was created. + pub fn posted_timestamp(&self) -> Timestamp { + u32::from_le_bytes(self.0[41..45].try_into().unwrap()).into() + } + + /// Unique id for this message. + pub fn nonce(&self) -> u32 { + u32::from_le_bytes(self.0[45..49].try_into().unwrap()) + } + + /// Sequence number of this message. + pub fn sequence(&self) -> u64 { + u64::from_le_bytes(self.0[49..57].try_into().unwrap()) + } + + /// Emitter of the message. This may either be the emitter authority or a program ID. + pub fn emitter(&self) -> Pubkey { + Pubkey::try_from(&self.0[59..91]).unwrap() + } + + pub fn payload_size(&self) -> usize { + u32::from_le_bytes(self.0[91..Self::PAYLOAD_START].try_into().unwrap()) + .try_into() + .unwrap() + } + + /// Encoded message. + pub fn payload(&self) -> &[u8] { + &self.0[Self::PAYLOAD_START..] + } + + pub(super) fn new(acc_info: &'a AccountInfo) -> Result { + let parsed = Self(acc_info.try_borrow_data()?); + require!( + parsed.0.len() >= Self::PAYLOAD_START, + ErrorCode::AccountDidNotDeserialize + ); + require_eq!( + parsed.0.len(), + Self::PAYLOAD_START + parsed.payload_size(), + ErrorCode::AccountDidNotDeserialize + ); + + Ok(parsed) + } +} + +impl<'a> crate::zero_copy::LoadZeroCopy<'a> for PostedMessageV1<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let parsed = Self::new(acc_info)?; + require!( + parsed.discriminator() == Self::DISC, + ErrorCode::AccountDidNotDeserialize + ); + + Ok(parsed) + } +} diff --git a/solana/programs/core-bridge/src/zero_copy/mod.rs b/solana/programs/core-bridge/src/zero_copy/mod.rs index 6abf41fc93..1716cda0ce 100644 --- a/solana/programs/core-bridge/src/zero_copy/mod.rs +++ b/solana/programs/core-bridge/src/zero_copy/mod.rs @@ -1,18 +1,18 @@ //! Zero-copy account structs for the Core Bridge Program. Wherever possible, it is recommended to -//! use these structs instead of using Anchor's [anchor_lang::prelude::Account] to deserialize these -//! accounts. +//! use these structs instead of using Anchor's [Account](anchor_lang::prelude::Account) to +//! deserialize these accounts. mod config; pub use config::*; -mod encoded_vaa; -pub use encoded_vaa::*; +mod message; +pub use message::*; -mod posted_message_v1; -pub use posted_message_v1::*; +mod vaa; +pub use vaa::*; -mod posted_message_v1_unreliable; -pub use posted_message_v1_unreliable::*; +use anchor_lang::prelude::{AccountInfo, Result}; -mod posted_vaa_v1; -pub use posted_vaa_v1::*; +pub trait LoadZeroCopy<'a>: Sized { + fn load(acc_info: &'a AccountInfo) -> Result; +} diff --git a/solana/programs/core-bridge/src/zero_copy/posted_message_v1.rs b/solana/programs/core-bridge/src/zero_copy/posted_message_v1.rs deleted file mode 100644 index 7855a0434b..0000000000 --- a/solana/programs/core-bridge/src/zero_copy/posted_message_v1.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{state, types::Timestamp}; -use anchor_lang::prelude::{require, ErrorCode, Pubkey}; - -/// Account used to store a published Wormhole message. -/// -/// NOTE: If your integration requires reusable message accounts, please see -/// [PostedMessageV1Unreliable](crate::zero_copy::PostedMessageV1Unreliable). -pub struct PostedMessageV1<'a>(&'a [u8]); - -impl<'a> PostedMessageV1<'a> { - pub const DISCRIMINATOR: [u8; 4] = state::POSTED_MESSAGE_V1_DISCRIMINATOR; - pub const PAYLOAD_START: usize = 91; - - const DISC_LEN: usize = Self::DISCRIMINATOR.len(); - - /// Level of consistency requested by the emitter. - pub fn consistency_level(&self) -> u8 { - self.0[0] - } - - /// Authority used to write the message. This field is set to default when the message is - /// posted. - pub fn emitter_authority(&self) -> Pubkey { - Pubkey::try_from(&self.0[1..33]).unwrap() - } - - /// If a large message is been written, this is the expected length of the message. When this - /// message is posted, this value will be overwritten as zero. - pub fn status(&self) -> state::MessageStatus { - anchor_lang::AnchorDeserialize::deserialize(&mut &self.0[33..34]).unwrap() - } - - /// Time the posted message was created. - pub fn posted_timestamp(&self) -> Timestamp { - u32::from_le_bytes(self.0[37..41].try_into().unwrap()).into() - } - - /// Unique id for this message. - pub fn nonce(&self) -> u32 { - u32::from_le_bytes(self.0[41..45].try_into().unwrap()) - } - - /// Sequence number of this message. - pub fn sequence(&self) -> u64 { - u64::from_le_bytes(self.0[45..53].try_into().unwrap()) - } - - /// Emitter of the message. This may either be the emitter authority or a program ID. - pub fn emitter(&self) -> Pubkey { - Pubkey::try_from(&self.0[55..87]).unwrap() - } - - pub fn payload_size(&self) -> usize { - u32::from_le_bytes(self.0[87..Self::PAYLOAD_START].try_into().unwrap()) - .try_into() - .unwrap() - } - - /// Encoded message. - pub fn payload(&self) -> &'a [u8] { - &self.0[Self::PAYLOAD_START..] - } - - /// Parse account data assumed to match the [PostedMessageV1](state::PostedMessageV1) schema. - /// - /// NOTE: There is no ownership check because [AccountInfo](anchor_lang::prelude::AccountInfo) - /// is not passed into this method. - pub fn parse(span: &'a [u8]) -> anchor_lang::Result { - require!( - span.len() > Self::DISC_LEN, - ErrorCode::AccountDidNotDeserialize - ); - require!( - span[..Self::DISC_LEN] == Self::DISCRIMINATOR, - ErrorCode::AccountDidNotDeserialize - ); - - Ok(Self(&span[Self::DISC_LEN..])) - } -} diff --git a/solana/programs/core-bridge/src/zero_copy/posted_message_v1_unreliable.rs b/solana/programs/core-bridge/src/zero_copy/posted_message_v1_unreliable.rs deleted file mode 100644 index 4500b1a9ce..0000000000 --- a/solana/programs/core-bridge/src/zero_copy/posted_message_v1_unreliable.rs +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{state, types::Timestamp}; -use anchor_lang::prelude::{require, ErrorCode, Pubkey}; - -/// Account used to store a published (reusable) Wormhole message. -pub struct PostedMessageV1Unreliable<'a>(&'a [u8]); - -impl<'a> PostedMessageV1Unreliable<'a> { - pub const DISCRIMINATOR: [u8; 4] = state::POSTED_MESSAGE_V1_UNRELIABLE_DISCRIMINATOR; - pub const PAYLOAD_START: usize = 91; - - const DISC_LEN: usize = Self::DISCRIMINATOR.len(); - - /// Level of consistency requested by the emitter. - pub fn consistency_level(&self) -> u8 { - self.0[0] - } - - /// Authority used to write the message. This field is set to default when the message is - /// posted. - pub fn emitter_authority(&self) -> Pubkey { - Pubkey::try_from(&self.0[1..33]).unwrap() - } - - /// If a large message is been written, this is the expected length of the message. When this - /// message is posted, this value will be overwritten as zero. - pub fn status(&self) -> state::MessageStatus { - anchor_lang::AnchorDeserialize::deserialize(&mut &self.0[33..34]).unwrap() - } - - /// Time the posted message was created. - pub fn posted_timestamp(&self) -> Timestamp { - u32::from_le_bytes(self.0[37..41].try_into().unwrap()).into() - } - - /// Unique id for this message. - pub fn nonce(&self) -> u32 { - u32::from_le_bytes(self.0[41..45].try_into().unwrap()) - } - - /// Sequence number of this message. - pub fn sequence(&self) -> u64 { - u64::from_le_bytes(self.0[45..53].try_into().unwrap()) - } - - /// Emitter of the message. This may either be the emitter authority or a program ID. - pub fn emitter(&self) -> Pubkey { - Pubkey::try_from(&self.0[55..87]).unwrap() - } - - pub fn payload_size(&self) -> usize { - u32::from_le_bytes(self.0[87..Self::PAYLOAD_START].try_into().unwrap()) - .try_into() - .unwrap() - } - - /// Encoded message. - pub fn payload(&self) -> &'a [u8] { - &self.0[Self::PAYLOAD_START..] - } - - /// Parse account data assumed to match the - /// [PostedMessageV1Unreliable](state::PostedMessageV1Unreliable) schema. - /// - /// NOTE: There is no ownership check because [AccountInfo](anchor_lang::prelude::AccountInfo) - /// is not passed into this method. - pub fn parse(span: &'a [u8]) -> anchor_lang::Result { - require!( - span.len() > Self::DISC_LEN, - ErrorCode::AccountDidNotDeserialize - ); - require!( - span[..Self::DISC_LEN] == Self::DISCRIMINATOR, - ErrorCode::AccountDidNotDeserialize - ); - - Ok(Self(&span[Self::DISC_LEN..])) - } -} diff --git a/solana/programs/core-bridge/src/zero_copy/vaa/encoded_vaa.rs b/solana/programs/core-bridge/src/zero_copy/vaa/encoded_vaa.rs new file mode 100644 index 0000000000..4562612f43 --- /dev/null +++ b/solana/programs/core-bridge/src/zero_copy/vaa/encoded_vaa.rs @@ -0,0 +1,112 @@ +use std::cell::Ref; + +use crate::{error::CoreBridgeError, state}; +use anchor_lang::{ + prelude::{ + err, error, require, require_eq, require_keys_eq, AccountInfo, AnchorDeserialize, + ErrorCode, Pubkey, Result, + }, + Discriminator, +}; +use wormhole_raw_vaas::Vaa; + +/// Account used to warehouse VAA buffer. +pub struct EncodedVaa<'a>(Ref<'a, &'a mut [u8]>); + +impl<'a> EncodedVaa<'a> { + pub const DISC: [u8; 8] = state::EncodedVaa::DISCRIMINATOR; + pub const VAA_START: usize = state::EncodedVaa::VAA_START; + + pub fn discriminator(&self) -> [u8; 8] { + self.0[..8].try_into().unwrap() + } + + /// Processing status. **This encoded VAA is only considered usable when this status is set + /// to [Verified](state::ProcessingStatus::Verified).** + pub fn status(&self) -> state::ProcessingStatus { + AnchorDeserialize::deserialize(&mut &self.0[8..9]).unwrap() + } + + /// The authority that has write privilege to this account. + pub fn write_authority(&self) -> Pubkey { + Pubkey::try_from(&self.0[9..41]).unwrap() + } + + /// VAA version. Only when the VAA is verified is this version set to something that is not + /// [Unset](VaaVersion::Unset). + pub fn version(&self) -> u8 { + self.0[41] + } + + pub fn vaa_size(&self) -> usize { + let mut buf = &self.0[42..Self::VAA_START]; + u32::deserialize(&mut buf).unwrap().try_into().unwrap() + } + + pub fn buf(&self) -> &[u8] { + &self.0[Self::VAA_START..] + } + + pub fn as_vaa(&self) -> Result { + match self.version() { + 1 => Ok(state::VaaVersion::V1( + Vaa::parse(&self.0[Self::VAA_START..]).unwrap(), + )), + _ => err!(CoreBridgeError::InvalidVaaVersion), + } + } + + pub(super) fn new(acc_info: &'a AccountInfo) -> Result { + let parsed = Self(acc_info.try_borrow_data()?); + parsed.require_correct_size()?; + + // We only allow verified VAAs to be read. + require!( + parsed.status() == state::ProcessingStatus::Verified, + CoreBridgeError::UnverifiedVaa + ); + Ok(parsed) + } + + pub(crate) fn parse_unverified(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let parsed = Self(acc_info.try_borrow_data()?); + parsed.require_correct_size()?; + + require!( + parsed.discriminator() == Self::DISC, + ErrorCode::AccountDiscriminatorMismatch + ); + + Ok(parsed) + } + + fn require_correct_size(&self) -> Result<()> { + require!( + self.0.len() >= Self::VAA_START, + ErrorCode::AccountDidNotDeserialize + ); + require_eq!( + self.0.len(), + Self::VAA_START + self.vaa_size(), + ErrorCode::AccountDidNotDeserialize + ); + Ok(()) + } +} + +impl<'a> crate::zero_copy::LoadZeroCopy<'a> for EncodedVaa<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let parsed = Self::new(acc_info)?; + + require!( + parsed.discriminator() == Self::DISC, + ErrorCode::AccountDiscriminatorMismatch + ); + + Ok(parsed) + } +} diff --git a/solana/programs/core-bridge/src/zero_copy/vaa/mod.rs b/solana/programs/core-bridge/src/zero_copy/vaa/mod.rs new file mode 100644 index 0000000000..f221a0a528 --- /dev/null +++ b/solana/programs/core-bridge/src/zero_copy/vaa/mod.rs @@ -0,0 +1,86 @@ +mod encoded_vaa; +pub use encoded_vaa::*; + +mod posted_vaa_v1; +pub use posted_vaa_v1::*; + +use anchor_lang::prelude::{err, error, require, require_keys_eq, AccountInfo, ErrorCode, Result}; +use wormhole_raw_vaas::Payload; + +use crate::state::VaaVersion; + +use super::LoadZeroCopy; + +#[non_exhaustive] +pub enum VaaAccount<'a> { + EncodedVaa(EncodedVaa<'a>), + PostedVaaV1(PostedVaaV1<'a>), +} + +impl<'a> VaaAccount<'a> { + pub fn version(&'a self) -> u8 { + match self { + Self::EncodedVaa(inner) => inner.version(), + Self::PostedVaaV1(_) => 1, + } + } + + pub fn try_emitter_info(&self) -> Result<([u8; 32], u16, u64)> { + match self { + Self::EncodedVaa(inner) => match inner.as_vaa()? { + VaaVersion::V1(vaa) => Ok(( + vaa.body().emitter_address(), + vaa.body().emitter_chain(), + vaa.body().sequence(), + )), + }, + Self::PostedVaaV1(inner) => Ok(( + inner.emitter_address(), + inner.emitter_chain(), + inner.sequence(), + )), + } + } + + pub fn try_payload(&self) -> Result { + match self { + Self::EncodedVaa(inner) => match inner.as_vaa()? { + VaaVersion::V1(vaa) => Ok(vaa.body().payload()), + }, + Self::PostedVaaV1(inner) => Ok(Payload::parse(inner.payload())), + } + } + + pub fn encoded_vaa(&'a self) -> Option<&'a EncodedVaa<'a>> { + match self { + Self::EncodedVaa(inner) => Some(inner), + _ => None, + } + } + + pub fn posted_vaa_v1(&'a self) -> Option<&'a PostedVaaV1<'a>> { + match self { + Self::PostedVaaV1(inner) => Some(inner), + _ => None, + } + } +} + +impl<'a> LoadZeroCopy<'a> for VaaAccount<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let discriminator = { + let data = acc_info.try_borrow_data()?; + require!(data.len() > 8, ErrorCode::AccountDidNotDeserialize); + + data[..8].try_into().unwrap() + }; + + match discriminator { + EncodedVaa::DISC => Ok(Self::EncodedVaa(EncodedVaa::new(acc_info)?)), + [118, 97, 97, 1, _, _, _, _] => Ok(Self::PostedVaaV1(PostedVaaV1::new(acc_info)?)), + _ => err!(ErrorCode::AccountDidNotDeserialize), + } + } +} diff --git a/solana/programs/core-bridge/src/zero_copy/posted_vaa_v1.rs b/solana/programs/core-bridge/src/zero_copy/vaa/posted_vaa_v1.rs similarity index 62% rename from solana/programs/core-bridge/src/zero_copy/posted_vaa_v1.rs rename to solana/programs/core-bridge/src/zero_copy/vaa/posted_vaa_v1.rs index 8a5cfd5dc9..87bdf9ff4b 100644 --- a/solana/programs/core-bridge/src/zero_copy/posted_vaa_v1.rs +++ b/solana/programs/core-bridge/src/zero_copy/vaa/posted_vaa_v1.rs @@ -1,32 +1,38 @@ +use std::cell::Ref; + use crate::{state, types::Timestamp}; use anchor_lang::{ - prelude::{require, ErrorCode, Pubkey}, + prelude::{ + error, require, require_eq, require_keys_eq, AccountInfo, ErrorCode, Pubkey, Result, + }, solana_program::keccak, }; /// Account used to store a verified VAA. -pub struct PostedVaaV1<'a>(&'a [u8]); +pub struct PostedVaaV1<'a>(Ref<'a, &'a mut [u8]>); impl<'a> PostedVaaV1<'a> { - pub const DISCRIMINATOR: [u8; 4] = state::POSTED_VAA_V1_DISCRIMINATOR; - pub const PAYLOAD_START: usize = 91; + pub const DISC: [u8; 4] = state::POSTED_VAA_V1_DISCRIMINATOR; + pub const PAYLOAD_START: usize = 95; pub const SEED_PREFIX: &'static [u8] = state::POSTED_VAA_V1_SEED_PREFIX; - const DISC_LEN: usize = Self::DISCRIMINATOR.len(); + pub fn discriminator(&self) -> [u8; 4] { + self.0[..4].try_into().unwrap() + } /// Level of consistency requested by the emitter. pub fn consistency_level(&self) -> u8 { - self.0[0] + self.0[4] } /// Time the message was submitted. pub fn timestamp(&self) -> Timestamp { - u32::from_le_bytes(self.0[1..5].try_into().unwrap()).into() + u32::from_le_bytes(self.0[5..9].try_into().unwrap()).into() } /// Pubkey of `SignatureSet` account that represent this VAA's signature verification. pub fn signature_set(&self) -> Pubkey { - Pubkey::try_from(&self.0[5..37]).unwrap() + Pubkey::try_from(&self.0[9..41]).unwrap() } /// Guardian set index used to verify signatures for `SignatureSet`. @@ -37,37 +43,37 @@ impl<'a> PostedVaaV1<'a> { /// the Core Bridge (other Core Bridge implementations require that the guardian set that /// attested for the governance VAA is the current one). pub fn guardian_set_index(&self) -> u32 { - u32::from_le_bytes(self.0[37..41].try_into().unwrap()) + u32::from_le_bytes(self.0[41..45].try_into().unwrap()) } /// Unique ID for this message. pub fn nonce(&self) -> u32 { - u32::from_le_bytes(self.0[41..45].try_into().unwrap()) + u32::from_le_bytes(self.0[45..49].try_into().unwrap()) } /// Sequence number of this message. pub fn sequence(&self) -> u64 { - u64::from_le_bytes(self.0[45..53].try_into().unwrap()) + u64::from_le_bytes(self.0[49..57].try_into().unwrap()) } /// The Wormhole chain ID denoting the origin of this message. pub fn emitter_chain(&self) -> u16 { - u16::from_le_bytes(self.0[53..55].try_into().unwrap()) + u16::from_le_bytes(self.0[57..59].try_into().unwrap()) } /// Emitter of the message. pub fn emitter_address(&self) -> [u8; 32] { - self.0[55..87].try_into().unwrap() + self.0[59..91].try_into().unwrap() } pub fn payload_size(&self) -> usize { - u32::from_le_bytes(self.0[87..Self::PAYLOAD_START].try_into().unwrap()) + u32::from_le_bytes(self.0[91..Self::PAYLOAD_START].try_into().unwrap()) .try_into() .unwrap() } /// Message payload. - pub fn payload(&self) -> &'a [u8] { + pub fn payload(&self) -> &[u8] { &self.0[Self::PAYLOAD_START..] } @@ -89,20 +95,31 @@ impl<'a> PostedVaaV1<'a> { keccak::hash(self.message_hash().as_ref()) } - /// Parse account data assumed to match the [PostedVaaV1](state::PostedVaaV1) schema. - /// - /// NOTE: There is no ownership check because [AccountInfo](anchor_lang::prelude::AccountInfo) - /// is not passed into this method. - pub fn parse(span: &'a [u8]) -> anchor_lang::Result { + pub(super) fn new(acc_info: &'a AccountInfo) -> Result { + let parsed = Self(acc_info.try_borrow_data()?); require!( - span.len() > Self::DISC_LEN, + parsed.0.len() >= Self::PAYLOAD_START, ErrorCode::AccountDidNotDeserialize ); + require_eq!( + parsed.0.len(), + Self::PAYLOAD_START + parsed.payload_size(), + ErrorCode::AccountDidNotDeserialize + ); + Ok(parsed) + } +} + +impl<'a> crate::zero_copy::LoadZeroCopy<'a> for PostedVaaV1<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!(*acc_info.owner, crate::ID, ErrorCode::ConstraintOwner); + + let parsed = Self::new(acc_info)?; require!( - span[..Self::DISC_LEN] == Self::DISCRIMINATOR, + parsed.discriminator() == Self::DISC, ErrorCode::AccountDidNotDeserialize ); - Ok(Self(&span[Self::DISC_LEN..])) + Ok(parsed) } } diff --git a/solana/programs/mock-cpi/src/processor/core_bridge/mod.rs b/solana/programs/mock-cpi/src/processor/core_bridge/mod.rs index 96d5ad8db0..eda403314b 100644 --- a/solana/programs/mock-cpi/src/processor/core_bridge/mod.rs +++ b/solana/programs/mock-cpi/src/processor/core_bridge/mod.rs @@ -4,5 +4,5 @@ pub use post_message::*; mod post_message_unreliable; pub use post_message_unreliable::*; -mod prepare_message_v1; -pub use prepare_message_v1::*; +mod prepare_message; +pub use prepare_message::*; diff --git a/solana/programs/mock-cpi/src/processor/core_bridge/post_message.rs b/solana/programs/mock-cpi/src/processor/core_bridge/post_message.rs index 41518619f6..de4b34f8d3 100644 --- a/solana/programs/mock-cpi/src/processor/core_bridge/post_message.rs +++ b/solana/programs/mock-cpi/src/processor/core_bridge/post_message.rs @@ -62,13 +62,7 @@ pub struct MockPostMessage<'info> { core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for MockPostMessage<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} - -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for MockPostMessage<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> for MockPostMessage<'info> { fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -79,16 +73,19 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for MockPostMessage<'info } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockPostMessage<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_emitter(&self) -> Option> { - self.core_custom_emitter.clone() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter_authority(&self) -> Option> { - self.core_program_emitter.clone() + fn core_emitter_authority(&self) -> AccountInfo<'info> { + match &self.core_program_emitter { + Some(program_emitter) => program_emitter.to_account_info(), + None => self.core_custom_emitter.as_ref().unwrap().to_account_info(), + } } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -100,10 +97,6 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockPostMessage<'inf .as_ref() .map(|acc| acc.to_account_info()) } - - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() - } } #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] @@ -123,39 +116,45 @@ pub fn mock_post_message(ctx: Context, args: MockPostMessageArg ) { (Some(_), _) => core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::ProgramMessage { program_id: crate::ID, nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[ - PROGRAM_EMITTER_SEED_PREFIX, - &[ctx.bumps["core_program_emitter"]], - ], Some(&[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - sequence.as_ref(), - &[ctx.bumps["core_message"]], + &[ + PROGRAM_EMITTER_SEED_PREFIX, + &[ctx.bumps["core_program_emitter"]], + ], + &[ + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence.as_ref(), + &[ctx.bumps["core_message"]], + ], ]), ), - (None, Some(_emitter_authority)) => core_bridge_sdk::cpi::publish_message( + (None, Some(_)) => core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::Message { nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[ - LEGACY_EMITTER_SEED_PREFIX, - &[ctx.bumps["core_custom_emitter"]], - ], Some(&[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - sequence.as_ref(), - &[ctx.bumps["core_message"]], + &[ + LEGACY_EMITTER_SEED_PREFIX, + &[ctx.bumps["core_custom_emitter"]], + ], + &[ + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence.as_ref(), + &[ctx.bumps["core_message"]], + ], ]), ), (None, None) => err!(ErrorCode::AccountNotEnoughKeys), diff --git a/solana/programs/mock-cpi/src/processor/core_bridge/post_message_unreliable.rs b/solana/programs/mock-cpi/src/processor/core_bridge/post_message_unreliable.rs index c20be70dd7..fabccbd20b 100644 --- a/solana/programs/mock-cpi/src/processor/core_bridge/post_message_unreliable.rs +++ b/solana/programs/mock-cpi/src/processor/core_bridge/post_message_unreliable.rs @@ -45,13 +45,9 @@ pub struct MockPostMessageUnreliable<'info> { core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for MockPostMessageUnreliable<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} - -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for MockPostMessageUnreliable<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockPostMessageUnreliable<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -62,12 +58,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for MockPostMessageUnreli } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockPostMessageUnreliable<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + fn core_bridge_config(&self) -> AccountInfo<'info> { self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -79,10 +79,6 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockPostMessageUnrel .as_ref() .map(|acc| acc.to_account_info()) } - - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() - } } #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] @@ -99,17 +95,20 @@ pub fn mock_post_message_unreliable( core_bridge_sdk::cpi::publish_message( ctx.accounts, + &ctx.accounts.core_message, core_bridge_sdk::cpi::PublishMessageDirective::UnreliableMessage { nonce, payload, commitment: core_bridge_sdk::types::Commitment::Finalized, }, - &[LEGACY_EMITTER_SEED_PREFIX, &[ctx.bumps["core_emitter"]]], Some(&[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - nonce.to_le_bytes().as_ref(), - &[ctx.bumps["core_message"]], + &[LEGACY_EMITTER_SEED_PREFIX, &[ctx.bumps["core_emitter"]]], + &[ + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + nonce.to_le_bytes().as_ref(), + &[ctx.bumps["core_message"]], + ], ]), ) } diff --git a/solana/programs/mock-cpi/src/processor/core_bridge/prepare_message_v1.rs b/solana/programs/mock-cpi/src/processor/core_bridge/prepare_message.rs similarity index 84% rename from solana/programs/mock-cpi/src/processor/core_bridge/prepare_message_v1.rs rename to solana/programs/mock-cpi/src/processor/core_bridge/prepare_message.rs index f7baa32378..b9713ab5f9 100644 --- a/solana/programs/mock-cpi/src/processor/core_bridge/prepare_message_v1.rs +++ b/solana/programs/mock-cpi/src/processor/core_bridge/prepare_message.rs @@ -23,14 +23,14 @@ pub struct MockPrepareMessageV1<'info> { #[account( init, payer = payer, - space = core_bridge_sdk::compute_init_message_v1_space(data_len.try_into().unwrap()), + space = core_bridge_sdk::compute_prepared_message_space(data_len.try_into().unwrap()), seeds = [ b"my_draft_message", payer.key().as_ref(), payer_sequence.to_le_bytes().as_ref() ], bump, - owner = core_bridge_sdk::PROGRAM_ID, + owner = core_bridge_program::ID, )] message: AccountInfo<'info>, @@ -45,13 +45,11 @@ pub struct MockPrepareMessageV1<'info> { system_program: Program<'info, System>, } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for MockPrepareMessageV1<'info> { +impl<'info> core_bridge_sdk::cpi::PrepareMessage<'info> for MockPrepareMessageV1<'info> { fn core_bridge_program(&self) -> AccountInfo<'info> { self.core_bridge_program.to_account_info() } -} -impl<'info> core_bridge_sdk::cpi::PrepareMessageV1<'info> for MockPrepareMessageV1<'info> { fn core_emitter_authority(&self) -> AccountInfo<'info> { self.emitter_authority.to_account_info() } @@ -73,7 +71,7 @@ pub fn mock_prepare_message_v1( ) -> Result<()> { let MockPrepareMessageV1Args { nonce, data } = args; - core_bridge_sdk::cpi::prepare_message_v1( + core_bridge_sdk::cpi::prepare_message( ctx.accounts, core_bridge_sdk::cpi::InitMessageV1Args { nonce, @@ -81,9 +79,9 @@ pub fn mock_prepare_message_v1( commitment: core_bridge_sdk::types::Commitment::Finalized, }, data, - &[ + Some(&[&[ EMITTER_AUTHORITY_SEED_PREFIX, &[ctx.bumps["emitter_authority"]], - ], + ]]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/native.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/native.rs index 49b77e7923..1fefaef73a 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/native.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/native.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_spl::{associated_token, token}; -use token_bridge_program::{self, TokenBridge}; +use token_bridge_program::sdk as token_bridge_sdk; #[derive(Accounts)] pub struct MockLegacyCompleteTransferNative<'info> { @@ -24,7 +24,7 @@ pub struct MockLegacyCompleteTransferNative<'info> { payer_token: Account<'info, token::TokenAccount>, /// CHECK: This account is needed for the Token Bridge program. - posted_vaa: UncheckedAccount<'info>, + vaa: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] @@ -46,36 +46,77 @@ pub struct MockLegacyCompleteTransferNative<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, associated_token_program: Program<'info, associated_token::AssociatedToken>, } +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyCompleteTransferNative<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> + for MockLegacyCompleteTransferNative<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.recipient_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn payer_token(&self) -> Option> { + Some(self.payer_token.to_account_info()) + } + + fn recipient(&self) -> Option> { + Some(self.recipient.to_account_info()) + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + Some(self.token_bridge_custody_authority.to_account_info()) + } + + fn token_bridge_custody_token_account(&self) -> Option> { + Some(self.token_bridge_custody_token.to_account_info()) + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.vaa.to_account_info() + } +} + pub fn mock_legacy_complete_transfer_native( ctx: Context, ) -> Result<()> { - token_bridge_program::legacy::cpi::complete_transfer_native(CpiContext::new( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::CompleteTransferNative { - payer: ctx.accounts.payer.to_account_info(), - posted_vaa: ctx.accounts.posted_vaa.to_account_info(), - claim: ctx.accounts.token_bridge_claim.to_account_info(), - registered_emitter: ctx - .accounts - .token_bridge_registered_emitter - .to_account_info(), - recipient_token: ctx.accounts.recipient_token.to_account_info(), - payer_token: ctx.accounts.payer_token.to_account_info(), - custody_token: ctx.accounts.token_bridge_custody_token.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - custody_authority: ctx - .accounts - .token_bridge_custody_authority - .to_account_info(), - recipient: Some(ctx.accounts.recipient.to_account_info()), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - )) + token_bridge_sdk::cpi::complete_transfer_specified( + ctx.accounts, + false, // is_wrapped_asset + None, + ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/wrapped.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/wrapped.rs index 5482790c2c..b280353d6a 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/wrapped.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer/wrapped.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, TokenBridge}; +use token_bridge_program::sdk as token_bridge_sdk; #[derive(Accounts)] pub struct MockLegacyCompleteTransferWrapped<'info> { @@ -24,7 +24,7 @@ pub struct MockLegacyCompleteTransferWrapped<'info> { payer_token: Account<'info, token::TokenAccount>, /// CHECK: This account is needed for the Token Bridge program. - posted_vaa: UncheckedAccount<'info>, + vaa: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] @@ -47,32 +47,76 @@ pub struct MockLegacyCompleteTransferWrapped<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyCompleteTransferWrapped<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> + for MockLegacyCompleteTransferWrapped<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.recipient_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.token_bridge_wrapped_mint.to_account_info() + } + + fn payer_token(&self) -> Option> { + Some(self.payer_token.to_account_info()) + } + + fn recipient(&self) -> Option> { + Some(self.recipient.to_account_info()) + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_mint_authority(&self) -> Option> { + Some(self.token_bridge_mint_authority.to_account_info()) + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + Some(self.token_bridge_wrapped_asset.to_account_info()) + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.vaa.to_account_info() + } +} + pub fn mock_legacy_complete_transfer_wrapped( ctx: Context, ) -> Result<()> { - token_bridge_program::legacy::cpi::complete_transfer_wrapped(CpiContext::new( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::CompleteTransferWrapped { - payer: ctx.accounts.payer.to_account_info(), - posted_vaa: ctx.accounts.posted_vaa.to_account_info(), - claim: ctx.accounts.token_bridge_claim.to_account_info(), - registered_emitter: ctx - .accounts - .token_bridge_registered_emitter - .to_account_info(), - recipient_token: ctx.accounts.recipient_token.to_account_info(), - payer_token: ctx.accounts.payer_token.to_account_info(), - wrapped_mint: ctx.accounts.token_bridge_wrapped_mint.to_account_info(), - wrapped_asset: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), - mint_authority: ctx.accounts.token_bridge_mint_authority.to_account_info(), - recipient: Some(ctx.accounts.recipient.to_account_info()), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - )) + token_bridge_sdk::cpi::complete_transfer_specified( + ctx.accounts, + true, // is_wrapped_asset + None, + ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/native.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/native.rs index 12cd463e38..f1f904b5aa 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/native.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/native.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_spl::{associated_token, token}; -use token_bridge_program::{self, constants::PROGRAM_REDEEMER_SEED_PREFIX, TokenBridge}; +use token_bridge_program::sdk as token_bridge_sdk; use crate::constants::CUSTOM_REDEEMER_SEED_PREFIX; @@ -11,7 +11,7 @@ pub struct MockLegacyCompleteTransferWithPayloadNative<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account( - seeds = [PROGRAM_REDEEMER_SEED_PREFIX], + seeds = [token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX], bump, )] token_bridge_program_redeemer_authority: Option>, @@ -30,7 +30,7 @@ pub struct MockLegacyCompleteTransferWithPayloadNative<'info> { dst_token: Account<'info, token::TokenAccount>, /// CHECK: This account is needed for the Token Bridge program. - posted_vaa: UncheckedAccount<'info>, + vaa: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] @@ -52,55 +52,94 @@ pub struct MockLegacyCompleteTransferWithPayloadNative<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, associated_token_program: Program<'info, associated_token::AssociatedToken>, } +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyCompleteTransferWithPayloadNative<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> + for MockLegacyCompleteTransferWithPayloadNative<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.dst_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn redeemer_authority(&self) -> Option> { + match ( + &self.token_bridge_program_redeemer_authority, + &self.token_bridge_custom_redeemer_authority, + ) { + (Some(redeemer_authority), _) => Some(redeemer_authority.to_account_info()), + (None, Some(redeemer_authority)) => Some(redeemer_authority.to_account_info()), + (None, None) => None, + } + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + Some(self.token_bridge_custody_authority.to_account_info()) + } + + fn token_bridge_custody_token_account(&self) -> Option> { + Some(self.token_bridge_custody_token.to_account_info()) + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.vaa.to_account_info() + } +} pub fn mock_legacy_complete_transfer_with_payload_native( ctx: Context, ) -> Result<()> { - let (redeemer_authority, redeemer_seed_prefix, redeemer_bump) = match ( + let (redeemer_seed_prefix, redeemer_bump) = match ( &ctx.accounts.token_bridge_program_redeemer_authority, &ctx.accounts.token_bridge_custom_redeemer_authority, ) { - (Some(redeemer_authority), _) => ( - redeemer_authority.to_account_info(), - PROGRAM_REDEEMER_SEED_PREFIX, + (Some(_), _) => ( + token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX, ctx.bumps["token_bridge_program_redeemer_authority"], ), - (None, Some(redeemer_authority)) => ( - redeemer_authority.to_account_info(), + (None, Some(_)) => ( CUSTOM_REDEEMER_SEED_PREFIX, ctx.bumps["token_bridge_custom_redeemer_authority"], ), (None, None) => return err!(ErrorCode::AccountNotEnoughKeys), }; - token_bridge_program::legacy::cpi::complete_transfer_with_payload_native( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::CompleteTransferWithPayloadNative { - payer: ctx.accounts.payer.to_account_info(), - posted_vaa: ctx.accounts.posted_vaa.to_account_info(), - claim: ctx.accounts.token_bridge_claim.to_account_info(), - registered_emitter: ctx - .accounts - .token_bridge_registered_emitter - .to_account_info(), - dst_token: ctx.accounts.dst_token.to_account_info(), - redeemer_authority, - custody_token: ctx.accounts.token_bridge_custody_token.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - custody_authority: ctx - .accounts - .token_bridge_custody_authority - .to_account_info(), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - &[&[redeemer_seed_prefix, &[redeemer_bump]]], - ), + token_bridge_sdk::cpi::complete_transfer_specified( + ctx.accounts, + false, // is_wrapped_asset + Some(&[&[redeemer_seed_prefix, &[redeemer_bump]]]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/wrapped.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/wrapped.rs index d48559dea6..ea370fb925 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/wrapped.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/complete_transfer_with_payload/wrapped.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, constants::PROGRAM_REDEEMER_SEED_PREFIX, TokenBridge}; +use token_bridge_program::sdk as token_bridge_sdk; use crate::constants::CUSTOM_REDEEMER_SEED_PREFIX; @@ -11,7 +11,7 @@ pub struct MockLegacyCompleteTransferWithPayloadWrapped<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account( - seeds = [PROGRAM_REDEEMER_SEED_PREFIX], + seeds = [token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX], bump, )] token_bridge_program_redeemer_authority: Option>, @@ -30,7 +30,7 @@ pub struct MockLegacyCompleteTransferWithPayloadWrapped<'info> { dst_token: Account<'info, token::TokenAccount>, /// CHECK: This account is needed for the Token Bridge program. - posted_vaa: UncheckedAccount<'info>, + vaa: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] @@ -53,51 +53,94 @@ pub struct MockLegacyCompleteTransferWithPayloadWrapped<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyCompleteTransferWithPayloadWrapped<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> + for MockLegacyCompleteTransferWithPayloadWrapped<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.dst_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.token_bridge_wrapped_mint.to_account_info() + } + + fn redeemer_authority(&self) -> Option> { + match ( + &self.token_bridge_program_redeemer_authority, + &self.token_bridge_custom_redeemer_authority, + ) { + (Some(authority), _) => Some(authority.to_account_info()), + (None, Some(authority)) => Some(authority.to_account_info()), + (None, None) => None, + } + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_mint_authority(&self) -> Option> { + Some(self.token_bridge_mint_authority.to_account_info()) + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + Some(self.token_bridge_wrapped_asset.to_account_info()) + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.vaa.to_account_info() + } +} + pub fn mock_legacy_complete_transfer_with_payload_wrapped( ctx: Context, ) -> Result<()> { - let (redeemer_authority, redeemer_seed_prefix, redeemer_bump) = match ( + let (redeemer_seed_prefix, redeemer_bump) = match ( &ctx.accounts.token_bridge_program_redeemer_authority, &ctx.accounts.token_bridge_custom_redeemer_authority, ) { - (Some(sender_authority), _) => ( - sender_authority.to_account_info(), - PROGRAM_REDEEMER_SEED_PREFIX, + (Some(_), _) => ( + token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX, ctx.bumps["token_bridge_program_redeemer_authority"], ), - (None, Some(sender_authority)) => ( - sender_authority.to_account_info(), + (None, Some(_)) => ( CUSTOM_REDEEMER_SEED_PREFIX, ctx.bumps["token_bridge_custom_redeemer_authority"], ), (None, None) => return err!(ErrorCode::AccountNotEnoughKeys), }; - token_bridge_program::legacy::cpi::complete_transfer_with_payload_wrapped( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::CompleteTransferWithPayloadWrapped { - payer: ctx.accounts.payer.to_account_info(), - posted_vaa: ctx.accounts.posted_vaa.to_account_info(), - claim: ctx.accounts.token_bridge_claim.to_account_info(), - registered_emitter: ctx - .accounts - .token_bridge_registered_emitter - .to_account_info(), - dst_token: ctx.accounts.dst_token.to_account_info(), - redeemer_authority, - wrapped_mint: ctx.accounts.token_bridge_wrapped_mint.to_account_info(), - wrapped_asset: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), - mint_authority: ctx.accounts.token_bridge_mint_authority.to_account_info(), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - &[&[redeemer_seed_prefix, &[redeemer_bump]]], - ), + token_bridge_sdk::cpi::complete_transfer_specified( + ctx.accounts, + true, // is_wrapped_asset + Some(&[&[redeemer_seed_prefix, &[redeemer_bump]]]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/native.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/native.rs index 3e7ac4a9f0..24d416e0ff 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/native.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/native.rs @@ -1,7 +1,7 @@ use crate::{constants::MESSAGE_SEED_PREFIX, state::SignerSequence}; use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, TokenBridge}; +use token_bridge_program::sdk::{self as token_bridge_sdk, core_bridge_sdk}; use super::MockLegacyTransferTokensArgs; @@ -70,10 +70,80 @@ pub struct MockLegacyTransferTokensNative<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyTransferTokensNative<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockLegacyTransferTokensNative<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + self.core_fee_collector + .as_ref() + .map(|acc| acc.to_account_info()) + } +} + +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> for MockLegacyTransferTokensNative<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.src_token.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + Some(self.token_bridge_custody_authority.to_account_info()) + } + + fn token_bridge_custody_token_account(&self) -> Option> { + Some(self.token_bridge_custody_token.to_account_info()) + } +} + pub fn mock_legacy_transfer_tokens_native( ctx: Context, args: MockLegacyTransferTokensArgs, @@ -88,48 +158,23 @@ pub fn mock_legacy_transfer_tokens_native( // Where's my money, foo? let relayer_fee = amount / 10; - token_bridge_program::legacy::cpi::transfer_tokens_native( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::TransferTokensNative { - payer: ctx.accounts.payer.to_account_info(), - src_token: ctx.accounts.src_token.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - custody_token: ctx.accounts.token_bridge_custody_token.to_account_info(), - transfer_authority: ctx - .accounts - .token_bridge_transfer_authority - .to_account_info(), - custody_authority: ctx - .accounts - .token_bridge_custody_authority - .to_account_info(), - core_bridge_config: ctx.accounts.core_bridge_config.to_account_info(), - core_message: ctx.accounts.core_message.to_account_info(), - core_emitter: ctx.accounts.core_emitter.to_account_info(), - core_emitter_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), - core_fee_collector: ctx - .accounts - .core_fee_collector - .as_ref() - .map(|acc| acc.to_account_info()), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - &[&[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - ctx.accounts.payer_sequence.take_and_uptick().as_ref(), - &[ctx.bumps["core_message"]], - ]], - ), - token_bridge_program::legacy::cpi::TransferTokensArgs { + let sequence_number = ctx.accounts.payer_sequence.take_and_uptick(); + + token_bridge_sdk::cpi::transfer_tokens_specified( + ctx.accounts, + token_bridge_sdk::cpi::TransferTokensDirective::Transfer { nonce, amount, relayer_fee, recipient, recipient_chain, }, + false, // is_wrapped_asset + Some(&[&[ + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence_number.as_ref(), + &[ctx.bumps["core_message"]], + ]]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/wrapped.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/wrapped.rs index 4aa85486d8..d5015fe415 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/wrapped.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens/wrapped.rs @@ -1,7 +1,7 @@ use crate::{constants::MESSAGE_SEED_PREFIX, state::SignerSequence}; use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, TokenBridge}; +use token_bridge_program::sdk::{self as token_bridge_sdk, core_bridge_sdk}; use super::MockLegacyTransferTokensArgs; @@ -66,10 +66,78 @@ pub struct MockLegacyTransferTokensWrapped<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyTransferTokensWrapped<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for MockLegacyTransferTokensWrapped<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + self.core_fee_collector + .as_ref() + .map(|acc| acc.to_account_info()) + } +} + +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> + for MockLegacyTransferTokensWrapped<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.token_bridge_wrapped_mint.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.src_token.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + Some(self.token_bridge_wrapped_asset.to_account_info()) + } +} + pub fn mock_legacy_transfer_tokens_wrapped( ctx: Context, args: MockLegacyTransferTokensArgs, @@ -84,44 +152,23 @@ pub fn mock_legacy_transfer_tokens_wrapped( // Where's my money, foo? let relayer_fee = amount / 10; - token_bridge_program::legacy::cpi::transfer_tokens_wrapped( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::TransferTokensWrapped { - payer: ctx.accounts.payer.to_account_info(), - src_token: ctx.accounts.src_token.to_account_info(), - wrapped_mint: ctx.accounts.token_bridge_wrapped_mint.to_account_info(), - wrapped_asset: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), - transfer_authority: ctx - .accounts - .token_bridge_transfer_authority - .to_account_info(), - core_bridge_config: ctx.accounts.core_bridge_config.to_account_info(), - core_message: ctx.accounts.core_message.to_account_info(), - core_emitter: ctx.accounts.core_emitter.to_account_info(), - core_emitter_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), - core_fee_collector: ctx - .accounts - .core_fee_collector - .as_ref() - .map(|acc| acc.to_account_info()), - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, - &[&[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - ctx.accounts.payer_sequence.take_and_uptick().as_ref(), - &[ctx.bumps["core_message"]], - ]], - ), - token_bridge_program::legacy::cpi::TransferTokensArgs { + let sequence_number = ctx.accounts.payer_sequence.take_and_uptick(); + + token_bridge_sdk::cpi::transfer_tokens_specified( + ctx.accounts, + token_bridge_sdk::cpi::TransferTokensDirective::Transfer { nonce, amount, relayer_fee, recipient, recipient_chain, }, + true, // is_wrapped_asset + Some(&[&[ + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence_number.as_ref(), + &[ctx.bumps["core_message"]], + ]]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/native.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/native.rs index 6c98a34f02..5bbfa0091c 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/native.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/native.rs @@ -1,7 +1,7 @@ use crate::{constants::MESSAGE_SEED_PREFIX, state::SignerSequence}; use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, constants::PROGRAM_SENDER_SEED_PREFIX, TokenBridge}; +use token_bridge_program::sdk::{self as token_bridge_sdk, core_bridge_sdk}; use super::{MockLegacyTransferTokensWithPayloadArgs, CUSTOM_SENDER_SEED_PREFIX}; @@ -21,7 +21,7 @@ pub struct MockLegacyTransferTokensWithPayloadNative<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account( - seeds = [PROGRAM_SENDER_SEED_PREFIX], + seeds = [token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX], bump, )] token_bridge_program_sender_authority: Option>, @@ -83,10 +83,95 @@ pub struct MockLegacyTransferTokensWithPayloadNative<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyTransferTokensWithPayloadNative<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> + for MockLegacyTransferTokensWithPayloadNative<'info> +{ + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + self.core_fee_collector + .as_ref() + .map(|acc| acc.to_account_info()) + } +} + +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> + for MockLegacyTransferTokensWithPayloadNative<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.src_token.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + Some(self.token_bridge_custody_authority.to_account_info()) + } + + fn token_bridge_custody_token_account(&self) -> Option> { + Some(self.token_bridge_custody_token.to_account_info()) + } + + fn sender_authority(&self) -> Option> { + match ( + &self.token_bridge_program_sender_authority, + &self.token_bridge_custom_sender_authority, + ) { + (Some(authority), _) => Some(authority.to_account_info()), + (None, Some(authority)) => Some(authority.to_account_info()), + (None, None) => None, + } + } +} + pub fn mock_legacy_transfer_tokens_with_payload_native( ctx: Context, args: MockLegacyTransferTokensWithPayloadArgs, @@ -101,72 +186,50 @@ pub fn mock_legacy_transfer_tokens_with_payload_native( // We are determining which sender authority to test. A program can either use his own program // ID as the sender address or a custom sender address (like his PDA). - let (cpi_program_id, sender_authority, sender_seed_prefix, sender_bump) = match ( + let (directive, sender_seed_prefix, sender_bump) = match ( &ctx.accounts.token_bridge_program_sender_authority, &ctx.accounts.token_bridge_custom_sender_authority, ) { - (Some(sender_authority), _) => ( - Some(crate::ID), - sender_authority.to_account_info(), - PROGRAM_SENDER_SEED_PREFIX, + (Some(_), _) => ( + token_bridge_sdk::cpi::TransferTokensDirective::ProgramTransferWithPayload { + program_id: crate::ID, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, + token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX, ctx.bumps["token_bridge_program_sender_authority"], ), - (None, Some(sender_authority)) => ( - None, - sender_authority.to_account_info(), + (None, Some(_)) => ( + token_bridge_sdk::cpi::TransferTokensDirective::SignerTransferWithPayload { + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, CUSTOM_SENDER_SEED_PREFIX, ctx.bumps["token_bridge_custom_sender_authority"], ), (None, None) => return err!(ErrorCode::AccountNotEnoughKeys), }; - token_bridge_program::legacy::cpi::transfer_tokens_with_payload_native( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::TransferTokensWithPayloadNative { - payer: ctx.accounts.payer.to_account_info(), - src_token: ctx.accounts.src_token.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - custody_token: ctx.accounts.token_bridge_custody_token.to_account_info(), - transfer_authority: ctx - .accounts - .token_bridge_transfer_authority - .to_account_info(), - custody_authority: ctx - .accounts - .token_bridge_custody_authority - .to_account_info(), - core_bridge_config: ctx.accounts.core_bridge_config.to_account_info(), - core_message: ctx.accounts.core_message.to_account_info(), - core_emitter: ctx.accounts.core_emitter.to_account_info(), - core_emitter_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), - core_fee_collector: ctx - .accounts - .core_fee_collector - .as_ref() - .map(|acc| acc.to_account_info()), - sender_authority, - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, + let sequence_number = ctx.accounts.payer_sequence.take_and_uptick(); + + token_bridge_sdk::cpi::transfer_tokens_specified( + ctx.accounts, + directive, + false, // is_wrapped_asset + Some(&[ &[ - &[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - ctx.accounts.payer_sequence.take_and_uptick().as_ref(), - &[ctx.bumps["core_message"]], - ], - &[sender_seed_prefix, &[sender_bump]], + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence_number.as_ref(), + &[ctx.bumps["core_message"]], ], - ), - token_bridge_program::legacy::cpi::TransferTokensWithPayloadArgs { - nonce, - amount, - redeemer, - redeemer_chain, - payload, - cpi_program_id, - }, + &[sender_seed_prefix, &[sender_bump]], + ]), ) } diff --git a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/wrapped.rs b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/wrapped.rs index d8137ce8c1..8c1c7d2033 100644 --- a/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/wrapped.rs +++ b/solana/programs/mock-cpi/src/processor/token_bridge/legacy/transfer_tokens_with_payload/wrapped.rs @@ -1,7 +1,7 @@ use crate::{constants::MESSAGE_SEED_PREFIX, state::SignerSequence}; use anchor_lang::prelude::*; use anchor_spl::token; -use token_bridge_program::{self, constants::PROGRAM_SENDER_SEED_PREFIX, TokenBridge}; +use token_bridge_program::sdk::{self as token_bridge_sdk, core_bridge_sdk}; use super::{MockLegacyTransferTokensWithPayloadArgs, CUSTOM_SENDER_SEED_PREFIX}; @@ -21,7 +21,7 @@ pub struct MockLegacyTransferTokensWithPayloadWrapped<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account( - seeds = [PROGRAM_SENDER_SEED_PREFIX], + seeds = [token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX], bump, )] token_bridge_program_sender_authority: Option>, @@ -80,10 +80,90 @@ pub struct MockLegacyTransferTokensWithPayloadWrapped<'info> { core_bridge_program: UncheckedAccount<'info>, system_program: Program<'info, System>, - token_bridge_program: Program<'info, TokenBridge>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, token_program: Program<'info, token::Token>, } +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for MockLegacyTransferTokensWithPayloadWrapped<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> + for MockLegacyTransferTokensWithPayloadWrapped<'info> +{ + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + self.core_fee_collector + .as_ref() + .map(|acc| acc.to_account_info()) + } +} +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> + for MockLegacyTransferTokensWithPayloadWrapped<'info> +{ + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.token_bridge_wrapped_mint.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.src_token.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + Some(self.token_bridge_wrapped_asset.to_account_info()) + } + + fn sender_authority(&self) -> Option> { + match ( + &self.token_bridge_program_sender_authority, + &self.token_bridge_custom_sender_authority, + ) { + (Some(sender_authority), _) => Some(sender_authority.to_account_info()), + (None, Some(sender_authority)) => Some(sender_authority.to_account_info()), + (None, None) => None, + } + } +} + pub fn mock_legacy_transfer_tokens_with_payload_wrapped( ctx: Context, args: MockLegacyTransferTokensWithPayloadArgs, @@ -98,68 +178,50 @@ pub fn mock_legacy_transfer_tokens_with_payload_wrapped( // We are determining which sender authority to test. A program can either use his own program // ID as the sender address or a custom sender address (like his PDA). - let (cpi_program_id, sender_authority, sender_seed_prefix, sender_bump) = match ( + let (directive, sender_seed_prefix, sender_bump) = match ( &ctx.accounts.token_bridge_program_sender_authority, &ctx.accounts.token_bridge_custom_sender_authority, ) { - (Some(sender_authority), _) => ( - Some(crate::ID), - sender_authority.to_account_info(), - PROGRAM_SENDER_SEED_PREFIX, + (Some(_), _) => ( + token_bridge_sdk::cpi::TransferTokensDirective::ProgramTransferWithPayload { + program_id: crate::ID, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, + token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX, ctx.bumps["token_bridge_program_sender_authority"], ), - (None, Some(sender_authority)) => ( - None, - sender_authority.to_account_info(), + (None, Some(_)) => ( + token_bridge_sdk::cpi::TransferTokensDirective::SignerTransferWithPayload { + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, CUSTOM_SENDER_SEED_PREFIX, ctx.bumps["token_bridge_custom_sender_authority"], ), (None, None) => return err!(ErrorCode::AccountNotEnoughKeys), }; - token_bridge_program::legacy::cpi::transfer_tokens_with_payload_wrapped( - CpiContext::new_with_signer( - ctx.accounts.token_bridge_program.to_account_info(), - token_bridge_program::legacy::cpi::TransferTokensWithPayloadWrapped { - payer: ctx.accounts.payer.to_account_info(), - src_token: ctx.accounts.src_token.to_account_info(), - wrapped_mint: ctx.accounts.token_bridge_wrapped_mint.to_account_info(), - wrapped_asset: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), - transfer_authority: ctx - .accounts - .token_bridge_transfer_authority - .to_account_info(), - core_bridge_config: ctx.accounts.core_bridge_config.to_account_info(), - core_message: ctx.accounts.core_message.to_account_info(), - core_emitter: ctx.accounts.core_emitter.to_account_info(), - core_emitter_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), - core_fee_collector: ctx - .accounts - .core_fee_collector - .as_ref() - .map(|acc| acc.to_account_info()), - sender_authority, - system_program: ctx.accounts.system_program.to_account_info(), - core_bridge_program: ctx.accounts.core_bridge_program.to_account_info(), - token_program: ctx.accounts.token_program.to_account_info(), - }, + let sequence_number = ctx.accounts.payer_sequence.take_and_uptick(); + + token_bridge_sdk::cpi::transfer_tokens_specified( + ctx.accounts, + directive, + true, // is_wrapped_asset + Some(&[ &[ - &[ - MESSAGE_SEED_PREFIX, - ctx.accounts.payer.key().as_ref(), - ctx.accounts.payer_sequence.take_and_uptick().as_ref(), - &[ctx.bumps["core_message"]], - ], - &[sender_seed_prefix, &[sender_bump]], + MESSAGE_SEED_PREFIX, + ctx.accounts.payer.key().as_ref(), + sequence_number.as_ref(), + &[ctx.bumps["core_message"]], ], - ), - token_bridge_program::legacy::cpi::TransferTokensWithPayloadArgs { - nonce, - amount, - redeemer, - redeemer_chain, - payload, - cpi_program_id, - }, + &[sender_seed_prefix, &[sender_bump]], + ]), ) } diff --git a/solana/programs/token-bridge/Cargo.toml b/solana/programs/token-bridge/Cargo.toml index 83bcc25292..ce4537774e 100644 --- a/solana/programs/token-bridge/Cargo.toml +++ b/solana/programs/token-bridge/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wormhole-token-bridge-solana" description = "Wormhole Token Bridge Program for Solana" -version = "0.0.0-alpha.1" +version = "0.0.0-alpha.2" edition = "2021" authors = ["W7"] license = "Apache-2.0" @@ -27,7 +27,7 @@ cpi = ["no-entrypoint"] [dependencies] wormhole-io = "0.1.0" -wormhole-raw-vaas = { version = "=0.0.0-alpha.7", features = ["on-chain", "ruint"], default-features = false } +wormhole-raw-vaas = { version = "=0.0.0-alpha.9", features = ["on-chain", "ruint"], default-features = false } core-bridge-program = { version = "0.0.0-alpha.1", path = "../core-bridge", package = "wormhole-core-bridge-solana", features = ["cpi"], default-features = false } anchor-lang = { version = "0.28.0", features = ["derive", "init-if-needed"] } diff --git a/solana/programs/token-bridge/README.md b/solana/programs/token-bridge/README.md new file mode 100644 index 0000000000..433e9fa227 --- /dev/null +++ b/solana/programs/token-bridge/README.md @@ -0,0 +1,779 @@ +# wormhole-token-bridge-solana + +This package implements Wormhole's Token Bridge specification on Solana with some modifications (due +to the nature of how Solana works). The program itself is written using the [Anchor] framework. + +## Example Integration (Inbound Transfer) + +In order to bridge assets into Solana with a program integrating with Token Bridge, there are a +couple of traits that you the integrator will have to implement: + +- `CompleteTransfer<'info>` + - Ensures that all Token Bridge accounts for inbound transfers are included in your + [account context]. +- `CreateAccount<'info>` + - Requires payer and System program account infos. + +These traits are found in the [SDK] submodule of the Token Bridge program crate. + +```rust,ignore +use wormhole_token_bridge_solana::sdk as token_bridge_sdk; +``` + +Your account context may resemble the following: + +```rust,ignore +#[derive(Accounts)] +pub struct RedeemHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. This account is mutable in case the transferred asset is + /// Token Bridge wrapped, which requires the Token Bridge program to mint to a token account. + #[account( + mut, + owner = token::ID + )] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge complete transfer with payload. + /// This PDA validates the redeemer address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX], + bump, + )] + redeemer_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This account warehouses the + /// VAA of the token transfer from another chain. + encoded_vaa: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + token_bridge_claim: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_registered_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + #[account(mut)] + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_mint_authority: Option>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} +``` + +This account context must have all of the accounts required by the Token Bridge program in order to +transfer assets into Solana: + +- `token_bridge_program` +- `token_program` (SPL Token program pubkey). +- `vaa` (VAA account, either an Encoded VAA account or legacy Posted VAA V1 account). + - **NOTE: These VAA accounts are created using a parse and verify workflow, which is executed + outside of your program. Please refer to the Wormhole JS SDK for more info.** +- `token_bridge_claim` (Account used to prevent replay attacks ensuring that each VAA is redeemed + only once). +- `token_bridge_registered_emitter` (Account reflecting which Token Bridge smart contract emits + Wormhole messages, seeds = \[emitter_chain\]). +- `dst_token_account` (where the assets will be bridged from). +- `mint` (SPL Mint, which should be the same mint of your token account). +- `token_bridge_redeemer_authority` (seeds: ["redeemer"]) + - **NOTE: Your program ID is the redeemer in this case and must match the encoded redeemer address + in the transfer VAA.** +- `token_bridge_custody_token_account` (required for native assets, seeds: \[mint.key\]). +- `token_bridge_custody_authority` (required for native assets, seeds: ["custody_signer"]). +- `token_bridge_mint_authority` (required for wrapped assets, seeds: ["mint_signer"]). +- `token_bridge_wrapped_asset` (required for wrapped assets, seeds: ["meta", mint.key]). + +**You are not required to re-derive these PDA addresses in your program's account context because +the Token Bridge program already does these derivations. Doing so is a waste of compute units.** + +The traits above would be implemented by calling `to_account_info` on the appropriate accounts in +your context. + +By making sure that the `token_bridge_program` account is the correct program, your context will use +the [Program] account wrapper with the `TokenBridge` type. + +Because redeeming asset transfers requires creating account(s), the `CompleteTransfer` trait +requires the `CreateAccount` trait, which defines a `payer` account, who has the lamports to send to +a new account, and the `system_program`, which is used via CPI to create accounts. + +```rust,ignore +impl<'info> token_bridge_sdk::cpi::system_program::CreateAccount<'info> + for RedeemHelloWorld<'info> +{ + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} +``` + +Finally implement the `CompleteTransfer` trait by providing the necessary Token Bridge accounts. + +**NOTE: For transfers where the redeemer address is your program ID, the +`token_bridge_redeemer_authority` in this case is `Some(redeemer_authority)`, which is your +program's PDA address derived using `[b"redeemer"]` as its seeds. This seed prefix is provided for +you as `PROGRAM_REDEEMER_SEED_PREFIX` and is used in your account context to validate that the +correct redeemer authority is provided.** + +```rust,ignore +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> for RedeemHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.encoded_vaa.to_account_info() + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn redeemer_authority(&self) -> Option> { + Some(self.redeemer_authority.to_account_info()) + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_mint_authority(&self) -> Option> { + self.token_bridge_mint_authority.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } +} +``` + +In your instruction handler/processor method, you would use the `complete_transfer` method from the +CPI SDK. The Token Bridge program will verify that your redeemer authority can be derived the same +way using the encoded redeemer address in your transfer VAA as your program ID (this validates the +correct redeemer address is used to redeem your transfer). + +This method will invoke the Token Bridge to bridge assets in, which basically uses the VAA to show +proof of a transfer originating from another network (whose observation was made by the Guardians). + +```rust,ignore +pub fn redeem_hello_world(ctx: Context) -> Result<()> { + token_bridge_sdk::cpi::complete_transfer( + ctx.accounts, + Some(&[&[ + token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX, + &[ctx.bumps["redeemer_authority"]], + ]]), + ) +} +``` + +And that is all you need to do to transfer assets into Solana. + +## Example Integration (Outbound Transfer) + +In order to bridge assets from Solana with a program integrating with Token Bridge, there are a few +traits that you the integrator will have to implement: + +- `TransferTokens<'info>` + - Ensures that all Token Bridge accounts for outbound transfers are included in your + [account context]. +- `PublishMessage<'info>` + - Ensures that all Core Bridge accounts are included in your [account context]. + - **NOTE: This includes having to implement `CreateAccount<'info>`. See + [Core Bridge program documentation] for more details.** + +These traits are found in the [SDK] submodule of the Token Bridge program crate. + +```rust,ignore +use wormhole_token_bridge_solana::sdk::{self as token_bridge_sdk, core_bridge_sdk}; +``` + +Your account context may resemble the following: + +```rust,ignore + +#[derive(Accounts)] +pub struct TransferHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. + #[account(owner = token::ID)] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge transfer with payload. This PDA + /// validates the sender address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX], + bump, + )] + sender_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_transfer_authority: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_core_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_bridge_config: UncheckedAccount<'info>, + + /// CHECK: This account will be created using a generated keypair. + #[account(mut)] + core_message: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_emitter_sequence: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_fee_collector: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + core_bridge_program: UncheckedAccount<'info>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} +``` + +This account context must have all of the accounts required by the Token Bridge program in order to +transfer assets out: + +- `token_bridge_program` +- `token_program` (SPL Token program pubkey). +- `src_token_account` (where the assets will be bridged from). +- `mint` (SPL Mint, which should be the same mint of your token account). +- `token_bridge_sender_authority` (seeds: ["sender"]) + - **NOTE: Your program ID is the sender in this case.** +- `token_bridge_transfer_authority` (seeds: ["authority_signer"]). +- `token_bridge_custody_token_account` (required for native assets, seeds: [mint.key]). +- `token_bridge_custody_authority` (required for native assets, seeds: ["custody_signer"]). +- `token_bridge_wrapped_asset` (required for wrapped assets, seeds: ["meta", mint.key]). + +**You are not required to re-derive these PDA addresses in your program's account context because +the Token Bridge program already does these derivations. Doing so is a waste of compute units.** + +The traits above would be implemented by calling `to_account_info` on the appropriate accounts in +your context. + +By making sure that the `token_bridge_program` account is the correct program, your context will use +the [Program] account wrapper with the `TokenBridge` type. + +Because transferring assets out message requires publishing a Wormhole message, you must implement +the `PublishMessage` trait and the other traits it depends on (`CreateAccount`). Please see the +[Core Bridge program documentation] for more details. + +Implement the `PublishMessage` trait by providing the necessary Core Bridge accounts. + +```rust,ignore +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferHelloWorld<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.token_bridge_core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + Some(self.core_fee_collector.to_account_info()) + } +} +``` + +And finally implement the `TransferTokens` trait by providing the necessary Token Bridge accounts. + +**NOTE: For transfers where the sender address is your program ID, the +`token_bridge_sender_authority` in this case is `Some(sender_authority)`, which is your program's +PDA address derived using `[b"sender"]` as its seeds. This seed prefix is provided for you as +`PROGRAM_SENDER_SEED_PREFIX` and is used in your account context to validate that the correct sender +authority is provided.** + +```rust,ignore +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> for TransferHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn token_bridge_sender_authority(&self) -> Option> { + Some(self.sender_authority.to_account_info()) + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } +} +``` + +In your instruction handler/processor method, you would use the `transfer_tokens` method from the +CPI SDK with the `TransferTokensDirective::ProgramTransferWithPayload` with your program ID. The +Token Bridge program will verify that your sender authority can be derived the same way using the +provided program ID (this validates the correct sender address will be used for your transfer). + +This directive with the other transfer arguments (`nonce`, `amount`, `redeemer`, `redeemer_chain` +and message `payload`) will invoke the Token Bridge to bridge assets out, which is basically a +Worhole message emitted by the Token Bridge observed by the Guardians. When the Wormhole Guardians +sign this message attesting to its observation, you may redeem this attested transfer (VAA) on the +specified redeemer's network (specified by redeemer_chain) where a Token Bridge smart contract is +deployed. + +```rust,ignore +pub fn transfer_hello_world(ctx: Context, amount: u64) -> Result<()> { + let nonce = 420; + let redeemer = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xbe, 0xef, + ]; + let redeemer_chain = 2; + let payload = b"Hello, world!".to_vec(); + + token_bridge_sdk::cpi::transfer_tokens( + ctx.accounts, + token_bridge_sdk::cpi::TransferTokensDirective::ProgramTransferWithPayload { + program_id: crate::ID, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, + Some(&[&[ + token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX, + &[ctx.bumps["sender_authority"]], + ]]), + ) +} +``` + +And that is all you need to do to transfer assets from Solana. + +## Putting it All Together + +```rust,ignore +#![allow(clippy::result_large_err)] + +use anchor_lang::prelude::*; +use anchor_spl::token; +use wormhole_token_bridge_solana::sdk::{self as token_bridge_sdk, core_bridge_sdk}; + +declare_id!("TokenBridgeHe11oWor1d1111111111111111111111"); + +#[derive(Accounts)] +pub struct RedeemHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. This account is mutable in case the transferred asset is + /// Token Bridge wrapped, which requires the Token Bridge program to mint to a token account. + #[account( + mut, + owner = token::ID + )] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge complete transfer with payload. + /// This PDA validates the redeemer address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX], + bump, + )] + redeemer_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This account warehouses the + /// VAA of the token transfer from another chain. + encoded_vaa: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + token_bridge_claim: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_registered_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + #[account(mut)] + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_mint_authority: Option>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} + +impl<'info> token_bridge_sdk::cpi::CreateAccount<'info> for RedeemHelloWorld<'info> { + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> token_bridge_sdk::cpi::CompleteTransfer<'info> for RedeemHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn vaa(&self) -> AccountInfo<'info> { + self.encoded_vaa.to_account_info() + } + + fn token_bridge_claim(&self) -> AccountInfo<'info> { + self.token_bridge_claim.to_account_info() + } + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info> { + self.token_bridge_registered_emitter.to_account_info() + } + + fn dst_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn redeemer_authority(&self) -> Option> { + Some(self.redeemer_authority.to_account_info()) + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_mint_authority(&self) -> Option> { + self.token_bridge_mint_authority.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } +} + +#[derive(Accounts)] +pub struct TransferHelloWorld<'info> { + #[account(mut)] + payer: Signer<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer, + )] + payer_token: Account<'info, token::TokenAccount>, + + /// CHECK: Mint of our token account. This account is mutable in case the transferred asset is + /// Token Bridge wrapped, which requires the Token Bridge program to burn from a token + /// account. + #[account( + mut, + owner = token::ID + )] + mint: AccountInfo<'info>, + + /// CHECK: This account acts as the signer for our Token Bridge transfer with payload. This PDA + /// validates the sender address as this program's ID. + #[account( + seeds = [token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX], + bump, + )] + sender_authority: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_transfer_authority: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + #[account(mut)] + token_bridge_custody_token_account: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// native tokens. + token_bridge_custody_authority: Option>, + + /// CHECK: This account is needed for the Token Bridge program. This should not be None for + /// wrapped tokens. + token_bridge_wrapped_asset: Option>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_core_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_bridge_config: UncheckedAccount<'info>, + + /// CHECK: This account will be created using a generated keypair. + #[account(mut)] + core_message: AccountInfo<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_emitter_sequence: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] + core_fee_collector: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + core_bridge_program: UncheckedAccount<'info>, + + system_program: Program<'info, System>, + token_bridge_program: Program<'info, token_bridge_sdk::cpi::TokenBridge>, + token_program: Program<'info, token::Token>, +} + +impl<'info> token_bridge_sdk::cpi::CreateAccount<'info> for TransferHelloWorld<'info> { + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } + + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } +} + +impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferHelloWorld<'info> { + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() + } + + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() + } + + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.token_bridge_core_emitter.to_account_info() + } + + fn core_emitter_sequence(&self) -> AccountInfo<'info> { + self.core_emitter_sequence.to_account_info() + } + + fn core_fee_collector(&self) -> Option> { + Some(self.core_fee_collector.to_account_info()) + } +} + +impl<'info> token_bridge_sdk::cpi::TransferTokens<'info> for TransferHelloWorld<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info> { + self.token_bridge_program.to_account_info() + } + + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn core_message(&self) -> AccountInfo<'info> { + self.core_message.to_account_info() + } + + fn src_token_account(&self) -> AccountInfo<'info> { + self.payer_token.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.mint.to_account_info() + } + + fn sender_authority(&self) -> Option> { + Some(self.sender_authority.to_account_info()) + } + + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info> { + self.token_bridge_transfer_authority.to_account_info() + } + + fn token_bridge_custody_authority(&self) -> Option> { + self.token_bridge_custody_authority.clone() + } + + fn token_bridge_custody_token_account(&self) -> Option> { + self.token_bridge_custody_token_account.clone() + } + + fn token_bridge_wrapped_asset(&self) -> Option> { + self.token_bridge_wrapped_asset.clone() + } +} + +#[program] +pub mod token_bridge_hello_world { + use super::*; + + pub fn redeem_hello_world(ctx: Context) -> Result<()> { + token_bridge_sdk::cpi::complete_transfer( + ctx.accounts, + Some(&[&[ + token_bridge_sdk::PROGRAM_REDEEMER_SEED_PREFIX, + &[ctx.bumps["redeemer_authority"]], + ]]), + ) + } + + pub fn transfer_hello_world(ctx: Context, amount: u64) -> Result<()> { + let nonce = 420; + let redeemer = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xde, 0xad, 0xbe, 0xef, + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, + 0xbe, 0xef, + ]; + let redeemer_chain = 2; + let payload = b"Hello, world!".to_vec(); + + token_bridge_sdk::cpi::transfer_tokens( + ctx.accounts, + token_bridge_sdk::cpi::TransferTokensDirective::ProgramTransferWithPayload { + program_id: crate::ID, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + }, + Some(&[&[ + token_bridge_sdk::PROGRAM_SENDER_SEED_PREFIX, + &[ctx.bumps["sender_authority"]], + ]]), + ) + } +} + +``` + +[account context]: https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html +[anchor]: https://docs.rs/anchor-lang/latest/anchor_lang/ +[core bridge program documentation]: https://docs.rs/wormhole-core-bridge-solana +[program]: https://docs.rs/anchor-lang/latest/anchor_lang/accounts/program/struct.Program.html +[sdk]: https://docs.rs/wormhole-token-bridge-solana/latest/wormhole_token_bridge_solana/sdk/cpi/index.html diff --git a/solana/programs/token-bridge/src/constants.rs b/solana/programs/token-bridge/src/constants.rs index 4b3b112601..68aeed4b73 100644 --- a/solana/programs/token-bridge/src/constants.rs +++ b/solana/programs/token-bridge/src/constants.rs @@ -1,3 +1,6 @@ +//! Constants used by the Token Bridge Program. For integrators, necessary constants are re-exported +//! in the [sdk](crate::sdk) module. + use anchor_lang::prelude::constant; /// Seed for upgrade authority. @@ -40,6 +43,8 @@ pub const PROGRAM_REDEEMER_SEED_PREFIX: &[u8] = b"redeemer"; #[constant] pub const MAX_DECIMALS: u8 = 8; +pub(crate) const GOVERNANCE_CHAIN: u16 = 1; + pub(crate) const GOVERNANCE_EMITTER: [u8; 32] = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, ]; diff --git a/solana/programs/token-bridge/src/error.rs b/solana/programs/token-bridge/src/error.rs index f6c4048a70..1e1a1b6b09 100644 --- a/solana/programs/token-bridge/src/error.rs +++ b/solana/programs/token-bridge/src/error.rs @@ -1,3 +1,5 @@ +//! Errors that may arise when interacting with the Token Bridge Program. +//! use anchor_lang::prelude::error_code; #[error_code] @@ -21,7 +23,8 @@ use anchor_lang::prelude::error_code; /// >= 0x1000 -- Legacy Complete Transfer with Payload Wrapped. /// >= 0x1100 -- Legacy Transfer Tokens with Payload Wrapped. /// >= 0x1200 -- Legacy Transfer Tokens with Payload Native. -/// >= 0x2000 -- Token Bridge Anchor Instruction. +/// >= 0x1600 -- Token Bridge Anchor Instruction. +/// >= 0x2000 -- Token Bridge SDK. /// /// NOTE: All of these error codes when triggered are offset by `ERROR_CODE_OFFSET` (6000). So for /// example, `U64Overflow` will return as 6006. @@ -83,21 +86,48 @@ pub enum TokenBridgeError { #[msg("InvalidRecipient")] InvalidRecipient = 0x48, + #[msg("InvalidRelayerFee")] + InvalidRelayerFee = 0x60, + + #[msg("InvalidProgramSender")] + InvalidProgramSender = 0x62, + #[msg("CannotSerializeJson")] CannotSerializeJson = 0x700, - #[msg("InvalidRelayerFee")] - InvalidRelayerFee = 0x60, + #[msg("AttestationOutOfSequence")] + AttestationOutOfSequence = 0x702, #[msg("ImplementationMismatch")] ImplementationMismatch = 0x800, #[msg("UnsupportedInstructionDirective")] - UnsupportedInstructionDirective = 0x2000, + UnsupportedInstructionDirective = 0x1600, #[msg("EmitterAlreadyRegistered")] - EmitterAlreadyRegistered = 0x2002, + EmitterAlreadyRegistered = 0x1602, #[msg("RegisteredEmitterMismatch")] - RegisteredEmitterMismatch = 0x2004, + RegisteredEmitterMismatch = 0x1604, + + #[msg("CustodyTokenAccountRequired")] + CustodyTokenAccountRequired = 0x2002, + + #[msg("CustodyAuthorityRequired")] + CustodyAuthorityRequired = 0x2004, + + #[msg("WrappedAssetRequired")] + WrappedAssetRequired = 0x2006, + + #[msg("SenderAuthorityRequired")] + SenderAuthorityRequired = 0x2008, + + #[msg("PayerTokenRequired")] + PayerTokenRequired = 0x200a, + + #[msg("MintAuthorityRequired")] + MintAuthorityRequired = 0x200c, + + #[msg("RedeemerAuthorityRequired")] + RedeemerAuthorityRequired = 0x200e, } diff --git a/solana/programs/token-bridge/src/legacy/accounts/mod.rs b/solana/programs/token-bridge/src/legacy/accounts/mod.rs new file mode 100644 index 0000000000..b68b585392 --- /dev/null +++ b/solana/programs/token-bridge/src/legacy/accounts/mod.rs @@ -0,0 +1,358 @@ +//! A set of structs mirroring the structs deriving [Accounts](anchor_lang::prelude::Accounts), +//! where each field is a [Pubkey]. This is useful for specifying self for a client. +//! +//! NOTE: This is similar to how [accounts](mod@crate::accounts) is generated via Anchor's +//! [program][anchor_lang::prelude::program] macro. + +use anchor_lang::prelude::{Pubkey, ToAccountMetas}; +use solana_program::instruction::AccountMeta; + +pub struct CompleteTransferNative { + pub payer: Pubkey, + pub vaa: Pubkey, + pub claim: Pubkey, + pub registered_emitter: Pubkey, + pub recipient_token: Pubkey, + pub payer_token: Pubkey, + pub custody_token: Pubkey, + pub mint: Pubkey, + pub custody_authority: Pubkey, + pub recipient: Option, + pub system_program: Pubkey, + pub token_program: Pubkey, +} + +impl ToAccountMetas for CompleteTransferNative { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new_readonly(self.vaa, false), + AccountMeta::new(self.claim, false), + AccountMeta::new_readonly(self.registered_emitter, false), + AccountMeta::new(self.recipient_token, false), + AccountMeta::new(self.payer_token, false), + AccountMeta::new(self.custody_token, false), + AccountMeta::new_readonly(self.mint, false), + AccountMeta::new_readonly(self.custody_authority, false), + AccountMeta::new_readonly(self.recipient.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + ] + } +} + +pub struct CompleteTransferWrapped { + pub payer: Pubkey, + pub vaa: Pubkey, + pub claim: Pubkey, + pub registered_emitter: Pubkey, + pub recipient_token: Pubkey, + pub payer_token: Pubkey, + pub wrapped_mint: Pubkey, + pub wrapped_asset: Pubkey, + pub mint_authority: Pubkey, + pub recipient: Option, + pub system_program: Pubkey, + pub token_program: Pubkey, +} + +impl ToAccountMetas for CompleteTransferWrapped { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new_readonly(self.vaa, false), + AccountMeta::new(self.claim, false), + AccountMeta::new_readonly(self.registered_emitter, false), + AccountMeta::new(self.recipient_token, false), + AccountMeta::new(self.payer_token, false), + AccountMeta::new(self.wrapped_mint, false), + AccountMeta::new_readonly(self.wrapped_asset, false), + AccountMeta::new_readonly(self.mint_authority, false), + AccountMeta::new_readonly(self.recipient.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + ] + } +} + +pub struct CompleteTransferWithPayloadNative { + pub payer: Pubkey, + pub vaa: Pubkey, + pub claim: Pubkey, + pub registered_emitter: Pubkey, + pub dst_token: Pubkey, + pub redeemer_authority: Pubkey, + pub custody_token: Pubkey, + pub mint: Pubkey, + pub custody_authority: Pubkey, + pub system_program: Pubkey, + pub token_program: Pubkey, +} + +impl ToAccountMetas for CompleteTransferWithPayloadNative { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new_readonly(self.vaa, false), + AccountMeta::new(self.claim, false), + AccountMeta::new_readonly(self.registered_emitter, false), + AccountMeta::new(self.dst_token, false), + AccountMeta::new_readonly(self.redeemer_authority, true), + AccountMeta::new_readonly(crate::ID, false), // _relayer_fee_token + AccountMeta::new(self.custody_token, false), + AccountMeta::new_readonly(self.mint, false), + AccountMeta::new_readonly(self.custody_authority, false), + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + ] + } +} + +pub struct CompleteTransferWithPayloadWrapped { + pub payer: Pubkey, + pub vaa: Pubkey, + pub claim: Pubkey, + pub registered_emitter: Pubkey, + pub dst_token: Pubkey, + pub redeemer_authority: Pubkey, + pub wrapped_mint: Pubkey, + pub wrapped_asset: Pubkey, + pub mint_authority: Pubkey, + pub system_program: Pubkey, + pub token_program: Pubkey, +} + +impl ToAccountMetas for CompleteTransferWithPayloadWrapped { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new_readonly(self.vaa, false), + AccountMeta::new(self.claim, false), + AccountMeta::new_readonly(self.registered_emitter, false), + AccountMeta::new(self.dst_token, false), + AccountMeta::new_readonly(self.redeemer_authority, true), + AccountMeta::new_readonly(crate::ID, false), // _relayer_fee_token + AccountMeta::new(self.wrapped_mint, false), + AccountMeta::new_readonly(self.wrapped_asset, false), + AccountMeta::new_readonly(self.mint_authority, false), + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + ] + } +} + +pub struct TransferTokensNative { + /// CHECK: Transaction payer (mut signer). + pub payer: Pubkey, + /// CHECK: Source Token Account (mut). + pub src_token: Pubkey, + /// CHECK: Mint (read-only). + pub mint: Pubkey, + /// CHECK: Transfer Authority (mut, seeds = \[mint.key\], seeds::program = + /// token_bridge_program). + pub custody_token: Pubkey, + /// CHECK: Transfer Authority (read-only, seeds = \["authority_signer"\], seeds::program = + /// token_bridge_program). + pub transfer_authority: Pubkey, + /// CHECK: Custody Authority (read-only, seeds = \["custody_signer"\], seeds::program = + /// token_bridge_program). + pub custody_authority: Pubkey, + /// CHECK: Core Bridge Program Data (mut, seeds = \["Bridge"\], seeds::program = + /// core_bridge_program). + pub core_bridge_config: Pubkey, + /// CHECK: Core Bridge Message (mut). + pub core_message: Pubkey, + /// CHECK: Core Bridge Emitter (read-only, seeds = \["emitter"\], seeds::program = + /// token_bridge_program). + pub core_emitter: Pubkey, + /// CHECK: Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\], + /// seeds::program = core_bridge_program). + pub core_emitter_sequence: Pubkey, + /// CHECK: Core Bridge Fee Collector (mut, seeds = \["fee_collector"\], seeds::program = + /// core_bridge_program). + pub core_fee_collector: Option, + /// CHECK: System Program. + pub system_program: Pubkey, + /// CHECK: Token Program. + pub token_program: Pubkey, + /// CHECK: Core Bridge Program. + pub core_bridge_program: Pubkey, +} + +impl ToAccountMetas for TransferTokensNative { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new(self.src_token, false), + AccountMeta::new_readonly(self.mint, false), + AccountMeta::new(self.custody_token, false), + AccountMeta::new_readonly(self.transfer_authority, false), + AccountMeta::new_readonly(self.custody_authority, false), + AccountMeta::new(self.core_bridge_config, false), + AccountMeta::new(self.core_message, true), + AccountMeta::new_readonly(self.core_emitter, false), + AccountMeta::new(self.core_emitter_sequence, false), + AccountMeta::new(self.core_fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + AccountMeta::new_readonly(self.core_bridge_program, false), + ] + } +} + +pub struct TransferTokensWrapped { + /// CHECK: Transaction payer (mut signer). + pub payer: Pubkey, + /// CHECK: Source Token Account (mut). + pub src_token: Pubkey, + /// CHECK: Wrapped Mint (mut, seeds = \["wrapped", token_chain, token_address\], + /// seeds::program = token_bridge_program). + pub wrapped_mint: Pubkey, + /// CHECK: Wrapped Asset (read-only, seeds = \[wrapped_mint.key\], seeds::program = + /// token_bridge_program). + pub wrapped_asset: Pubkey, + /// CHECK: Transfer Authority (read-only, seeds = \["authority_signer"\], seeds::program = + /// token_bridge_program). + pub transfer_authority: Pubkey, + /// CHECK: Core Bridge Program Data (mut, seeds = \["Bridge"\], seeds::program = + /// core_bridge_program). + pub core_bridge_config: Pubkey, + /// CHECK: Core Bridge Message (mut). + pub core_message: Pubkey, + /// CHECK: Core Bridge Emitter (read-only, seeds = \["emitter"\], seeds::program = + /// token_bridge_program). + pub core_emitter: Pubkey, + /// CHECK: Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\], + /// seeds::program = core_bridge_program). + pub core_emitter_sequence: Pubkey, + /// CHECK: Core Bridge Fee Collector (mut, seeds = \["fee_collector"\], seeds::program = + /// core_bridge_program). + pub core_fee_collector: Option, + /// CHECK: System Program. + pub system_program: Pubkey, + /// CHECK: Token Program. + pub token_program: Pubkey, + /// CHECK: Core Bridge Program. + pub core_bridge_program: Pubkey, +} + +impl ToAccountMetas for TransferTokensWrapped { + fn to_account_metas( + &self, + _is_signer: Option, + ) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new(self.src_token, false), + AccountMeta::new_readonly(crate::ID, false), // _src_owner + AccountMeta::new(self.wrapped_mint, false), + AccountMeta::new_readonly(self.wrapped_asset, false), + AccountMeta::new_readonly(self.transfer_authority, false), + AccountMeta::new(self.core_bridge_config, false), + AccountMeta::new(self.core_message, true), + AccountMeta::new_readonly(self.core_emitter, false), + AccountMeta::new(self.core_emitter_sequence, false), + AccountMeta::new(self.core_fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + AccountMeta::new_readonly(self.core_bridge_program, false), + ] + } +} + +pub struct TransferTokensWithPayloadNative { + pub payer: Pubkey, + pub src_token: Pubkey, + pub mint: Pubkey, + pub custody_token: Pubkey, + pub transfer_authority: Pubkey, + pub custody_authority: Pubkey, + pub core_bridge_config: Pubkey, + pub core_message: Pubkey, + pub core_emitter: Pubkey, + pub core_emitter_sequence: Pubkey, + pub core_fee_collector: Option, + pub sender_authority: Pubkey, + pub system_program: Pubkey, + pub token_program: Pubkey, + pub core_bridge_program: Pubkey, +} + +impl ToAccountMetas for TransferTokensWithPayloadNative { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new(self.src_token, false), + AccountMeta::new_readonly(self.mint, false), + AccountMeta::new(self.custody_token, false), + AccountMeta::new_readonly(self.transfer_authority, false), + AccountMeta::new_readonly(self.custody_authority, false), + AccountMeta::new(self.core_bridge_config, false), + AccountMeta::new(self.core_message, true), + AccountMeta::new_readonly(self.core_emitter, false), + AccountMeta::new(self.core_emitter_sequence, false), + AccountMeta::new(self.core_fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(self.sender_authority, true), + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + AccountMeta::new_readonly(self.core_bridge_program, false), + ] + } +} + +pub struct TransferTokensWithPayloadWrapped { + pub payer: Pubkey, + pub src_token: Pubkey, + pub wrapped_mint: Pubkey, + pub wrapped_asset: Pubkey, + pub transfer_authority: Pubkey, + pub core_bridge_config: Pubkey, + pub core_message: Pubkey, + pub core_emitter: Pubkey, + pub core_emitter_sequence: Pubkey, + pub core_fee_collector: Option, + pub sender_authority: Pubkey, + pub system_program: Pubkey, + pub token_program: Pubkey, + pub core_bridge_program: Pubkey, +} + +impl ToAccountMetas for TransferTokensWithPayloadWrapped { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + vec![ + AccountMeta::new(self.payer, true), + AccountMeta::new_readonly(crate::ID, false), // _config + AccountMeta::new(self.src_token, false), + AccountMeta::new_readonly(crate::ID, false), // _src_owner + AccountMeta::new(self.wrapped_mint, false), + AccountMeta::new_readonly(self.wrapped_asset, false), + AccountMeta::new_readonly(self.transfer_authority, false), + AccountMeta::new(self.core_bridge_config, false), + AccountMeta::new(self.core_message, true), + AccountMeta::new_readonly(self.core_emitter, false), + AccountMeta::new(self.core_emitter_sequence, false), + AccountMeta::new(self.core_fee_collector.unwrap_or(crate::ID), false), + AccountMeta::new_readonly(crate::ID, false), // _clock + AccountMeta::new_readonly(self.sender_authority, true), + AccountMeta::new_readonly(crate::ID, false), // _rent + AccountMeta::new_readonly(self.system_program, false), + AccountMeta::new_readonly(self.token_program, false), + AccountMeta::new_readonly(self.core_bridge_program, false), + ] + } +} diff --git a/solana/programs/token-bridge/src/legacy/instruction/attest_token.rs b/solana/programs/token-bridge/src/legacy/instruction/attest_token.rs deleted file mode 100644 index 69778b7eb0..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/attest_token.rs +++ /dev/null @@ -1,6 +0,0 @@ -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct LegacyAttestTokenArgs { - pub nonce: u32, -} diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/mod.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/mod.rs deleted file mode 100644 index d7d8cc242a..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod native; -pub use native::*; - -mod wrapped; -pub use wrapped::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/native.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/native.rs deleted file mode 100644 index 780577affb..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/native.rs +++ /dev/null @@ -1,54 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::LegacyInstruction; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct CompleteTransferNative { - pub payer: Pubkey, - pub posted_vaa: Pubkey, - pub claim: Pubkey, - pub registered_emitter: Pubkey, - pub recipient_token: Pubkey, - pub payer_token: Pubkey, - pub custody_token: Pubkey, - pub mint: Pubkey, - pub custody_authority: Pubkey, - pub recipient: Option, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn complete_transfer_native(accounts: CompleteTransferNative) -> Instruction { - let recipient = accounts.recipient.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new_readonly(accounts.posted_vaa, false), - AccountMeta::new(accounts.claim, false), - AccountMeta::new_readonly(accounts.registered_emitter, false), - AccountMeta::new(accounts.recipient_token, false), - AccountMeta::new(accounts.payer_token, false), - AccountMeta::new(accounts.custody_token, false), - AccountMeta::new_readonly(accounts.mint, false), - AccountMeta::new_readonly(accounts.custody_authority, false), - AccountMeta::new_readonly(recipient, false), - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::CompleteTransferNative), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/wrapped.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/wrapped.rs deleted file mode 100644 index 7aee297b59..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer/wrapped.rs +++ /dev/null @@ -1,54 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::LegacyInstruction; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct CompleteTransferWrapped { - pub payer: Pubkey, - pub posted_vaa: Pubkey, - pub claim: Pubkey, - pub registered_emitter: Pubkey, - pub recipient_token: Pubkey, - pub payer_token: Pubkey, - pub wrapped_mint: Pubkey, - pub wrapped_asset: Pubkey, - pub mint_authority: Pubkey, - pub recipient: Option, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn complete_transfer_wrapped(accounts: CompleteTransferWrapped) -> Instruction { - let recipient = accounts.recipient.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new_readonly(accounts.posted_vaa, false), - AccountMeta::new(accounts.claim, false), - AccountMeta::new_readonly(accounts.registered_emitter, false), - AccountMeta::new(accounts.recipient_token, false), - AccountMeta::new(accounts.payer_token, false), - AccountMeta::new(accounts.wrapped_mint, false), - AccountMeta::new_readonly(accounts.wrapped_asset, false), - AccountMeta::new_readonly(accounts.mint_authority, false), - AccountMeta::new_readonly(recipient, false), - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::CompleteTransferWrapped), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/mod.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/mod.rs deleted file mode 100644 index d7d8cc242a..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod native; -pub use native::*; - -mod wrapped; -pub use wrapped::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/native.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/native.rs deleted file mode 100644 index 6952156545..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/native.rs +++ /dev/null @@ -1,54 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::LegacyInstruction; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct CompleteTransferWithPayloadNative { - pub payer: Pubkey, - pub posted_vaa: Pubkey, - pub claim: Pubkey, - pub registered_emitter: Pubkey, - pub dst_token: Pubkey, - pub redeemer_authority: Pubkey, - pub custody_token: Pubkey, - pub mint: Pubkey, - pub custody_authority: Pubkey, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn complete_transfer_with_payload_native( - accounts: CompleteTransferWithPayloadNative, - ) -> Instruction { - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new_readonly(accounts.posted_vaa, false), - AccountMeta::new(accounts.claim, false), - AccountMeta::new_readonly(accounts.registered_emitter, false), - AccountMeta::new(accounts.dst_token, false), - AccountMeta::new_readonly(accounts.redeemer_authority, true), - AccountMeta::new_readonly(crate::ID, false), // _relayer_fee_token - AccountMeta::new(accounts.custody_token, false), - AccountMeta::new_readonly(accounts.mint, false), - AccountMeta::new_readonly(accounts.custody_authority, false), - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::CompleteTransferWithPayloadNative), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/wrapped.rs b/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/wrapped.rs deleted file mode 100644 index 62f6c34b11..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/complete_transfer_with_payload/wrapped.rs +++ /dev/null @@ -1,54 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::LegacyInstruction; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct CompleteTransferWithPayloadWrapped { - pub payer: Pubkey, - pub posted_vaa: Pubkey, - pub claim: Pubkey, - pub registered_emitter: Pubkey, - pub dst_token: Pubkey, - pub redeemer_authority: Pubkey, - pub wrapped_mint: Pubkey, - pub wrapped_asset: Pubkey, - pub mint_authority: Pubkey, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn complete_transfer_with_payload_wrapped( - accounts: CompleteTransferWithPayloadWrapped, - ) -> Instruction { - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new_readonly(accounts.posted_vaa, false), - AccountMeta::new(accounts.claim, false), - AccountMeta::new_readonly(accounts.registered_emitter, false), - AccountMeta::new(accounts.dst_token, false), - AccountMeta::new_readonly(accounts.redeemer_authority, true), - AccountMeta::new_readonly(crate::ID, false), // _relayer_fee_token - AccountMeta::new(accounts.wrapped_mint, false), - AccountMeta::new_readonly(accounts.wrapped_asset, false), - AccountMeta::new_readonly(accounts.mint_authority, false), - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::CompleteTransferWithPayloadWrapped), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/initialize.rs b/solana/programs/token-bridge/src/legacy/instruction/initialize.rs deleted file mode 100644 index d9563f04cc..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/initialize.rs +++ /dev/null @@ -1,7 +0,0 @@ -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; -use solana_program::pubkey::Pubkey; - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct InitializeArgs { - _core_bridge_program: Pubkey, -} diff --git a/solana/programs/token-bridge/src/legacy/instruction/mod.rs b/solana/programs/token-bridge/src/legacy/instruction/mod.rs index 23e03153af..078e4a4702 100644 --- a/solana/programs/token-bridge/src/legacy/instruction/mod.rs +++ b/solana/programs/token-bridge/src/legacy/instruction/mod.rs @@ -1,22 +1,4 @@ -mod attest_token; -pub use attest_token::*; - -mod complete_transfer; -pub use complete_transfer::*; - -mod complete_transfer_with_payload; -pub use complete_transfer_with_payload::*; - -mod initialize; -pub use initialize::*; - -mod transfer_tokens; -pub use transfer_tokens::*; - -mod transfer_tokens_with_payload; -pub use transfer_tokens_with_payload::*; - -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; +use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize, Pubkey}; /// NOTE: No more instructions should be added to this enum. Instead, add them as Anchor instruction /// handlers, which will inevitably live in lib.rs. @@ -39,3 +21,124 @@ pub enum LegacyInstruction { #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] pub struct EmptyArgs {} + +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InitializeArgs { + _gap: [u8; 32], +} + +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct LegacyAttestTokenArgs { + pub nonce: u32, +} + +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct TransferTokensArgs { + pub nonce: u32, + pub amount: u64, + pub relayer_fee: u64, + pub recipient: [u8; 32], + pub recipient_chain: u16, +} + +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct TransferTokensWithPayloadArgs { + pub nonce: u32, + pub amount: u64, + pub redeemer: [u8; 32], + pub redeemer_chain: u16, + pub payload: Vec, + pub cpi_program_id: Option, +} + +#[cfg(feature = "no-entrypoint")] +mod __no_entrypoint { + use crate::legacy::accounts; + use anchor_lang::ToAccountMetas; + use solana_program::instruction::Instruction; + + use super::*; + + pub fn complete_transfer_native(accounts: accounts::CompleteTransferNative) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::CompleteTransferNative), + accounts.to_account_metas(None), + ) + } + + pub fn complete_transfer_wrapped(accounts: accounts::CompleteTransferWrapped) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::CompleteTransferWrapped), + accounts.to_account_metas(None), + ) + } + + pub fn complete_transfer_with_payload_native( + accounts: accounts::CompleteTransferWithPayloadNative, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::CompleteTransferWithPayloadNative), + accounts.to_account_metas(None), + ) + } + + pub fn complete_transfer_with_payload_wrapped( + accounts: accounts::CompleteTransferWithPayloadWrapped, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::CompleteTransferWithPayloadWrapped), + accounts.to_account_metas(None), + ) + } + + pub fn transfer_tokens_native( + accounts: accounts::TransferTokensNative, + args: TransferTokensArgs, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::TransferTokensNative, args), + accounts.to_account_metas(None), + ) + } + + pub fn transfer_tokens_wrapped( + accounts: accounts::TransferTokensWrapped, + args: TransferTokensArgs, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::TransferTokensWrapped, args), + accounts.to_account_metas(None), + ) + } + + pub fn transfer_tokens_with_payload_native( + accounts: accounts::TransferTokensWithPayloadNative, + args: TransferTokensWithPayloadArgs, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::TransferTokensWithPayloadNative, args), + accounts.to_account_metas(None), + ) + } + + pub fn transfer_tokens_with_payload_wrapped( + accounts: accounts::TransferTokensWithPayloadWrapped, + args: TransferTokensWithPayloadArgs, + ) -> Instruction { + Instruction::new_with_borsh( + crate::ID, + &(LegacyInstruction::TransferTokensWithPayloadWrapped, args), + accounts.to_account_metas(None), + ) + } +} + +#[cfg(feature = "no-entrypoint")] +pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/mod.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/mod.rs deleted file mode 100644 index cc699ad39b..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/mod.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod native; -pub use native::*; - -mod wrapped; -pub use wrapped::*; - -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct TransferTokensArgs { - pub nonce: u32, - pub amount: u64, - pub relayer_fee: u64, - pub recipient: [u8; 32], - pub recipient_chain: u16, -} diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/native.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/native.rs deleted file mode 100644 index 48148684db..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/native.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::{LegacyInstruction, TransferTokensArgs}; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct TransferTokensNative { - pub payer: Pubkey, - pub src_token: Pubkey, - pub mint: Pubkey, - pub custody_token: Pubkey, - pub transfer_authority: Pubkey, - pub custody_authority: Pubkey, - pub core_bridge_config: Pubkey, - pub core_message: Pubkey, - pub core_emitter: Pubkey, - pub core_emitter_sequence: Pubkey, - pub core_fee_collector: Option, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn transfer_tokens_native( - accounts: TransferTokensNative, - args: TransferTokensArgs, - ) -> Instruction { - let core_fee_collector = accounts.core_fee_collector.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new(accounts.src_token, false), - AccountMeta::new_readonly(accounts.mint, false), - AccountMeta::new(accounts.custody_token, false), - AccountMeta::new_readonly(accounts.transfer_authority, false), - AccountMeta::new_readonly(accounts.custody_authority, false), - AccountMeta::new(accounts.core_bridge_config, false), - AccountMeta::new(accounts.core_message, true), - AccountMeta::new_readonly(accounts.core_emitter, false), - AccountMeta::new(accounts.core_emitter_sequence, false), - AccountMeta::new(core_fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::TransferTokensNative, args), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/wrapped.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/wrapped.rs deleted file mode 100644 index 7a4825f69e..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens/wrapped.rs +++ /dev/null @@ -1,60 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::{LegacyInstruction, TransferTokensArgs}; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct TransferTokensWrapped { - pub payer: Pubkey, - pub src_token: Pubkey, - pub wrapped_mint: Pubkey, - pub wrapped_asset: Pubkey, - pub transfer_authority: Pubkey, - pub core_bridge_config: Pubkey, - pub core_message: Pubkey, - pub core_emitter: Pubkey, - pub core_emitter_sequence: Pubkey, - pub core_fee_collector: Option, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn transfer_tokens_wrapped( - accounts: TransferTokensWrapped, - args: TransferTokensArgs, - ) -> Instruction { - let core_fee_collector = accounts.core_fee_collector.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new(accounts.src_token, false), - AccountMeta::new_readonly(crate::ID, false), // _src_owner - AccountMeta::new(accounts.wrapped_mint, false), - AccountMeta::new_readonly(accounts.wrapped_asset, false), - AccountMeta::new_readonly(accounts.transfer_authority, false), - AccountMeta::new(accounts.core_bridge_config, false), - AccountMeta::new(accounts.core_message, true), - AccountMeta::new_readonly(accounts.core_emitter, false), - AccountMeta::new(accounts.core_emitter_sequence, false), - AccountMeta::new(core_fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::TransferTokensWrapped, args), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/mod.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/mod.rs deleted file mode 100644 index 22142ca158..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod native; -pub use native::*; - -mod wrapped; -pub use wrapped::*; - -use anchor_lang::prelude::{borsh, AnchorDeserialize, AnchorSerialize}; -use solana_program::pubkey::Pubkey; - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct TransferTokensWithPayloadArgs { - pub nonce: u32, - pub amount: u64, - pub redeemer: [u8; 32], - pub redeemer_chain: u16, - pub payload: Vec, - pub cpi_program_id: Option, -} diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/native.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/native.rs deleted file mode 100644 index 017e3c91b2..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/native.rs +++ /dev/null @@ -1,63 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::{LegacyInstruction, TransferTokensWithPayloadArgs}; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct TransferTokensWithPayloadNative { - pub payer: Pubkey, - pub src_token: Pubkey, - pub mint: Pubkey, - pub custody_token: Pubkey, - pub transfer_authority: Pubkey, - pub custody_authority: Pubkey, - pub core_bridge_config: Pubkey, - pub core_message: Pubkey, - pub core_emitter: Pubkey, - pub core_emitter_sequence: Pubkey, - pub core_fee_collector: Option, - pub sender_authority: Pubkey, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn transfer_tokens_with_payload_native( - accounts: TransferTokensWithPayloadNative, - args: TransferTokensWithPayloadArgs, - ) -> Instruction { - let core_fee_collector = accounts.core_fee_collector.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new(accounts.src_token, false), - AccountMeta::new_readonly(accounts.mint, false), - AccountMeta::new(accounts.custody_token, false), - AccountMeta::new_readonly(accounts.transfer_authority, false), - AccountMeta::new_readonly(accounts.custody_authority, false), - AccountMeta::new(accounts.core_bridge_config, false), - AccountMeta::new(accounts.core_message, true), - AccountMeta::new_readonly(accounts.core_emitter, false), - AccountMeta::new(accounts.core_emitter_sequence, false), - AccountMeta::new(core_fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(accounts.sender_authority, true), - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::TransferTokensWithPayloadNative, args), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/wrapped.rs b/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/wrapped.rs deleted file mode 100644 index 076337e333..0000000000 --- a/solana/programs/token-bridge/src/legacy/instruction/transfer_tokens_with_payload/wrapped.rs +++ /dev/null @@ -1,62 +0,0 @@ -#[cfg(feature = "no-entrypoint")] -mod __no_entrypoint { - use crate::legacy::instruction::{LegacyInstruction, TransferTokensWithPayloadArgs}; - use solana_program::{ - instruction::{AccountMeta, Instruction}, - pubkey::Pubkey, - }; - - pub struct TransferTokensWithPayloadWrapped { - pub payer: Pubkey, - pub src_token: Pubkey, - pub wrapped_mint: Pubkey, - pub wrapped_asset: Pubkey, - pub transfer_authority: Pubkey, - pub core_bridge_config: Pubkey, - pub core_message: Pubkey, - pub core_emitter: Pubkey, - pub core_emitter_sequence: Pubkey, - pub core_fee_collector: Option, - pub sender_authority: Pubkey, - pub system_program: Pubkey, - pub core_bridge_program: Pubkey, - pub token_program: Pubkey, - } - - pub fn transfer_tokens_with_payload_wrapped( - accounts: TransferTokensWithPayloadWrapped, - args: TransferTokensWithPayloadArgs, - ) -> Instruction { - let core_fee_collector = accounts.core_fee_collector.unwrap_or(crate::ID); - - let accounts = vec![ - AccountMeta::new(accounts.payer, true), - AccountMeta::new_readonly(crate::ID, false), // _config - AccountMeta::new(accounts.src_token, false), - AccountMeta::new_readonly(crate::ID, false), // _src_owner - AccountMeta::new(accounts.wrapped_mint, false), - AccountMeta::new_readonly(accounts.wrapped_asset, false), - AccountMeta::new_readonly(accounts.transfer_authority, false), - AccountMeta::new(accounts.core_bridge_config, false), - AccountMeta::new(accounts.core_message, true), - AccountMeta::new_readonly(accounts.core_emitter, false), - AccountMeta::new(accounts.core_emitter_sequence, false), - AccountMeta::new(core_fee_collector, false), - AccountMeta::new_readonly(crate::ID, false), // _clock - AccountMeta::new_readonly(accounts.sender_authority, true), - AccountMeta::new_readonly(crate::ID, false), // _rent - AccountMeta::new_readonly(accounts.system_program, false), - AccountMeta::new_readonly(accounts.core_bridge_program, false), - AccountMeta::new_readonly(accounts.token_program, false), - ]; - - Instruction::new_with_borsh( - crate::ID, - &(LegacyInstruction::TransferTokensWithPayloadWrapped, args), - accounts, - ) - } -} - -#[cfg(feature = "no-entrypoint")] -pub use __no_entrypoint::*; diff --git a/solana/programs/token-bridge/src/legacy/mod.rs b/solana/programs/token-bridge/src/legacy/mod.rs index 5e26e8a8f3..c56255573d 100644 --- a/solana/programs/token-bridge/src/legacy/mod.rs +++ b/solana/programs/token-bridge/src/legacy/mod.rs @@ -1,29 +1,134 @@ -mod instruction; -pub(crate) use instruction::*; +//! Legacy Token Bridge state and instruction processing. + +pub use crate::ID; + +pub mod accounts; + +pub mod instruction; mod processor; pub(crate) use processor::*; pub mod state; -pub use crate::ID; - +/// Collection of methods to interact with the Token Bridge program via CPI. The structs defined in +/// this module mirror the structs deriving [Accounts](anchor_lang::prelude::Accounts), where each +/// field is an [AccountInfo]. **Integrators: Please use [sdk](crate::sdk) instead of this module.** +/// +/// NOTE: This is similar to how [cpi](mod@crate::cpi) is generated via Anchor's +/// [program][anchor_lang::prelude::program] macro. #[cfg(feature = "cpi")] pub mod cpi { - pub use instruction::{EmptyArgs, TransferTokensArgs, TransferTokensWithPayloadArgs}; - use anchor_lang::prelude::*; use solana_program::program::invoke_signed; use super::*; + pub fn complete_transfer_native<'info>( + ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferNative<'info>>, + ) -> Result<()> { + invoke_signed( + &instruction::complete_transfer_native(accounts::CompleteTransferNative { + payer: *ctx.accounts.payer.key, + vaa: *ctx.accounts.vaa.key, + claim: *ctx.accounts.claim.key, + registered_emitter: *ctx.accounts.registered_emitter.key, + recipient_token: *ctx.accounts.recipient_token.key, + payer_token: *ctx.accounts.payer_token.key, + custody_token: *ctx.accounts.custody_token.key, + mint: *ctx.accounts.mint.key, + custody_authority: *ctx.accounts.custody_authority.key, + recipient: ctx.accounts.recipient.as_ref().map(|info| *info.key), + system_program: *ctx.accounts.system_program.key, + token_program: *ctx.accounts.token_program.key, + }), + &ctx.to_account_infos(), + ctx.signer_seeds, + ) + .map_err(Into::into) + } + + pub fn complete_transfer_wrapped<'info>( + ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWrapped<'info>>, + ) -> Result<()> { + invoke_signed( + &instruction::complete_transfer_wrapped(accounts::CompleteTransferWrapped { + payer: *ctx.accounts.payer.key, + vaa: *ctx.accounts.vaa.key, + claim: *ctx.accounts.claim.key, + registered_emitter: *ctx.accounts.registered_emitter.key, + recipient_token: *ctx.accounts.recipient_token.key, + payer_token: *ctx.accounts.payer_token.key, + wrapped_mint: *ctx.accounts.wrapped_mint.key, + wrapped_asset: *ctx.accounts.wrapped_asset.key, + mint_authority: *ctx.accounts.mint_authority.key, + recipient: ctx.accounts.recipient.as_ref().map(|info| *info.key), + system_program: *ctx.accounts.system_program.key, + token_program: *ctx.accounts.token_program.key, + }), + &ctx.to_account_infos(), + ctx.signer_seeds, + ) + .map_err(Into::into) + } + + pub fn complete_transfer_with_payload_native<'info>( + ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWithPayloadNative<'info>>, + ) -> Result<()> { + invoke_signed( + &instruction::complete_transfer_with_payload_native( + accounts::CompleteTransferWithPayloadNative { + payer: *ctx.accounts.payer.key, + vaa: *ctx.accounts.vaa.key, + claim: *ctx.accounts.claim.key, + registered_emitter: *ctx.accounts.registered_emitter.key, + dst_token: *ctx.accounts.dst_token.key, + redeemer_authority: *ctx.accounts.redeemer_authority.key, + custody_token: *ctx.accounts.custody_token.key, + mint: *ctx.accounts.mint.key, + custody_authority: *ctx.accounts.custody_authority.key, + system_program: *ctx.accounts.system_program.key, + token_program: *ctx.accounts.token_program.key, + }, + ), + &ctx.to_account_infos(), + ctx.signer_seeds, + ) + .map_err(Into::into) + } + + pub fn complete_transfer_with_payload_wrapped<'info>( + ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWithPayloadWrapped<'info>>, + ) -> Result<()> { + invoke_signed( + &instruction::complete_transfer_with_payload_wrapped( + accounts::CompleteTransferWithPayloadWrapped { + payer: *ctx.accounts.payer.key, + vaa: *ctx.accounts.vaa.key, + claim: *ctx.accounts.claim.key, + registered_emitter: *ctx.accounts.registered_emitter.key, + dst_token: *ctx.accounts.dst_token.key, + redeemer_authority: *ctx.accounts.redeemer_authority.key, + wrapped_mint: *ctx.accounts.wrapped_mint.key, + wrapped_asset: *ctx.accounts.wrapped_asset.key, + mint_authority: *ctx.accounts.mint_authority.key, + system_program: *ctx.accounts.system_program.key, + token_program: *ctx.accounts.token_program.key, + }, + ), + &ctx.to_account_infos(), + ctx.signer_seeds, + ) + .map_err(Into::into) + } + pub fn transfer_tokens_native<'info>( ctx: CpiContext<'_, '_, '_, 'info, TransferTokensNative<'info>>, - args: TransferTokensArgs, + args: instruction::TransferTokensArgs, ) -> Result<()> { invoke_signed( &instruction::transfer_tokens_native( - instruction::TransferTokensNative { + accounts::TransferTokensNative { payer: *ctx.accounts.payer.key, src_token: *ctx.accounts.src_token.key, mint: *ctx.accounts.mint.key, @@ -40,8 +145,8 @@ pub mod cpi { .as_ref() .map(|info| *info.key), system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, token_program: *ctx.accounts.token_program.key, + core_bridge_program: *ctx.accounts.core_bridge_program.key, }, args, ), @@ -53,11 +158,11 @@ pub mod cpi { pub fn transfer_tokens_wrapped<'info>( ctx: CpiContext<'_, '_, '_, 'info, TransferTokensWrapped<'info>>, - args: TransferTokensArgs, + args: instruction::TransferTokensArgs, ) -> Result<()> { invoke_signed( &instruction::transfer_tokens_wrapped( - instruction::TransferTokensWrapped { + accounts::TransferTokensWrapped { payer: *ctx.accounts.payer.key, src_token: *ctx.accounts.src_token.key, wrapped_mint: *ctx.accounts.wrapped_mint.key, @@ -73,8 +178,8 @@ pub mod cpi { .as_ref() .map(|info| *info.key), system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, token_program: *ctx.accounts.token_program.key, + core_bridge_program: *ctx.accounts.core_bridge_program.key, }, args, ), @@ -86,11 +191,11 @@ pub mod cpi { pub fn transfer_tokens_with_payload_native<'info>( ctx: CpiContext<'_, '_, '_, 'info, TransferTokensWithPayloadNative<'info>>, - args: TransferTokensWithPayloadArgs, + args: instruction::TransferTokensWithPayloadArgs, ) -> Result<()> { invoke_signed( &instruction::transfer_tokens_with_payload_native( - instruction::TransferTokensWithPayloadNative { + accounts::TransferTokensWithPayloadNative { payer: *ctx.accounts.payer.key, src_token: *ctx.accounts.src_token.key, mint: *ctx.accounts.mint.key, @@ -108,8 +213,8 @@ pub mod cpi { .map(|info| *info.key), sender_authority: *ctx.accounts.sender_authority.key, system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, token_program: *ctx.accounts.token_program.key, + core_bridge_program: *ctx.accounts.core_bridge_program.key, }, args, ), @@ -121,11 +226,11 @@ pub mod cpi { pub fn transfer_tokens_with_payload_wrapped<'info>( ctx: CpiContext<'_, '_, '_, 'info, TransferTokensWithPayloadWrapped<'info>>, - args: TransferTokensWithPayloadArgs, + args: instruction::TransferTokensWithPayloadArgs, ) -> Result<()> { invoke_signed( &instruction::transfer_tokens_with_payload_wrapped( - instruction::TransferTokensWithPayloadWrapped { + accounts::TransferTokensWithPayloadWrapped { payer: *ctx.accounts.payer.key, src_token: *ctx.accounts.src_token.key, wrapped_mint: *ctx.accounts.wrapped_mint.key, @@ -142,8 +247,8 @@ pub mod cpi { .map(|info| *info.key), sender_authority: *ctx.accounts.sender_authority.key, system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, token_program: *ctx.accounts.token_program.key, + core_bridge_program: *ctx.accounts.core_bridge_program.key, }, args, ), @@ -153,108 +258,161 @@ pub mod cpi { .map_err(Into::into) } - pub fn complete_transfer_native<'info>( - ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferNative<'info>>, - ) -> Result<()> { - invoke_signed( - &instruction::complete_transfer_native(instruction::CompleteTransferNative { - payer: *ctx.accounts.payer.key, - posted_vaa: *ctx.accounts.posted_vaa.key, - claim: *ctx.accounts.claim.key, - registered_emitter: *ctx.accounts.registered_emitter.key, - recipient_token: *ctx.accounts.recipient_token.key, - payer_token: *ctx.accounts.payer_token.key, - custody_token: *ctx.accounts.custody_token.key, - mint: *ctx.accounts.mint.key, - custody_authority: *ctx.accounts.custody_authority.key, - recipient: ctx.accounts.recipient.as_ref().map(|info| *info.key), - system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, - token_program: *ctx.accounts.token_program.key, - }), - &ctx.to_account_infos(), - ctx.signer_seeds, - ) - .map_err(Into::into) + #[derive(Accounts)] + pub struct CompleteTransferNative<'info> { + /// CHECK: Transaction payer (mut signer). + pub payer: AccountInfo<'info>, + /// CHECK: Posted VAA Account (read-only, seeds = ["PostedVAA", message_hash], + /// seeds::program = core_bridge_program). + pub vaa: AccountInfo<'info>, + /// CHECK: Claim Account (mut, seeds = [emitter_address, emitter_chain, sequence], + /// seeds::program = token_bridge_program). + pub claim: AccountInfo<'info>, + /// CHECK: Registered Emitter Account (mut, seeds = \[emitter_chain\], seeds::program = + /// token_bridge_program). + /// + /// NOTE: If the above PDA does not exist, there is a legacy account whose address is + /// derived using seeds = \[emitter_chain, emitter_address\]. + pub registered_emitter: AccountInfo<'info>, + /// CHECK: Recipient Token Account (mut). + pub recipient_token: AccountInfo<'info>, + /// CHECK: Payer (Relayer) Token Account (mut). + pub payer_token: AccountInfo<'info>, + /// CHECK: Custody Token Account (mut, seeds = \[mint.key\], seeds::program = + /// token_bridge_program). + pub custody_token: AccountInfo<'info>, + /// CHECK: Mint (read-only). + pub mint: AccountInfo<'info>, + /// CHECK: Custody Authority (read-only, seeds = \["custody_signer"\], seeds::program = + /// token_bridge_program). + pub custody_authority: AccountInfo<'info>, + /// CHECK: Recipient, which should be the account owner of recipient token (read-only). + /// + /// NOTE: This used to be the rent sysvar. If the VAA encodes the recipient token account, + /// this account does not need to be provided. Otherwise you need to provide this account, + /// whose pubkey should match the VAA recipient. + pub recipient: Option>, + /// CHECK: System Program. + pub system_program: AccountInfo<'info>, + /// CHECK: Token Program. + pub token_program: AccountInfo<'info>, } - pub fn complete_transfer_wrapped<'info>( - ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWrapped<'info>>, - ) -> Result<()> { - invoke_signed( - &instruction::complete_transfer_wrapped(instruction::CompleteTransferWrapped { - payer: *ctx.accounts.payer.key, - posted_vaa: *ctx.accounts.posted_vaa.key, - claim: *ctx.accounts.claim.key, - registered_emitter: *ctx.accounts.registered_emitter.key, - recipient_token: *ctx.accounts.recipient_token.key, - payer_token: *ctx.accounts.payer_token.key, - wrapped_mint: *ctx.accounts.wrapped_mint.key, - wrapped_asset: *ctx.accounts.wrapped_asset.key, - mint_authority: *ctx.accounts.mint_authority.key, - recipient: ctx.accounts.recipient.as_ref().map(|info| *info.key), - system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, - token_program: *ctx.accounts.token_program.key, - }), - &ctx.to_account_infos(), - ctx.signer_seeds, - ) - .map_err(Into::into) + #[derive(Accounts)] + pub struct CompleteTransferWrapped<'info> { + /// CHECK: Transaction payer (mut signer). + pub payer: AccountInfo<'info>, + /// CHECK: Posted VAA Account (read-only, seeds = \["PostedVAA", message_hash\], + /// seeds::program = core_bridge_program). + pub vaa: AccountInfo<'info>, + /// CHECK: Claim Account (mut, seeds = \[emitter_address, emitter_chain, sequence\], + /// seeds::program = token_bridge_program). + pub claim: AccountInfo<'info>, + /// CHECK: Registered Emitter Account (mut, seeds = \[emitter_chain\], seeds::program = + /// token_bridge_program). + /// + /// NOTE: If the above PDA does not exist, there is a legacy account whose address is + /// derived using seeds = \[emitter_chain, emitter_address\]. + pub registered_emitter: AccountInfo<'info>, + /// CHECK: Recipient Token Account (mut). + pub recipient_token: AccountInfo<'info>, + /// CHECK: Payer (Relayer) Token Account (mut). + pub payer_token: AccountInfo<'info>, + /// CHECK: Wrapped Mint (mut, seeds = \["wrapped", token_chain, token_address\], + /// seeds::program = token_bridge_program). + pub wrapped_mint: AccountInfo<'info>, + /// CHECK: Wrapped Asset (read-only, seeds = \["meta", wrapped_mint.key\], seeds::program = + /// token_bridge_program). + pub wrapped_asset: AccountInfo<'info>, + /// CHECK: Mint Authority (read-only, seeds = \["mint_signer"\], seeds::program = + /// token_bridge_program). + pub mint_authority: AccountInfo<'info>, + /// CHECK: Recipient, which should be the account owner of recipient token (read-only). + /// + /// NOTE: This used to be the rent sysvar. If the VAA encodes the recipient token account, + /// this account does not need to be provided. Otherwise you need to provide this account, + /// whose pubkey should match the VAA recipient. + pub recipient: Option>, + /// CHECK: System Program. + pub system_program: AccountInfo<'info>, + /// CHECK: Token Program. + pub token_program: AccountInfo<'info>, } - pub fn complete_transfer_with_payload_native<'info>( - ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWithPayloadNative<'info>>, - ) -> Result<()> { - invoke_signed( - &instruction::complete_transfer_with_payload_native( - instruction::CompleteTransferWithPayloadNative { - payer: *ctx.accounts.payer.key, - posted_vaa: *ctx.accounts.posted_vaa.key, - claim: *ctx.accounts.claim.key, - registered_emitter: *ctx.accounts.registered_emitter.key, - dst_token: *ctx.accounts.dst_token.key, - redeemer_authority: *ctx.accounts.redeemer_authority.key, - custody_token: *ctx.accounts.custody_token.key, - mint: *ctx.accounts.mint.key, - custody_authority: *ctx.accounts.custody_authority.key, - system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, - token_program: *ctx.accounts.token_program.key, - }, - ), - &ctx.to_account_infos(), - ctx.signer_seeds, - ) - .map_err(Into::into) + #[derive(Accounts)] + pub struct CompleteTransferWithPayloadNative<'info> { + /// CHECK: Transaction payer (mut signer). + pub payer: AccountInfo<'info>, + /// CHECK: Posted VAA Account (read-only, seeds = \["PostedVAA", message_hash\], + /// seeds::program = core_bridge_program). + pub vaa: AccountInfo<'info>, + /// CHECK: Claim Account (mut, seeds = \[emitter_address, emitter_chain, sequence\], + /// seeds::program = token_bridge_program). + pub claim: AccountInfo<'info>, + /// CHECK: Registered Emitter Account (mut, seeds = \[emitter_chain\], seeds::program = + /// token_bridge_program). + /// + /// NOTE: If the above PDA does not exist, there is a legacy account whose address is + /// derived using seeds = \[emitter_chain, emitter_address\]. + pub registered_emitter: AccountInfo<'info>, + /// CHECK: Destination Token Account (mut). + pub dst_token: AccountInfo<'info>, + /// CHECK: Redeemer Authority (read-only signer). + /// + /// NOTE: In order to redeem a transfer sent to an address matching your program ID, use + /// seeds = \["redeemer"\]. + pub redeemer_authority: AccountInfo<'info>, + /// CHECK: Custody Token Account (mut, seeds = \[mint.key\], seeds::program = + /// token_bridge_program). + pub custody_token: AccountInfo<'info>, + /// CHECK: Mint (read-only). + pub mint: AccountInfo<'info>, + /// CHECK: Custody Authority (read-only, seeds = \["custody_signer"\], seeds::program = + /// token_bridge_program). + pub custody_authority: AccountInfo<'info>, + /// CHECK: System Program. + pub system_program: AccountInfo<'info>, + /// CHECK: Token Program. + pub token_program: AccountInfo<'info>, } - pub fn complete_transfer_with_payload_wrapped<'info>( - ctx: CpiContext<'_, '_, '_, 'info, CompleteTransferWithPayloadWrapped<'info>>, - ) -> Result<()> { - invoke_signed( - &instruction::complete_transfer_with_payload_wrapped( - instruction::CompleteTransferWithPayloadWrapped { - payer: *ctx.accounts.payer.key, - posted_vaa: *ctx.accounts.posted_vaa.key, - claim: *ctx.accounts.claim.key, - registered_emitter: *ctx.accounts.registered_emitter.key, - dst_token: *ctx.accounts.dst_token.key, - redeemer_authority: *ctx.accounts.redeemer_authority.key, - wrapped_mint: *ctx.accounts.wrapped_mint.key, - wrapped_asset: *ctx.accounts.wrapped_asset.key, - mint_authority: *ctx.accounts.mint_authority.key, - system_program: *ctx.accounts.system_program.key, - core_bridge_program: *ctx.accounts.core_bridge_program.key, - token_program: *ctx.accounts.token_program.key, - }, - ), - &ctx.to_account_infos(), - ctx.signer_seeds, - ) - .map_err(Into::into) + #[derive(Accounts)] + pub struct CompleteTransferWithPayloadWrapped<'info> { + /// CHECK: Transaction payer (mut signer). + pub payer: AccountInfo<'info>, + /// CHECK: Posted VAA Account (read-only, seeds = \["PostedVAA", message_hash\], + /// seeds::program = core_bridge_program). + pub vaa: AccountInfo<'info>, + /// CHECK: Claim Account (mut, seeds = \[emitter_address, emitter_chain, sequence\], + /// seeds::program = token_bridge_program). + pub claim: AccountInfo<'info>, + /// CHECK: Registered Emitter Account (mut, seeds = \[emitter_chain\], seeds::program = + /// token_bridge_program). + /// + /// NOTE: If the above PDA does not exist, there is a legacy account whose address is + /// derived using seeds = \[emitter_chain, emitter_address\]. + pub registered_emitter: AccountInfo<'info>, + /// CHECK: Destination Token Account (mut). + pub dst_token: AccountInfo<'info>, + /// CHECK: Redeemer Authority (read-only signer). + /// + /// NOTE: In order to redeem a transfer sent to an address matching your program ID, use + /// seeds = ["redeemer"]. + pub redeemer_authority: AccountInfo<'info>, + /// CHECK: Wrapped Mint (mut, seeds = \["wrapped", token_chain, token_address\], + /// seeds::program = token_bridge_program). + pub wrapped_mint: AccountInfo<'info>, + /// CHECK: Wrapped Asset (read-only, seeds = \["meta", wrapped_mint.key\], seeds::program = + /// token_bridge_program). + pub wrapped_asset: AccountInfo<'info>, + /// CHECK: Mint Authority (read-only, seeds = \["mint_signer"\], seeds::program = + /// token_bridge_program). + pub mint_authority: AccountInfo<'info>, + /// CHECK: System Program. + pub system_program: AccountInfo<'info>, + /// CHECK: Token Program. + pub token_program: AccountInfo<'info>, } - #[derive(Accounts)] pub struct TransferTokensNative<'info> { /// CHECK: Transaction payer (mut signer). @@ -263,35 +421,35 @@ pub mod cpi { pub src_token: AccountInfo<'info>, /// CHECK: Mint (read-only). pub mint: AccountInfo<'info>, - /// CHECK: Transfer Authority (mut, seeds = [mint.key], seeds::program = + /// CHECK: Custody Token Account (mut, seeds = \[mint.key\], seeds::program = /// token_bridge_program). pub custody_token: AccountInfo<'info>, - /// CHECK: Transfer Authority (read-only, seeds = ["authority_signer"], seeds::program = + /// CHECK: Transfer Authority (read-only, seeds = \["authority_signer"\], seeds::program = /// token_bridge_program). pub transfer_authority: AccountInfo<'info>, - /// CHECK: Custody Authority (read-only, seeds = ["custody_signer"], seeds::program = + /// CHECK: Custody Authority (read-only, seeds = \["custody_signer"\], seeds::program = /// token_bridge_program). pub custody_authority: AccountInfo<'info>, - /// CHECK: Core Bridge Program Data (mut, seeds = ["Bridge"], seeds::program = + /// CHECK: Core Bridge Program Data (mut, seeds = \["Bridge"\], seeds::program = /// core_bridge_program). pub core_bridge_config: AccountInfo<'info>, /// CHECK: Core Bridge Message (mut). pub core_message: AccountInfo<'info>, - /// CHECK: Core Bridge Emitter (read-only, seeds = ["emitter"], seeds::program = + /// CHECK: Core Bridge Emitter (read-only, seeds = \["emitter"\], seeds::program = /// token_bridge_program). pub core_emitter: AccountInfo<'info>, - /// CHECK: Core Bridge Emitter Sequence (mut, seeds = ["Sequence", emitter.key], + /// CHECK: Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\], /// seeds::program = core_bridge_program). pub core_emitter_sequence: AccountInfo<'info>, - /// CHECK: Core Bridge Fee Collector (mut, seeds = ["fee_collector"], seeds::program = + /// CHECK: Core Bridge Fee Collector (mut, seeds = \["fee_collector"\], seeds::program = /// core_bridge_program). pub core_fee_collector: Option>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, + /// CHECK: SPL Token Program. + pub token_program: AccountInfo<'info>, /// CHECK: Core Bridge Program. pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, } #[derive(Accounts)] @@ -300,35 +458,35 @@ pub mod cpi { pub payer: AccountInfo<'info>, /// CHECK: Source Token Account (mut). pub src_token: AccountInfo<'info>, - /// CHECK: Wrapped Mint (mut, seeds = ["wrapped", token_chain, token_address], + /// CHECK: Wrapped Mint (mut, seeds = \["wrapped", token_chain, token_address\], /// seeds::program = token_bridge_program). pub wrapped_mint: AccountInfo<'info>, - /// CHECK: Wrapped Asset (read-only, seeds = [wrapped_mint.key], seeds::program = + /// CHECK: Wrapped Asset (read-only, seeds = ["meta", wrapped_mint.key], seeds::program = /// token_bridge_program). pub wrapped_asset: AccountInfo<'info>, - /// CHECK: Transfer Authority (read-only, seeds = ["authority_signer"], seeds::program = + /// CHECK: Transfer Authority (read-only, seeds = \["authority_signer"\], seeds::program = /// token_bridge_program). pub transfer_authority: AccountInfo<'info>, - /// CHECK: Core Bridge Program Data (mut, seeds = ["Bridge"], seeds::program = + /// CHECK: Core Bridge Program Data (mut, seeds = \["Bridge"\], seeds::program = /// core_bridge_program). pub core_bridge_config: AccountInfo<'info>, /// CHECK: Core Bridge Message (mut). pub core_message: AccountInfo<'info>, - /// CHECK: Core Bridge Emitter (read-only, seeds = ["emitter"], seeds::program = + /// CHECK: Core Bridge Emitter (read-only, seeds = \["emitter"\], seeds::program = /// token_bridge_program). pub core_emitter: AccountInfo<'info>, - /// CHECK: Core Bridge Emitter Sequence (mut, seeds = ["Sequence", emitter.key], + /// CHECK: Core Bridge Emitter Sequence (mut, seeds = \["Sequence", emitter.key\], /// seeds::program = core_bridge_program). pub core_emitter_sequence: AccountInfo<'info>, - /// CHECK: Core Bridge Fee Collector (mut, seeds = ["fee_collector"], seeds::program = + /// CHECK: Core Bridge Fee Collector (mut, seeds = \["fee_collector"\], seeds::program = /// core_bridge_program). pub core_fee_collector: Option>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, + /// CHECK: SPL Token Program. + pub token_program: AccountInfo<'info>, /// CHECK: Core Bridge Program. pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, } #[derive(Accounts)] @@ -339,7 +497,7 @@ pub mod cpi { pub src_token: AccountInfo<'info>, /// CHECK: Mint (read-only). pub mint: AccountInfo<'info>, - /// CHECK: Transfer Authority (mut, seeds = [mint.key], seeds::program = + /// CHECK: Custody Token Account (mut, seeds = \[mint.key\], seeds::program = /// token_bridge_program). pub custody_token: AccountInfo<'info>, /// CHECK: Transfer Authority (read-only, seeds = ["authority_signer"], seeds::program = @@ -369,10 +527,10 @@ pub mod cpi { pub sender_authority: AccountInfo<'info>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, - /// CHECK: Core Bridge Program. - pub core_bridge_program: AccountInfo<'info>, /// CHECK: Token Program. pub token_program: AccountInfo<'info>, + /// CHECK: Core Bridge Program. + pub core_bridge_program: AccountInfo<'info>, } #[derive(Accounts)] @@ -384,7 +542,7 @@ pub mod cpi { /// CHECK: Wrapped Mint (mut, seeds = ["wrapped", token_chain, token_address], /// seeds::program = token_bridge_program). pub wrapped_mint: AccountInfo<'info>, - /// CHECK: Wrapped Asset (read-only, seeds = [wrapped_mint.key], seeds::program = + /// CHECK: Wrapped Asset (read-only, seeds = ["meta", wrapped_mint.key], seeds::program = /// token_bridge_program). pub wrapped_asset: AccountInfo<'info>, /// CHECK: Transfer Authority (read-only, seeds = ["authority_signer"], seeds::program = @@ -411,173 +569,9 @@ pub mod cpi { pub sender_authority: AccountInfo<'info>, /// CHECK: System Program. pub system_program: AccountInfo<'info>, - /// CHECK: Core Bridge Program. - pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, - } - - #[derive(Accounts)] - pub struct CompleteTransferNative<'info> { - /// CHECK: Transaction payer (mut signer). - pub payer: AccountInfo<'info>, - /// CHECK: Posted VAA Account (read-only, seeds = ["PostedVAA", message_hash], - /// seeds::program = core_bridge_program). - pub posted_vaa: AccountInfo<'info>, - /// CHECK: Claim Account (mut, seeds = [emitter_address, emitter_chain, sequence], - /// seeds::program = token_bridge_program). - pub claim: AccountInfo<'info>, - /// CHECK: Registered Emitter Account (mut, seeds = [emitter_chain], seeds::program = - /// token_bridge_program). - /// - /// NOTE: If the above PDA does not exist, there is a legacy account whose address is - /// derived using seeds = [emitter_chain, emitter_address]. - pub registered_emitter: AccountInfo<'info>, - /// CHECK: Recipient Token Account (mut). - pub recipient_token: AccountInfo<'info>, - /// CHECK: Payer (Relayer) Token Account (mut). - pub payer_token: AccountInfo<'info>, - /// CHECK: Transfer Authority (mut, seeds = [mint.key], seeds::program = - /// token_bridge_program). - pub custody_token: AccountInfo<'info>, - /// CHECK: Mint (read-only). - pub mint: AccountInfo<'info>, - /// CHECK: Custody Authority (read-only, seeds = ["custody_signer"], seeds::program = - /// token_bridge_program). - pub custody_authority: AccountInfo<'info>, - /// CHECK: Recipient, which should be the account owner of recipient token (read-only). - /// - /// NOTE: This used to be the rent sysvar. If the VAA encodes the recipient token account, - /// this account does not need to be provided. Otherwise you need to provide this account, - /// whose pubkey should match the VAA recipient. - pub recipient: Option>, - /// CHECK: System Program. - pub system_program: AccountInfo<'info>, - /// CHECK: Core Bridge Program. - pub core_bridge_program: AccountInfo<'info>, /// CHECK: Token Program. pub token_program: AccountInfo<'info>, - } - - #[derive(Accounts)] - pub struct CompleteTransferWrapped<'info> { - /// CHECK: Transaction payer (mut signer). - pub payer: AccountInfo<'info>, - /// CHECK: Posted VAA Account (read-only, seeds = ["PostedVAA", message_hash], - /// seeds::program = core_bridge_program). - pub posted_vaa: AccountInfo<'info>, - /// CHECK: Claim Account (mut, seeds = [emitter_address, emitter_chain, sequence], - /// seeds::program = token_bridge_program). - pub claim: AccountInfo<'info>, - /// CHECK: Registered Emitter Account (mut, seeds = [emitter_chain], seeds::program = - /// token_bridge_program). - /// - /// NOTE: If the above PDA does not exist, there is a legacy account whose address is - /// derived using seeds = [emitter_chain, emitter_address]. - pub registered_emitter: AccountInfo<'info>, - /// CHECK: Recipient Token Account (mut). - pub recipient_token: AccountInfo<'info>, - /// CHECK: Payer (Relayer) Token Account (mut). - pub payer_token: AccountInfo<'info>, - /// CHECK: Wrapped Mint (mut, seeds = ["wrapped", token_chain, token_address], - /// seeds::program = token_bridge_program). - pub wrapped_mint: AccountInfo<'info>, - /// CHECK: Wrapped Asset (read-only, seeds = [wrapped_mint.key], seeds::program = - /// token_bridge_program). - pub wrapped_asset: AccountInfo<'info>, - /// CHECK: Mint Authority (read-only, seeds = ["mint_signer"], seeds::program = - /// token_bridge_program). - pub mint_authority: AccountInfo<'info>, - /// CHECK: Recipient, which should be the account owner of recipient token (read-only). - /// - /// NOTE: This used to be the rent sysvar. If the VAA encodes the recipient token account, - /// this account does not need to be provided. Otherwise you need to provide this account, - /// whose pubkey should match the VAA recipient. - pub recipient: Option>, - /// CHECK: System Program. - pub system_program: AccountInfo<'info>, /// CHECK: Core Bridge Program. pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, - } - - #[derive(Accounts)] - pub struct CompleteTransferWithPayloadNative<'info> { - /// CHECK: Transaction payer (mut signer). - pub payer: AccountInfo<'info>, - /// CHECK: Posted VAA Account (read-only, seeds = ["PostedVAA", message_hash], - /// seeds::program = core_bridge_program). - pub posted_vaa: AccountInfo<'info>, - /// CHECK: Claim Account (mut, seeds = [emitter_address, emitter_chain, sequence], - /// seeds::program = token_bridge_program). - pub claim: AccountInfo<'info>, - /// CHECK: Registered Emitter Account (mut, seeds = [emitter_chain], seeds::program = - /// token_bridge_program). - /// - /// NOTE: If the above PDA does not exist, there is a legacy account whose address is - /// derived using seeds = [emitter_chain, emitter_address]. - pub registered_emitter: AccountInfo<'info>, - /// CHECK: Destination Token Account (mut). - pub dst_token: AccountInfo<'info>, - /// CHECK: Redeemer Authority (read-only signer). - /// - /// NOTE: In order to redeem a transfer sent to an address matching your program ID, use - /// seeds = ["redeemer"]. - pub redeemer_authority: AccountInfo<'info>, - /// CHECK: Transfer Authority (mut, seeds = [mint.key], seeds::program = - /// token_bridge_program). - pub custody_token: AccountInfo<'info>, - /// CHECK: Mint (read-only). - pub mint: AccountInfo<'info>, - /// CHECK: Custody Authority (read-only, seeds = ["custody_signer"], seeds::program = - /// token_bridge_program). - pub custody_authority: AccountInfo<'info>, - /// CHECK: System Program. - pub system_program: AccountInfo<'info>, - /// CHECK: Core Bridge Program. - pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, - } - - #[derive(Accounts)] - pub struct CompleteTransferWithPayloadWrapped<'info> { - /// CHECK: Transaction payer (mut signer). - pub payer: AccountInfo<'info>, - /// CHECK: Posted VAA Account (read-only, seeds = ["PostedVAA", message_hash], - /// seeds::program = core_bridge_program). - pub posted_vaa: AccountInfo<'info>, - /// CHECK: Claim Account (mut, seeds = [emitter_address, emitter_chain, sequence], - /// seeds::program = token_bridge_program). - pub claim: AccountInfo<'info>, - /// CHECK: Registered Emitter Account (mut, seeds = [emitter_chain], seeds::program = - /// token_bridge_program). - /// - /// NOTE: If the above PDA does not exist, there is a legacy account whose address is - /// derived using seeds = [emitter_chain, emitter_address]. - pub registered_emitter: AccountInfo<'info>, - /// CHECK: Destination Token Account (mut). - pub dst_token: AccountInfo<'info>, - /// CHECK: Redeemer Authority (read-only signer). - /// - /// NOTE: In order to redeem a transfer sent to an address matching your program ID, use - /// seeds = ["redeemer"]. - pub redeemer_authority: AccountInfo<'info>, - /// CHECK: Wrapped Mint (mut, seeds = ["wrapped", token_chain, token_address], - /// seeds::program = token_bridge_program). - pub wrapped_mint: AccountInfo<'info>, - /// CHECK: Wrapped Asset (read-only, seeds = [wrapped_mint.key], seeds::program = - /// token_bridge_program). - pub wrapped_asset: AccountInfo<'info>, - /// CHECK: Mint Authority (read-only, seeds = ["mint_signer"], seeds::program = - /// token_bridge_program). - pub mint_authority: AccountInfo<'info>, - /// CHECK: System Program. - pub system_program: AccountInfo<'info>, - /// CHECK: Core Bridge Program. - pub core_bridge_program: AccountInfo<'info>, - /// CHECK: Token Program. - pub token_program: AccountInfo<'info>, } } diff --git a/solana/programs/token-bridge/src/legacy/processor/attest_token.rs b/solana/programs/token-bridge/src/legacy/processor/attest_token.rs index fd64301895..e69272d171 100644 --- a/solana/programs/token-bridge/src/legacy/processor/attest_token.rs +++ b/solana/programs/token-bridge/src/legacy/processor/attest_token.rs @@ -1,10 +1,9 @@ use crate::{ - constants::EMITTER_SEED_PREFIX, legacy::LegacyAttestTokenArgs, - processor::post_token_bridge_message, zero_copy::Mint, + error::TokenBridgeError, legacy::instruction::LegacyAttestTokenArgs, utils, zero_copy::Mint, }; use anchor_lang::prelude::*; use anchor_spl::metadata; -use core_bridge_program::sdk as core_bridge_sdk; +use core_bridge_program::sdk::{self as core_bridge_sdk, LoadZeroCopy}; #[derive(Accounts)] pub struct AttestToken<'info> { @@ -33,7 +32,7 @@ pub struct AttestToken<'info> { bump, seeds::program = metadata::Metadata::id() )] - token_metadata: Box>, + token_metadata: Account<'info, metadata::MetadataAccount>, /// CHECK: This account is needed for the Core Bridge program. #[account(mut)] @@ -44,10 +43,7 @@ pub struct AttestToken<'info> { core_message: Signer<'info>, /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] + /// This PDA address is checked in `post_token_bridge_message`. core_emitter: AccountInfo<'info>, /// CHECK: This account is needed for the Core Bridge program. @@ -68,22 +64,7 @@ pub struct AttestToken<'info> { core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, } -impl<'info> - core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, LegacyAttestTokenArgs> - for AttestToken<'info> -{ - const LOG_IX_NAME: &'static str = "LegacyAttestToken"; - - const ANCHOR_IX_FN: fn(Context, LegacyAttestTokenArgs) -> Result<()> = attest_token; -} - -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for AttestToken<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() - } -} - -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for AttestToken<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> for AttestToken<'info> { fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -94,16 +75,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for AttestToken<'info> { } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for AttestToken<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -117,11 +98,65 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for AttestToken<'info> { } } +impl<'info> + core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, LegacyAttestTokenArgs> + for AttestToken<'info> +{ + const LOG_IX_NAME: &'static str = "LegacyAttestToken"; + + const ANCHOR_IX_FN: fn(Context, LegacyAttestTokenArgs) -> Result<()> = attest_token; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + const NUM_ACCOUNTS: usize = 14; + const CORE_BRIDGE_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + const SYSTEM_PROGRAM_IDX: usize = CORE_BRIDGE_PROGRAM_IDX - 1; + + let mut infos = account_infos.to_vec(); + + // This check is inclusive because Core Bridge program, System program and Token program can + // be in any order. + if infos.len() >= NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + + // Core Bridge program needs to exist in these account infos. + let core_bridge_program_idx = infos + .iter() + .position(|info| info.key() == core_bridge_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if core_bridge_program_idx != CORE_BRIDGE_PROGRAM_IDX { + infos.swap(CORE_BRIDGE_PROGRAM_IDX, core_bridge_program_idx); + } + } + + // Done. + Ok(infos) + } +} + impl<'info> AttestToken<'info> { fn constraints(ctx: &Context) -> Result<()> { // Make sure the mint authority is not the Token Bridge's. If it is, then this mint // originated from a foreign network. - crate::utils::require_native_mint(&ctx.accounts.mint) + let mint = Mint::load(&ctx.accounts.mint)?; + require!( + !crate::utils::is_wrapped_mint(&mint), + TokenBridgeError::WrappedAsset + ); + + Ok(()) } } @@ -130,14 +165,12 @@ fn attest_token(ctx: Context, args: LegacyAttestTokenArgs) -> Resul let LegacyAttestTokenArgs { nonce } = args; let metadata = &ctx.accounts.token_metadata.data; - let decimals = Mint::parse(&ctx.accounts.mint.data.borrow()) - .unwrap() - .decimals(); + let decimals = Mint::load(&ctx.accounts.mint).unwrap().decimals(); // Finally post Wormhole message via Core Bridge. - post_token_bridge_message( + utils::cpi::post_token_bridge_message( ctx.accounts, - ctx.bumps["core_emitter"], + &ctx.accounts.core_message, nonce, crate::messages::Attestation { token_address: ctx.accounts.mint.key().to_bytes(), diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/mod.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/mod.rs index 681848eda9..46dce4b05f 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/mod.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/mod.rs @@ -7,30 +7,31 @@ pub use wrapped::*; use crate::{error::TokenBridgeError, state::RegisteredEmitter}; use anchor_lang::prelude::*; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; -use wormhole_raw_vaas::token_bridge::{TokenBridgeMessage, Transfer}; - -pub fn validate_posted_token_transfer<'ctx>( - vaa_acc_key: &'ctx Pubkey, - vaa_acc_data: &'ctx [u8], - registered_emitter: &'ctx Account<'_, LegacyAnchorized<0, RegisteredEmitter>>, - recipient_token: &'ctx AccountInfo<'_>, - recipient: &'ctx Option, -) -> Result> { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - let msg = - crate::utils::require_valid_posted_token_bridge_vaa(vaa_acc_key, &vaa, registered_emitter)?; - - let transfer = match msg { - TokenBridgeMessage::Transfer(inner) => inner, - _ => return err!(TokenBridgeError::InvalidTokenBridgeVaa), +use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; + +pub fn validate_token_transfer_vaa( + vaa_acc_info: &AccountInfo, + registered_emitter: &Account>, + recipient_token: &AccountInfo, + recipient: &Option, +) -> Result<(u16, [u8; 32])> { + let vaa_key = vaa_acc_info.key(); + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let msg = crate::utils::require_valid_token_bridge_vaa(&vaa_key, &vaa, registered_emitter)?; + + let transfer = if let TokenBridgeMessage::Transfer(inner) = msg { + inner + } else { + return err!(TokenBridgeError::InvalidTokenBridgeVaa); }; // This token bridge transfer must be intended to be redeemed on Solana. require_eq!( transfer.recipient_chain(), - SOLANA_CHAIN, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::RecipientChainNotSolana ); @@ -68,12 +69,73 @@ pub fn validate_posted_token_transfer<'ctx>( ); // Finally check that the owner of the recipient token account is the encoded recipient. - crate::zero_copy::TokenAccount::require_owner( - &recipient_token.try_borrow_data()?, - &expected_recipient, - )?; + let token_account = crate::zero_copy::TokenAccount::load(recipient_token)?; + require_keys_eq!( + token_account.owner(), + expected_recipient, + ErrorCode::ConstraintTokenOwner + ); + } + + // Done. + Ok((transfer.token_chain(), transfer.token_address())) +} + +/// The Anchor context orders the accounts as: +/// +/// 1. `payer` +/// 2. `config` +/// 3. `vaa` +/// 4. `claim` +/// 5. `registered_emitter` +/// 6. `recipient_token` +/// 7. `payer_token` +/// 8. `custody_token` OR `wrapped_mint` +/// 9. `mint` OR `wrapped_asset` +/// 10. `custody_authority` OR `mint_authority` +/// 11. `recipient` <-- order unspecified +/// 12. `system_program` <-- order unspecified +/// 13. `token_program` <-- order unspecified +/// +/// Because the legacy implementation did not require specifying where the recipient, System program +/// and SPL token program should be, we ensure that these accounts are 11, 12 and 13 respectively +/// because the Anchor account context requires them to be in these positions. +/// +/// NOTE: Recipient as account #11 is optional based on whether the VAA's encoded recipient is the +/// owner of the `recipient_token` account. See the complete transfer account contexts for more +/// info. +pub fn order_complete_transfer_account_infos<'info>( + account_infos: &[AccountInfo<'info>], +) -> Result>> { + const NUM_ACCOUNTS: usize = 13; + const TOKEN_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + const SYSTEM_PROGRAM_IDX: usize = TOKEN_PROGRAM_IDX - 1; + + let mut infos = account_infos.to_vec(); + if infos.len() >= NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + + // Token program needs to exist in these account infos. + let token_program_idx = infos + .iter() + .position(|info| info.key() == anchor_spl::token::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if token_program_idx != TOKEN_PROGRAM_IDX { + infos.swap(TOKEN_PROGRAM_IDX, token_program_idx); + } } // Done. - Ok(transfer) + Ok(infos) } diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/native.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/native.rs index c45dc03f9b..f4d1b098f3 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/native.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/native.rs @@ -1,16 +1,11 @@ use crate::{ - constants::CUSTODY_AUTHORITY_SEED_PREFIX, - error::TokenBridgeError, - legacy::EmptyArgs, - processor::withdraw_native_tokens, - state::{Claim, RegisteredEmitter}, - zero_copy::Mint, + constants::CUSTODY_AUTHORITY_SEED_PREFIX, error::TokenBridgeError, + legacy::instruction::EmptyArgs, state::RegisteredEmitter, utils, zero_copy::Mint, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; @@ -22,29 +17,15 @@ pub struct CompleteTransferNative<'info> { /// CHECK: Token Bridge never needed this account for this instruction. _config: UncheckedAccount<'info>, - /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program - )] - posted_vaa: AccountInfo<'info>, + /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// This account is a foreign token Bridge and is created via the Register Chain governance /// decree. @@ -53,8 +34,8 @@ pub struct CompleteTransferNative<'info> { /// allows registering multiple emitter addresses for the same chain ID. These seeds are not /// checked via Anchor macro, but will be checked in the access control function instead. /// - /// See the `require_valid_token_bridge_posted_vaa` instruction handler for more details. - registered_emitter: Box>>, + /// See the `require_valid_token_bridge_vaa` instruction handler for more details. + registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, /// CHECK: Recipient token account. Because we check the mint of the custody token account, we /// can be sure that this token account is the same mint since the Token Program transfer @@ -104,37 +85,68 @@ pub struct CompleteTransferNative<'info> { recipient: Option>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, - token_program: Program<'info, token::Token>, + token_program: Program<'info, anchor_spl::token::Token>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for CompleteTransferNative<'info> +{ + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + +impl<'info> utils::cpi::Transfer<'info> for CompleteTransferNative<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn from(&self) -> Option> { + Some(self.custody_token.to_account_info()) + } + + fn authority(&self) -> Option> { + Some(self.custody_authority.to_account_info()) + } } impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for CompleteTransferNative<'info> { - const LOG_IX_NAME: &'static str = "LegacCompleteTransferNative"; + const LOG_IX_NAME: &'static str = "LegacyCompleteTransferNative"; const ANCHOR_IX_FN: fn(Context, EmptyArgs) -> Result<()> = complete_transfer_native; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_complete_transfer_account_infos(account_infos) + } } impl<'info> CompleteTransferNative<'info> { fn constraints(ctx: &Context) -> Result<()> { + let payer_token_account = crate::zero_copy::TokenAccount::load(&ctx.accounts.payer_token)?; require_keys_eq!( - crate::zero_copy::TokenAccount::parse(&ctx.accounts.payer_token.try_borrow_data()?)? - .owner(), + payer_token_account.owner(), ctx.accounts.payer.key(), ErrorCode::ConstraintTokenOwner ); // Make sure the mint authority is not the Token Bridge's. If it is, then this mint // originated from a foreign network. - crate::utils::require_native_mint(&ctx.accounts.mint)?; - - let vaa = &ctx.accounts.posted_vaa; - let vaa_key = vaa.key(); - let acc_data = vaa.try_borrow_data()?; - let transfer = super::validate_posted_token_transfer( - &vaa_key, - &acc_data, + let mint = Mint::load(&ctx.accounts.mint)?; + require!( + !crate::utils::is_wrapped_mint(&mint), + TokenBridgeError::WrappedAsset + ); + + let (token_chain, token_address) = super::validate_token_transfer_vaa( + &ctx.accounts.vaa, &ctx.accounts.registered_emitter, &ctx.accounts.recipient_token, &ctx.accounts.recipient, @@ -142,15 +154,15 @@ impl<'info> CompleteTransferNative<'info> { // For native transfers, this mint must have been created on Solana. require_eq!( - transfer.token_chain(), - SOLANA_CHAIN, + token_chain, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::WrappedAsset ); // Mint account must agree with the encoded token address. - require_eq!( + require_keys_eq!( ctx.accounts.mint.key(), - Pubkey::from(transfer.token_address()), + Pubkey::from(token_address), TokenBridgeError::InvalidMint ); @@ -161,18 +173,17 @@ impl<'info> CompleteTransferNative<'info> { #[access_control(CompleteTransferNative::constraints(&ctx))] fn complete_transfer_native(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); - let msg = TokenBridgeMessage::parse(vaa.payload()).unwrap(); + let msg = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()).unwrap(); let transfer = msg.transfer().unwrap(); - let decimals = Mint::parse(&ctx.accounts.mint.data.borrow()) - .unwrap() - .decimals(); + let decimals = Mint::load(&ctx.accounts.mint).unwrap().decimals(); // Denormalize transfer transfer_amount and relayer payouts based on this mint's decimals. When these // transfers were made outbound, the amounts were normalized, so it is safe to unwrap these @@ -189,14 +200,14 @@ fn complete_transfer_native(ctx: Context, _args: EmptyAr .unwrap(); // Save references to these accounts to be used later. - let token_program = &ctx.accounts.token_program; - let custody_token = &ctx.accounts.custody_token; - let custody_authority = &ctx.accounts.custody_authority; let recipient_token = &ctx.accounts.recipient_token; let payer_token = &ctx.accounts.payer_token; // Custody authority is who has the authority to transfer tokens from the custody account. - let custody_authority_bump = ctx.bumps["custody_authority"]; + let custody_authority_seeds = &[ + CUSTODY_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["custody_authority"]], + ]; // If there is a payout to the relayer and the relayer's token account differs from the transfer // recipient's, we have to make an extra transfer. @@ -205,23 +216,19 @@ fn complete_transfer_native(ctx: Context, _args: EmptyAr // total outbound transfer transfer_amount. transfer_amount -= relayer_payout; - withdraw_native_tokens( - token_program, - custody_token, + utils::cpi::transfer( + ctx.accounts, payer_token, - custody_authority, - custody_authority_bump, relayer_payout, + Some(&[custody_authority_seeds]), )?; } // Finally transfer remaining transfer_amount to recipient. - withdraw_native_tokens( - token_program, - custody_token, + utils::cpi::transfer( + ctx.accounts, recipient_token, - custody_authority, - custody_authority_bump, transfer_amount, + Some(&[custody_authority_seeds]), ) } diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/wrapped.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/wrapped.rs index cff96847bc..dac74b8e1b 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer/wrapped.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer/wrapped.rs @@ -1,15 +1,14 @@ use crate::{ constants::MINT_AUTHORITY_SEED_PREFIX, error::TokenBridgeError, - legacy::EmptyArgs, - processor::mint_wrapped_tokens, - state::{Claim, RegisteredEmitter, WrappedAsset}, + legacy::instruction::EmptyArgs, + state::{LegacyWrappedAsset, RegisteredEmitter}, + utils, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; @@ -21,29 +20,15 @@ pub struct CompleteTransferWrapped<'info> { /// CHECK: Token Bridge never needed this account for this instruction. _config: UncheckedAccount<'info>, - /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program - )] - posted_vaa: AccountInfo<'info>, + /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// This account is a foreign token Bridge and is created via the Register Chain governance /// decree. @@ -52,8 +37,8 @@ pub struct CompleteTransferWrapped<'info> { /// allows registering multiple emitter addresses for the same chain ID. These seeds are not /// checked via Anchor macro, but will be checked in the access control function instead. /// - /// See the `require_valid_token_bridge_posted_vaa` instruction handler for more details. - registered_emitter: Box>>, + /// See the `require_valid_token_bridge_vaa` instruction handler for more details. + registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, /// CHECK: Recipient token account. Because we verify the wrapped mint, we can depend on the /// Token Program to mint the right tokens to this account because it requires that this mint @@ -69,16 +54,21 @@ pub struct CompleteTransferWrapped<'info> { /// CHECK: Wrapped mint (i.e. minted by Token Bridge program). /// - /// NOTE: Instead of checking the seeds, we check that the mint authority is the Token Bridge's - /// in access control. + /// NOTE: Because this mint is guaranteed to have a Wrapped Asset account (since this account's + /// pubkey is a part of the Wrapped Asset's PDA address), we do not need to check that this + /// mint is one that the Token Bridge program has mint authority for. #[account(mut)] wrapped_mint: AccountInfo<'info>, + /// Wrapped asset account, which is deserialized as its legacy representation. The latest + /// version has an additional field (sequence number), which may not deserialize if wrapped + /// metadata were not attested again to realloc this account. So we must deserialize this as the + /// legacy representation. #[account( - seeds = [WrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], + seeds = [LegacyWrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], bump, )] - wrapped_asset: Box>>, + wrapped_asset: Account<'info, LegacyAnchorized<0, LegacyWrappedAsset>>, /// CHECK: This account is the authority that can burn and mint wrapped assets. #[account( @@ -99,31 +89,53 @@ pub struct CompleteTransferWrapped<'info> { recipient: Option>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, - token_program: Program<'info, token::Token>, + token_program: Program<'info, anchor_spl::token::Token>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for CompleteTransferWrapped<'info> +{ + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + +impl<'info> utils::cpi::MintTo<'info> for CompleteTransferWrapped<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.wrapped_mint.to_account_info() + } + + fn mint_authority(&self) -> AccountInfo<'info> { + self.mint_authority.to_account_info() + } } impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for CompleteTransferWrapped<'info> { - const LOG_IX_NAME: &'static str = "LegacCompleteTransferWrapped"; + const LOG_IX_NAME: &'static str = "LegacyCompleteTransferWrapped"; const ANCHOR_IX_FN: fn(Context, EmptyArgs) -> Result<()> = complete_transfer_wrapped; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_complete_transfer_account_infos(account_infos) + } } impl<'info> CompleteTransferWrapped<'info> { fn constraints(ctx: &Context) -> Result<()> { - crate::zero_copy::Mint::require_mint_authority( - &ctx.accounts.wrapped_mint.try_borrow_data()?, - Some(&ctx.accounts.mint_authority.key()), - )?; - - let vaa = &ctx.accounts.posted_vaa; - let vaa_key = vaa.key(); - let acc_data = vaa.try_borrow_data()?; - let transfer = super::validate_posted_token_transfer( - &vaa_key, - &acc_data, + let (token_chain, token_address) = super::validate_token_transfer_vaa( + &ctx.accounts.vaa, &ctx.accounts.registered_emitter, &ctx.accounts.recipient_token, &ctx.accounts.recipient, @@ -135,16 +147,15 @@ impl<'info> CompleteTransferWrapped<'info> { // chain ID != 1. But there may be accounts that exist where the chain ID == 1, so we do perform this check as a // precaution). require_neq!( - transfer.token_chain(), - SOLANA_CHAIN, + token_chain, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::NativeAsset ); // Wrapped asset account must agree with the encoded token info. let asset = &ctx.accounts.wrapped_asset; require!( - transfer.token_chain() == asset.token_chain - && transfer.token_address() == asset.token_address, + token_chain == asset.token_chain && token_address == asset.token_address, TokenBridgeError::InvalidMint ); @@ -158,13 +169,14 @@ fn complete_transfer_wrapped( ctx: Context, _args: EmptyArgs, ) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); - let msg = TokenBridgeMessage::parse(vaa.payload()).unwrap(); + let msg = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()).unwrap(); let transfer = msg.transfer().unwrap(); // We do not have to denormalize wrapped mint amounts because by definition wrapped mints can @@ -177,14 +189,11 @@ fn complete_transfer_wrapped( let relayer_payout = transfer.encoded_relayer_fee().0.try_into().unwrap(); // Save references to the token accounts to be used later. - let token_program = &ctx.accounts.token_program; - let wrapped_mint = &ctx.accounts.wrapped_mint; - let mint_authority = &ctx.accounts.mint_authority; let recipient_token = &ctx.accounts.recipient_token; let payer_token = &ctx.accounts.payer_token; // Mint authority is who has the authority to mint. - let mint_authority_bump = ctx.bumps["mint_authority"]; + let mint_authority_seeds = &[MINT_AUTHORITY_SEED_PREFIX, &[ctx.bumps["mint_authority"]]]; // If there is a payout to the relayer and the relayer's token account differs from the transfer // recipient's, we have to make an extra mint. @@ -193,23 +202,19 @@ fn complete_transfer_wrapped( // total outbound transfer amount. mint_amount -= relayer_payout; - mint_wrapped_tokens( - token_program, - wrapped_mint, + utils::cpi::mint_to( + ctx.accounts, payer_token, - mint_authority, - mint_authority_bump, relayer_payout, + Some(&[mint_authority_seeds]), )?; } // If there is any amount left after the relayer payout, finally mint remaining. - mint_wrapped_tokens( - token_program, - wrapped_mint, + utils::cpi::mint_to( + ctx.accounts, recipient_token, - mint_authority, - mint_authority_bump, mint_amount, + Some(&[mint_authority_seeds]), ) } diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/mod.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/mod.rs index c448ea3550..8fdf785b3c 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/mod.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/mod.rs @@ -7,39 +7,38 @@ pub use wrapped::*; use crate::{error::TokenBridgeError, legacy::state::RegisteredEmitter}; use anchor_lang::prelude::*; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; -use wormhole_raw_vaas::token_bridge::{TokenBridgeMessage, TransferWithMessage}; +use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; -pub fn validate_posted_token_transfer_with_payload<'ctx>( - vaa_acc_key: &'ctx Pubkey, - vaa_acc_data: &'ctx [u8], - registered_emitter: &'ctx Account<'_, LegacyAnchorized<0, RegisteredEmitter>>, - redeemer_authority: &'ctx Signer<'_>, - dst_token: &'ctx AccountInfo<'_>, -) -> Result> { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - let msg = - crate::utils::require_valid_posted_token_bridge_vaa(vaa_acc_key, &vaa, registered_emitter)?; +pub fn validate_token_transfer_with_payload_vaa( + vaa_acc_info: &AccountInfo, + registered_emitter: &Account>, + redeemer_authority: &Signer, + dst_token: &AccountInfo, +) -> Result<(u16, [u8; 32])> { + let vaa_key = vaa_acc_info.key(); + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let msg = crate::utils::require_valid_token_bridge_vaa(&vaa_key, &vaa, registered_emitter)?; - let transfer = match msg { - TokenBridgeMessage::TransferWithMessage(inner) => inner, - _ => return err!(TokenBridgeError::InvalidTokenBridgeVaa), + let transfer = if let TokenBridgeMessage::TransferWithMessage(inner) = msg { + inner + } else { + return err!(TokenBridgeError::InvalidTokenBridgeVaa); }; // This token bridge transfer must be intended to be redeemed on Solana. require_eq!( transfer.redeemer_chain(), - SOLANA_CHAIN, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::RedeemerChainNotSolana ); // The encoded transfer recipient can either be the signer of this instruction or a // program whose signer is a PDA using the seeds [b"redeemer"] (and the encoded redeemer - // is the program ID). If the latter, the transfer redeemer can be any PDA that signs + // is the program ID). If the former, the transfer redeemer can be any PDA that signs // for this instruction. - // - // NOTE: Requiring that the transfer redeemer be a signer is a patch. let redeemer = Pubkey::from(transfer.redeemer()); let redeemer_authority = redeemer_authority.key(); if redeemer != redeemer_authority { @@ -56,13 +55,68 @@ pub fn validate_posted_token_transfer_with_payload<'ctx>( // The redeemer must be the token account owner if the redeemer authority is the // same as the redeemer (i.e. the signer of this transaction, which does not // represent a program's PDA. - require_keys_eq!( - redeemer, - crate::zero_copy::TokenAccount::parse(&dst_token.try_borrow_data()?)?.owner(), - ErrorCode::ConstraintTokenOwner - ); + let token = crate::zero_copy::TokenAccount::load(dst_token)?; + require_keys_eq!(redeemer, token.owner(), ErrorCode::ConstraintTokenOwner); + } + + // Done. + Ok((transfer.token_chain(), transfer.token_address())) +} + +/// The Anchor context orders the accounts as: +/// +/// 1. `payer` +/// 2. `_config` +/// 3. `vaa` +/// 4. `claim` +/// 5. `registered_emitter` +/// 6. `dst_token` +/// 7. `redeemer_authority` +/// 8. `_relayer_fee_token` +/// 9. `custody_token` OR `wrapped_mint` +/// 10. `mint` OR `wrapped_asset` +/// 11. `custody_authority` OR `mint_aurhority` +/// 12. `_rent` <-- order unspecified +/// 13. `system_program` <-- order unspecified +/// 14. `token_program` <-- order unspecified +/// +/// Because the legacy implementation did not require specifying where the Rent sysvar, System +/// program and SPL token program should be, we ensure that these accounts are 12, 13 and 14 +/// respectively because the Anchor account context requires them to be in these positions. +pub fn order_complete_transfer_with_payload_account_infos<'info>( + account_infos: &[AccountInfo<'info>], +) -> Result>> { + const NUM_ACCOUNTS: usize = 14; + const TOKEN_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + const SYSTEM_PROGRAM_IDX: usize = TOKEN_PROGRAM_IDX - 1; + + let mut infos = account_infos.to_vec(); + + // This check is inclusive because System program and Token program can be in any order. + if infos.len() >= NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + + // Token program needs to exist in these account infos. + let token_program_idx = infos + .iter() + .position(|info| info.key() == anchor_spl::token::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if token_program_idx != TOKEN_PROGRAM_IDX { + infos.swap(TOKEN_PROGRAM_IDX, token_program_idx); + } } // Done. - Ok(transfer) + Ok(infos) } diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/native.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/native.rs index d66239b056..e2e0c6dbdc 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/native.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/native.rs @@ -1,16 +1,11 @@ use crate::{ - constants::CUSTODY_AUTHORITY_SEED_PREFIX, - error::TokenBridgeError, - legacy::EmptyArgs, - processor::withdraw_native_tokens, - state::{Claim, RegisteredEmitter}, - zero_copy::Mint, + constants::CUSTODY_AUTHORITY_SEED_PREFIX, error::TokenBridgeError, + legacy::instruction::EmptyArgs, state::RegisteredEmitter, utils, zero_copy::Mint, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; @@ -22,29 +17,15 @@ pub struct CompleteTransferWithPayloadNative<'info> { /// CHECK: Token Bridge never needed this account for this instruction. _config: UncheckedAccount<'info>, - /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program - )] - posted_vaa: AccountInfo<'info>, + /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// This account is a foreign token Bridge and is created via the Register Chain governance /// decree. @@ -53,7 +34,7 @@ pub struct CompleteTransferWithPayloadNative<'info> { /// allows registering multiple emitter addresses for the same chain ID. These seeds are not /// checked via Anchor macro, but will be checked in the access control function instead. /// - /// See the `require_valid_token_bridge_posted_vaa` instruction handler for more details. + /// See the `require_valid_token_bridge_vaa` instruction handler for more details. registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, /// CHECK: Destination token account. Because we check the mint of the custody token account, we @@ -93,31 +74,62 @@ pub struct CompleteTransferWithPayloadNative<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, - token_program: Program<'info, token::Token>, + token_program: Program<'info, anchor_spl::token::Token>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for CompleteTransferWithPayloadNative<'info> +{ + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + +impl<'info> utils::cpi::Transfer<'info> for CompleteTransferWithPayloadNative<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn from(&self) -> Option> { + Some(self.custody_token.to_account_info()) + } + + fn authority(&self) -> Option> { + Some(self.custody_authority.to_account_info()) + } } impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for CompleteTransferWithPayloadNative<'info> { - const LOG_IX_NAME: &'static str = "LegacCompleteTransferWithPayloadNative"; + const LOG_IX_NAME: &'static str = "LegacyCompleteTransferWithPayloadNative"; const ANCHOR_IX_FN: fn(Context, EmptyArgs) -> Result<()> = complete_transfer_with_payload_native; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_complete_transfer_with_payload_account_infos(account_infos) + } } impl<'info> CompleteTransferWithPayloadNative<'info> { fn constraints(ctx: &Context) -> Result<()> { // Make sure the mint authority is not the Token Bridge's. If it is, then this mint // originated from a foreign network. - crate::utils::require_native_mint(&ctx.accounts.mint)?; - - let vaa = &ctx.accounts.posted_vaa; - let vaa_key = vaa.key(); - let acc_data = vaa.try_borrow_data()?; - let transfer = super::validate_posted_token_transfer_with_payload( - &vaa_key, - &acc_data, + let mint = Mint::load(&ctx.accounts.mint)?; + require!( + !crate::utils::is_wrapped_mint(&mint), + TokenBridgeError::WrappedAsset + ); + + let (token_chain, token_address) = super::validate_token_transfer_with_payload_vaa( + &ctx.accounts.vaa, &ctx.accounts.registered_emitter, &ctx.accounts.redeemer_authority, &ctx.accounts.dst_token, @@ -125,15 +137,15 @@ impl<'info> CompleteTransferWithPayloadNative<'info> { // For native transfers, this mint must have been created on Solana. require_eq!( - transfer.token_chain(), - SOLANA_CHAIN, + token_chain, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::WrappedAsset ); // Mint account must agree with the encoded token address. - require_eq!( + require_keys_eq!( ctx.accounts.mint.key(), - Pubkey::from(transfer.token_address()), + Pubkey::from(token_address), TokenBridgeError::InvalidMint ); @@ -147,34 +159,32 @@ fn complete_transfer_with_payload_native( ctx: Context, _args: EmptyArgs, ) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; // Denormalize transfer amount based on this mint's decimals. When these transfers were made // outbound, the amounts were normalized, so it is safe to unwrap these operations. - let decimals = Mint::parse(&ctx.accounts.mint.data.borrow()) - .unwrap() - .decimals(); - let transfer_amount = TokenBridgeMessage::parse(vaa.payload()) + let transfer_amount = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()) .unwrap() .transfer_with_message() .unwrap() .encoded_amount() - .denorm(decimals) + .denorm(Mint::load(&ctx.accounts.mint).unwrap().decimals()) .try_into() .expect("Solana token amounts are u64"); // Finally transfer encoded amount. - withdraw_native_tokens( - &ctx.accounts.token_program, - &ctx.accounts.custody_token, + utils::cpi::transfer( + ctx.accounts, &ctx.accounts.dst_token, - &ctx.accounts.custody_authority, - ctx.bumps["custody_authority"], transfer_amount, + Some(&[&[ + CUSTODY_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["custody_authority"]], + ]]), ) } diff --git a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/wrapped.rs b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/wrapped.rs index 13186436cc..a62d4b0e92 100644 --- a/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/wrapped.rs +++ b/solana/programs/token-bridge/src/legacy/processor/complete_transfer_with_payload/wrapped.rs @@ -1,15 +1,14 @@ use crate::{ constants::MINT_AUTHORITY_SEED_PREFIX, error::TokenBridgeError, - legacy::EmptyArgs, - processor::mint_wrapped_tokens, - state::{Claim, RegisteredEmitter, WrappedAsset}, + legacy::instruction::EmptyArgs, + state::{LegacyWrappedAsset, RegisteredEmitter}, + utils, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; @@ -21,29 +20,15 @@ pub struct CompleteTransferWithPayloadWrapped<'info> { /// CHECK: Token Bridge never needed this account for this instruction. _config: UncheckedAccount<'info>, - /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program - )] - posted_vaa: AccountInfo<'info>, + /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// This account is a foreign token Bridge and is created via the Register Chain governance /// decree. @@ -52,8 +37,8 @@ pub struct CompleteTransferWithPayloadWrapped<'info> { /// allows registering multiple emitter addresses for the same chain ID. These seeds are not /// checked via Anchor macro, but will be checked in the access control function instead. /// - /// See the `require_valid_token_bridge_posted_vaa` instruction handler for more details. - registered_emitter: Box>>, + /// See the `require_valid_token_bridge_vaa` instruction handler for more details. + registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, /// CHECK: Destination token account. Because we verify the wrapped mint, we can depend on the /// Token Program to mint the right tokens to this account because it requires that this mint @@ -68,16 +53,21 @@ pub struct CompleteTransferWithPayloadWrapped<'info> { /// CHECK: Wrapped mint (i.e. minted by Token Bridge program). /// - /// NOTE: Instead of checking the seeds, we check that the mint authority is the Token Bridge's - /// in access control. + /// NOTE: Because this mint is guaranteed to have a Wrapped Asset account (since this account's + /// pubkey is a part of the Wrapped Asset's PDA address), we do not need to check that this + /// mint is one that the Token Bridge program has mint authority for. #[account(mut)] wrapped_mint: AccountInfo<'info>, + /// Wrapped asset account, which is deserialized as its legacy representation. The latest + /// version has an additional field (sequence number), which may not deserialize if wrapped + /// metadata were not attested again to realloc this account. So we must deserialize this as the + /// legacy representation. #[account( - seeds = [WrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], + seeds = [LegacyWrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], bump, )] - wrapped_asset: Box>>, + wrapped_asset: Account<'info, LegacyAnchorized<0, LegacyWrappedAsset>>, /// CHECK: This account is the authority that can burn and mint wrapped assets. #[account( @@ -90,32 +80,54 @@ pub struct CompleteTransferWithPayloadWrapped<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, - token_program: Program<'info, token::Token>, + token_program: Program<'info, anchor_spl::token::Token>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for CompleteTransferWithPayloadWrapped<'info> +{ + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + +impl<'info> utils::cpi::MintTo<'info> for CompleteTransferWithPayloadWrapped<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } + + fn mint(&self) -> AccountInfo<'info> { + self.wrapped_mint.to_account_info() + } + + fn mint_authority(&self) -> AccountInfo<'info> { + self.mint_authority.to_account_info() + } } impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for CompleteTransferWithPayloadWrapped<'info> { - const LOG_IX_NAME: &'static str = "LegacCompleteTransferWithPayloadWrapped"; + const LOG_IX_NAME: &'static str = "LegacyCompleteTransferWithPayloadWrapped"; const ANCHOR_IX_FN: fn(Context, EmptyArgs) -> Result<()> = complete_transfer_with_payload_wrapped; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_complete_transfer_with_payload_account_infos(account_infos) + } } impl<'info> CompleteTransferWithPayloadWrapped<'info> { fn constraints(ctx: &Context) -> Result<()> { - crate::zero_copy::Mint::require_mint_authority( - &ctx.accounts.wrapped_mint.try_borrow_data()?, - Some(&ctx.accounts.mint_authority.key()), - )?; - - let vaa = &ctx.accounts.posted_vaa; - let vaa_key = vaa.key(); - let acc_data = vaa.try_borrow_data()?; - let transfer = super::validate_posted_token_transfer_with_payload( - &vaa_key, - &acc_data, + let (token_chain, token_address) = super::validate_token_transfer_with_payload_vaa( + &ctx.accounts.vaa, &ctx.accounts.registered_emitter, &ctx.accounts.redeemer_authority, &ctx.accounts.dst_token, @@ -123,16 +135,15 @@ impl<'info> CompleteTransferWithPayloadWrapped<'info> { // For wrapped transfers, this token must have originated from another network. require_neq!( - transfer.token_chain(), - SOLANA_CHAIN, + token_chain, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::NativeAsset ); // Wrapped asset account must agree with the encoded token info. let asset = &ctx.accounts.wrapped_asset; require!( - transfer.token_chain() == asset.token_chain - && transfer.token_address() == asset.token_address, + token_chain == asset.token_chain && token_address == asset.token_address, TokenBridgeError::InvalidMint ); @@ -146,15 +157,15 @@ fn complete_transfer_with_payload_wrapped( ctx: Context, _args: EmptyArgs, ) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; // Take transfer amount as-is. - let mint_amount = TokenBridgeMessage::parse(vaa.payload()) + let mint_amount = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()) .unwrap() .transfer_with_message() .unwrap() @@ -164,12 +175,10 @@ fn complete_transfer_with_payload_wrapped( .map_err(|_| TokenBridgeError::U64Overflow)?; // Finally transfer encoded amount by minting to the redeemer's token account. - mint_wrapped_tokens( - &ctx.accounts.token_program, - &ctx.accounts.wrapped_mint, + utils::cpi::mint_to( + ctx.accounts, &ctx.accounts.dst_token, - &ctx.accounts.mint_authority, - ctx.bumps["mint_authority"], mint_amount, + Some(&[&[MINT_AUTHORITY_SEED_PREFIX, &[ctx.bumps["mint_authority"]]]]), ) } diff --git a/solana/programs/token-bridge/src/legacy/processor/create_or_update_wrapped.rs b/solana/programs/token-bridge/src/legacy/processor/create_or_update_wrapped.rs index 1757cbcd58..4edaa58f27 100644 --- a/solana/programs/token-bridge/src/legacy/processor/create_or_update_wrapped.rs +++ b/solana/programs/token-bridge/src/legacy/processor/create_or_update_wrapped.rs @@ -1,14 +1,14 @@ use crate::{ constants::{MAX_DECIMALS, MINT_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, error::TokenBridgeError, - legacy::EmptyArgs, - state::{Claim, RegisteredEmitter, WrappedAsset}, + legacy::instruction::EmptyArgs, + state::{LegacyWrappedAsset, RegisteredEmitter, WrappedAsset}, }; -use anchor_lang::prelude::*; +use anchor_lang::{prelude::*, system_program}; use anchor_spl::{metadata, token}; use core_bridge_program::{ - self, constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use mpl_token_metadata::state::DataV2; use wormhole_raw_vaas::token_bridge::{Attestation, TokenBridgeMessage}; @@ -28,32 +28,18 @@ pub struct CreateOrUpdateWrapped<'info> { /// allows registering multiple emitter addresses for the same chain ID. These seeds are not /// checked via Anchor macro, but will be checked in the access control function instead. /// - /// See the `require_valid_token_bridge_posted_vaa` instruction handler for more details. - registered_emitter: Box>>, + /// See the `require_valid_token_bridge_vaa` instruction handler for more details. + registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, - /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program - )] - posted_vaa: AccountInfo<'info>, + /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// CHECK: To avoid multiple borrows to the posted vaa account to generate seeds and other mint /// parameters, we perform these checks outside of this accounts context. The pubkey for this @@ -62,25 +48,33 @@ pub struct CreateOrUpdateWrapped<'info> { #[account( init_if_needed, payer = payer, - mint::decimals = try_attestation_decimals(&posted_vaa.try_borrow_data()?)?, + mint::decimals = try_attestation_decimals(&vaa)?, mint::authority = mint_authority, seeds = [ WRAPPED_MINT_SEED_PREFIX, - &try_attestation_token_chain(&posted_vaa.try_borrow_data()?)?.to_be_bytes(), - try_attestation_token_address(&posted_vaa.try_borrow_data()?)?.as_ref(), + try_attestation_token_chain_bytes(&vaa)?.as_ref(), + try_attestation_token_address(&vaa)?.as_ref(), ], bump, )] wrapped_mint: Box>, + /// CHECK: Wrapped asset. This account will either be created if it does not exist or its size + /// be reallocated in case this account if this account uses the old schema. In the old schema, + /// there was no data reflecting the last VAA sequence number used, which can lead to metadata + /// being overwritten by a stale VAA. + /// + /// NOTE: Because this account needs special handling via realloc, we cannot use the + /// `init_if_needed` macro here. #[account( - init_if_needed, - payer = payer, - space = WrappedAsset::INIT_SPACE, - seeds = [WrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], + mut, + seeds = [ + WrappedAsset::SEED_PREFIX, + wrapped_mint.key().as_ref(), + ], bump, )] - wrapped_asset: Box>>, + wrapped_asset: AccountInfo<'info>, /// CHECK: This account is managed by the MPL Token Metadata program. We verify this PDA to /// ensure that we deserialize the correct metadata before creating or updating. @@ -106,15 +100,26 @@ pub struct CreateOrUpdateWrapped<'info> { )] mint_authority: AccountInfo<'info>, - /// CHECK: Rent is needed for the MPL Token Metadata program. - rent: UncheckedAccount<'info>, + /// CHECK: Previously needed sysvar. + _rent: UncheckedAccount<'info>, - core_bridge_program: Program<'info, CoreBridge>, system_program: Program<'info, System>, token_program: Program<'info, token::Token>, mpl_token_metadata_program: Program<'info, metadata::Metadata>, } +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for CreateOrUpdateWrapped<'info> +{ + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } +} + impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> for CreateOrUpdateWrapped<'info> { @@ -123,18 +128,18 @@ impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, const ANCHOR_IX_FN: fn(Context, EmptyArgs) -> Result<()> = create_or_update_wrapped; } -fn try_attestation_decimals(vaa_acc_data: &[u8]) -> Result { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - let msg = TokenBridgeMessage::parse(vaa.payload()) +fn try_attestation_decimals(vaa_acc_info: &AccountInfo) -> Result { + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let msg = TokenBridgeMessage::try_from(vaa.try_payload()?) .map_err(|_| TokenBridgeError::InvalidTokenBridgePayload)?; msg.attestation() .map(|attestation| cap_decimals(attestation.decimals())) .ok_or(error!(TokenBridgeError::InvalidTokenBridgeVaa)) } -fn try_attestation_token_chain(vaa_acc_data: &[u8]) -> Result { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - let msg = TokenBridgeMessage::parse(vaa.payload()) +fn try_attestation_token_chain_bytes(vaa_acc_info: &AccountInfo) -> Result<[u8; 2]> { + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let msg = TokenBridgeMessage::try_from(vaa.try_payload()?) .map_err(|_| TokenBridgeError::InvalidTokenBridgePayload)?; let token_chain = msg @@ -143,15 +148,19 @@ fn try_attestation_token_chain(vaa_acc_data: &[u8]) -> Result { .ok_or(error!(TokenBridgeError::InvalidTokenBridgeVaa))?; // This token must have originated from another network. - require_neq!(token_chain, SOLANA_CHAIN, TokenBridgeError::NativeAsset); + require_neq!( + token_chain, + core_bridge_sdk::SOLANA_CHAIN, + TokenBridgeError::NativeAsset + ); // Done. - Ok(token_chain) + Ok(token_chain.to_be_bytes()) } -fn try_attestation_token_address(vaa_acc_data: &[u8]) -> Result<[u8; 32]> { - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - let msg = TokenBridgeMessage::parse(vaa.payload()) +fn try_attestation_token_address(vaa_acc_info: &AccountInfo) -> Result<[u8; 32]> { + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let msg = TokenBridgeMessage::try_from(vaa.try_payload()?) .map_err(|_| TokenBridgeError::InvalidTokenBridgePayload)?; msg.attestation() .map(|attestation| attestation.token_address()) @@ -160,25 +169,23 @@ fn try_attestation_token_address(vaa_acc_data: &[u8]) -> Result<[u8; 32]> { impl<'info> CreateOrUpdateWrapped<'info> { fn constraints(ctx: &Context) -> Result<()> { - let vaa = &ctx.accounts.posted_vaa; + let vaa = &ctx.accounts.vaa; // NOTE: Other attestation validation is performed using the try_attestation_* methods, // which were used in the accounts context. - crate::utils::require_valid_posted_token_bridge_vaa( + crate::utils::require_valid_token_bridge_vaa( &vaa.key(), - &PostedVaaV1::parse(&vaa.data.borrow()).unwrap(), + &core_bridge_sdk::VaaAccount::load(vaa).unwrap(), &ctx.accounts.registered_emitter, - ) - .map(|_| ()) + )?; + + // Done. + Ok(()) } } #[access_control(CreateOrUpdateWrapped::constraints(&ctx))] fn create_or_update_wrapped(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; - // Check if token metadata has been created yet. If it isn't, we must create this account and // the wrapped asset account. if ctx.accounts.token_metadata.data_is_empty() { @@ -189,25 +196,48 @@ fn create_or_update_wrapped(ctx: Context, _args: EmptyArg } fn handle_create_wrapped(ctx: Context) -> Result<()> { - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); - let msg = TokenBridgeMessage::parse(vaa.payload()).unwrap(); + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; + + let msg = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()).unwrap(); let attestation = msg.attestation().unwrap(); - // Set wrapped asset data. - let wrapped_asset = &mut ctx.accounts.wrapped_asset; - wrapped_asset.set_inner( - WrappedAsset { + let (_, _, sequence) = vaa.try_emitter_info().unwrap(); + let wrapped_asset = WrappedAsset { + legacy: LegacyWrappedAsset { token_chain: attestation.token_chain(), token_address: attestation.token_address(), native_decimals: attestation.decimals(), - } - .into(), - ); + }, + last_updated_sequence: sequence, + }; // The wrapped asset account data will be encoded as JSON in the token metadata's URI. let uri = wrapped_asset.to_uri(); + // Create and set wrapped asset data. + { + core_bridge_sdk::cpi::system_program::create_account( + ctx.accounts, + &ctx.accounts.wrapped_asset, + WrappedAsset::INIT_SPACE, + &crate::ID, + Some(&[&[ + WrappedAsset::SEED_PREFIX, + ctx.accounts.wrapped_mint.key().as_ref(), + &[ctx.bumps["wrapped_asset"]], + ]]), + )?; + + let acc_data: &mut [_] = &mut ctx.accounts.wrapped_asset.data.borrow_mut(); + let mut writer = std::io::Cursor::new(acc_data); + LegacyAnchorized::from(wrapped_asset).try_serialize(&mut writer)?; + } + let FixedMeta { symbol, name } = fix_symbol_and_name(attestation); metadata::create_metadata_accounts_v3( @@ -220,7 +250,7 @@ fn handle_create_wrapped(ctx: Context) -> Result<()> { payer: ctx.accounts.payer.to_account_info(), update_authority: ctx.accounts.mint_authority.to_account_info(), system_program: ctx.accounts.system_program.to_account_info(), - rent: ctx.accounts.rent.to_account_info(), + rent: ctx.accounts.mpl_token_metadata_program.to_account_info(), // optional rent }, &[&[MINT_AUTHORITY_SEED_PREFIX, &[ctx.bumps["mint_authority"]]]], ), @@ -240,16 +270,77 @@ fn handle_create_wrapped(ctx: Context) -> Result<()> { } fn handle_update_wrapped(ctx: Context) -> Result<()> { - let acc_data = ctx.accounts.posted_vaa.data.borrow(); - let vaa = PostedVaaV1::parse(&acc_data).unwrap(); - let msg = TokenBridgeMessage::parse(vaa.payload()).unwrap(); + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; + + let msg = TokenBridgeMessage::try_from(vaa.try_payload().unwrap()).unwrap(); let attestation = msg.attestation().unwrap(); + // For wrapped assets created before this implementation, the wrapped asset schema did not + // include a VAA sequence number, which prevents metadata attestations to be redeemed out-of- + // order. For example, if a VAA with metadata name = "A" were never redeemed and then the name + // changed to "B", someone would have been able to redeem name = "B" and then overwrite the name + // with "A" by redeeming the old VAA. + // + // Here we need to check whether the wrapped asset is the old schema. If it is, we need to + // increase the size of the account by 8 bytes to account for the sequence. + if ctx.accounts.wrapped_asset.data_len() == WrappedAsset::INIT_SPACE - 8 { + let acc_info = &ctx.accounts.wrapped_asset; + + let lamports_diff = Rent::get().map(|rent| { + rent.minimum_balance(WrappedAsset::INIT_SPACE) + .saturating_sub(acc_info.lamports()) + })?; + + system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + system_program::Transfer { + from: ctx.accounts.payer.to_account_info(), + to: acc_info.to_account_info(), + }, + ), + lamports_diff, + )?; + + acc_info.realloc(WrappedAsset::INIT_SPACE, false)?; + } + + // Now check the sequence to see whether this VAA is stale. + let (_, _, updated_sequence) = vaa.try_emitter_info().unwrap(); + let wrapped_asset = { + let acc_data = ctx.accounts.wrapped_asset.data.borrow(); + let mut wrapped_asset = + LegacyAnchorized::<0, WrappedAsset>::try_deserialize(&mut acc_data.as_ref())?; + require!( + updated_sequence > wrapped_asset.last_updated_sequence, + TokenBridgeError::AttestationOutOfSequence + ); + + // Modify this wrapped asset to prepare it for writing. + wrapped_asset.last_updated_sequence = updated_sequence; + + wrapped_asset + }; + + // Update wrapped asset. + { + let acc_data: &mut [_] = &mut ctx.accounts.wrapped_asset.data.borrow_mut(); + let mut writer = std::io::Cursor::new(acc_data); + wrapped_asset.try_serialize(&mut writer)?; + } + // Deserialize token metadata so we can check whether the name or symbol have changed in // this asset metadata VAA. let data = { - let mut acc_data: &[u8] = &ctx.accounts.token_metadata.try_borrow_data()?; - metadata::MetadataAccount::try_deserialize(&mut acc_data).map(|acct| acct.data.clone())? + metadata::MetadataAccount::try_deserialize( + &mut ctx.accounts.token_metadata.data.borrow().as_ref(), + ) + .map(|meta| meta.data.clone())? }; let FixedMeta { symbol, name } = fix_symbol_and_name(attestation); diff --git a/solana/programs/token-bridge/src/legacy/processor/governance/mod.rs b/solana/programs/token-bridge/src/legacy/processor/governance/mod.rs index 331450d37a..7e218cd2f9 100644 --- a/solana/programs/token-bridge/src/legacy/processor/governance/mod.rs +++ b/solana/programs/token-bridge/src/legacy/processor/governance/mod.rs @@ -1,26 +1,2 @@ mod upgrade_contract; pub use upgrade_contract::*; - -use crate::error::TokenBridgeError; -use anchor_lang::prelude::*; -use core_bridge_program::{constants::SOLANA_CHAIN, zero_copy::PostedVaaV1}; -use wormhole_raw_vaas::token_bridge::{TokenBridgeDecree, TokenBridgeGovPayload}; - -pub fn require_valid_posted_governance_vaa<'ctx>( - vaa_key: &'ctx Pubkey, - vaa_acc_data: &'ctx [u8], -) -> Result> { - crate::utils::require_valid_posted_vaa_key(vaa_key)?; - - let vaa = PostedVaaV1::parse(vaa_acc_data)?; - - require!( - vaa.emitter_chain() == SOLANA_CHAIN - && vaa.emitter_address() == crate::constants::GOVERNANCE_EMITTER, - TokenBridgeError::InvalidGovernanceEmitter - ); - - TokenBridgeGovPayload::parse(vaa.payload()) - .map(|msg| msg.decree()) - .map_err(|_| error!(TokenBridgeError::InvalidGovernanceVaa)) -} diff --git a/solana/programs/token-bridge/src/legacy/processor/governance/upgrade_contract.rs b/solana/programs/token-bridge/src/legacy/processor/governance/upgrade_contract.rs index ad6f7be73d..33e369a258 100644 --- a/solana/programs/token-bridge/src/legacy/processor/governance/upgrade_contract.rs +++ b/solana/programs/token-bridge/src/legacy/processor/governance/upgrade_contract.rs @@ -1,12 +1,8 @@ use crate::{ constants::UPGRADE_SEED_PREFIX, error::TokenBridgeError, legacy::instruction::EmptyArgs, - state::Claim, }; use anchor_lang::prelude::*; -use core_bridge_program::{ - constants::SOLANA_CHAIN, legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, - zero_copy::PostedVaaV1, -}; +use core_bridge_program::sdk::{self as core_bridge_sdk, LoadZeroCopy}; use solana_program::{bpf_loader_upgradeable, program::invoke_signed}; #[derive(Accounts)] @@ -15,30 +11,14 @@ pub struct UpgradeContract<'info> { payer: Signer<'info>, /// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the - /// instruction handler. - #[account( - seeds = [ - PostedVaaV1::SEED_PREFIX, - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.message_hash().as_ref() - ], - bump, - seeds::program = core_bridge_program, - )] - posted_vaa: AccountInfo<'info>, + /// instruction handler, which also checks this account discriminator (so there is no need to + /// check PDA seeds here). + vaa: AccountInfo<'info>, - /// Account representing that a VAA has been consumed. - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_address().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.emitter_chain().to_be_bytes().as_ref(), - PostedVaaV1::parse(&posted_vaa.try_borrow_data()?)?.sequence().to_be_bytes().as_ref(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, /// CHECK: We need this upgrade authority to invoke the BPF Loader Upgradeable program to /// upgrade this program's executable. We verify this PDA address here out of convenience to get @@ -74,7 +54,16 @@ pub struct UpgradeContract<'info> { bpf_loader_upgradeable_program: AccountInfo<'info>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> for UpgradeContract<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } } impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, EmptyArgs> @@ -87,10 +76,10 @@ impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, impl<'info> UpgradeContract<'info> { fn constraints(ctx: &Context) -> Result<()> { - let vaa = &ctx.accounts.posted_vaa; - let vaa_key = vaa.key(); - let acc_data: &[u8] = &vaa.try_borrow_data()?; - let gov_payload = super::require_valid_posted_governance_vaa(&vaa_key, acc_data)?; + let vaa_acc_info = &ctx.accounts.vaa; + let vaa_key = vaa_acc_info.key(); + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let gov_payload = crate::processor::require_valid_governance_vaa(&vaa_key, &vaa)?; let decree = gov_payload .contract_upgrade() @@ -99,7 +88,7 @@ impl<'info> UpgradeContract<'info> { // Make sure that the contract upgrade is intended for this network. require_eq!( decree.chain(), - SOLANA_CHAIN, + core_bridge_sdk::SOLANA_CHAIN, TokenBridgeError::GovernanceForAnotherChain ); @@ -119,9 +108,12 @@ impl<'info> UpgradeContract<'info> { /// Loader Upgradeable program to upgrade this program's executable to the provided buffer. #[access_control(UpgradeContract::constraints(&ctx))] fn upgrade_contract(ctx: Context, _args: EmptyArgs) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; // Finally upgrade. invoke_signed( diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/mod.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/mod.rs index d7d8cc242a..470bf30796 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/mod.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/mod.rs @@ -3,3 +3,80 @@ pub use native::*; mod wrapped; pub use wrapped::*; + +use anchor_lang::prelude::*; + +/// The Anchor context orders the accounts as: +/// +/// 1. `payer` +/// 2. `_config` +/// 3. `src_token` +/// 4. `mint` OR `_src_owner` +/// 5. `custody_token` OR `wrapped_mint` +/// 6. `transfer_authority` OR `wrapped_asset` +/// 7. `custody_authority` OR `transfer_authority` +/// 8. `core_bridge_config` +/// 9. `core_message` +/// 10. `core_emitter` +/// 11. `core_emitter_sequence` +/// 12. `core_fee_collector` +/// 13. `_clock` +/// 14. `_rent` <-- order unspecified +/// 15. `system_program` <-- order unspecified +/// 16. `token_program` <-- order unspecified +/// 17. `core_bridge_program` <-- order unspecified +/// +/// Because the legacy implementation did not require specifying where the Rent sysvar, System +/// program, SPL token program and Core Bridge program should be, we ensure that these accounts are +/// 14, 15, 16 and 17 respectively because the Anchor account context requires them to be in these +/// positions. +pub(super) fn order_transfer_tokens_account_infos<'info>( + account_infos: &[AccountInfo<'info>], +) -> Result>> { + const NUM_ACCOUNTS: usize = 17; + const CORE_BRIDGE_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + const TOKEN_PROGRAM_IDX: usize = CORE_BRIDGE_PROGRAM_IDX - 1; + const SYSTEM_PROGRAM_IDX: usize = TOKEN_PROGRAM_IDX - 1; + + let mut infos = account_infos.to_vec(); + + // This check is inclusive because Core Bridge program, System program and Token program can + // be in any order. + if infos.len() >= NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + + // Token program needs to exist in these account infos. + let token_program_idx = infos + .iter() + .position(|info| info.key() == anchor_spl::token::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if token_program_idx != TOKEN_PROGRAM_IDX { + infos.swap(TOKEN_PROGRAM_IDX, token_program_idx); + } + + // Core Bridge program needs to exist in these account infos. + let core_bridge_program_idx = infos + .iter() + .position(|info| info.key() == core_bridge_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if core_bridge_program_idx != CORE_BRIDGE_PROGRAM_IDX { + infos.swap(CORE_BRIDGE_PROGRAM_IDX, core_bridge_program_idx); + } + } + + // Done. + Ok(infos) +} diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/native.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/native.rs index 40c146d5ba..569dbfd0f8 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/native.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/native.rs @@ -1,15 +1,12 @@ use crate::{ - constants::{ - CUSTODY_AUTHORITY_SEED_PREFIX, EMITTER_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX, - }, + constants::{CUSTODY_AUTHORITY_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX}, error::TokenBridgeError, - legacy::TransferTokensArgs, - processor::{deposit_native_tokens, post_token_bridge_message}, + legacy::instruction::TransferTokensArgs, + utils::{self, TruncateAmount}, zero_copy::Mint, }; use anchor_lang::prelude::*; -use anchor_spl::token; -use core_bridge_program::sdk as core_bridge_sdk; +use core_bridge_program::sdk::{self as core_bridge_sdk, LoadZeroCopy}; use ruint::aliases::U256; use wormhole_raw_vaas::support::EncodedAmount; @@ -39,7 +36,7 @@ pub struct TransferTokensNative<'info> { seeds = [mint.key().as_ref()], bump, )] - custody_token: Box>, + custody_token: Account<'info, anchor_spl::token::TokenAccount>, /// CHECK: This authority is whom the source token account owner delegates spending approval for /// transferring native assets or burning wrapped assets. @@ -65,10 +62,7 @@ pub struct TransferTokensNative<'info> { core_message: Signer<'info>, /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] + /// This PDA address is checked in `post_token_bridge_message`. core_emitter: AccountInfo<'info>, /// CHECK: This account is needed for the Core Bridge program. @@ -86,26 +80,27 @@ pub struct TransferTokensNative<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, + token_program: Program<'info, anchor_spl::token::Token>, core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, - token_program: Program<'info, token::Token>, } -impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, TransferTokensArgs> - for TransferTokensNative<'info> -{ - const LOG_IX_NAME: &'static str = "LegacyTransferTokensNative"; +impl<'info> utils::cpi::Transfer<'info> for TransferTokensNative<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } - const ANCHOR_IX_FN: fn(Context, TransferTokensArgs) -> Result<()> = - transfer_tokens_native; -} + fn from(&self) -> Option> { + Some(self.src_token.to_account_info()) + } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for TransferTokensNative<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() + fn authority(&self) -> Option> { + Some(self.transfer_authority.to_account_info()) } } -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensNative<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for TransferTokensNative<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -116,16 +111,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensNative< } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensNative<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -139,16 +134,34 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensNative } } +impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, TransferTokensArgs> + for TransferTokensNative<'info> +{ + const LOG_IX_NAME: &'static str = "LegacyTransferTokensNative"; + + const ANCHOR_IX_FN: fn(Context, TransferTokensArgs) -> Result<()> = + transfer_tokens_native; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_transfer_tokens_account_infos(account_infos) + } +} + impl<'info> TransferTokensNative<'info> { fn constraints(ctx: &Context, args: &TransferTokensArgs) -> Result<()> { // Make sure the mint authority is not the Token Bridge's. If it is, then this mint // originated from a foreign network. - crate::utils::require_native_mint(&ctx.accounts.mint)?; + let mint = Mint::load(&ctx.accounts.mint)?; + require!( + !crate::utils::is_wrapped_mint(&mint), + TokenBridgeError::WrappedAsset + ); // Cannot configure a fee greater than the total transfer amount. - require_gte!( - args.amount, - args.relayer_fee, + require!( + args.relayer_fee <= args.amount, TokenBridgeError::InvalidRelayerFee ); @@ -170,26 +183,25 @@ fn transfer_tokens_native( recipient_chain, } = args; + let mint = Mint::load(&ctx.accounts.mint).unwrap(); + // Deposit native assets from the sender's account into the custody account. - let amount = deposit_native_tokens( - &ctx.accounts.token_program, - &ctx.accounts.mint, - &ctx.accounts.src_token, - &ctx.accounts.custody_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], - amount, + utils::cpi::transfer( + ctx.accounts, + ctx.accounts.custody_token.as_ref(), + mint.truncate_amount(amount), + Some(&[&[ + TRANSFER_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["transfer_authority"]], + ]]), )?; // Prepare Wormhole message. We need to normalize these amounts because we are working with // native assets. - let mint = &ctx.accounts.mint; - let token_address = mint.key().to_bytes(); - - let decimals = Mint::parse(&mint.data.borrow()).unwrap().decimals(); + let decimals = mint.decimals(); let token_transfer = crate::messages::Transfer { norm_amount: EncodedAmount::norm(U256::from(amount), decimals).0, - token_address, + token_address: ctx.accounts.mint.key().to_bytes(), token_chain: core_bridge_sdk::SOLANA_CHAIN, recipient, recipient_chain, @@ -197,9 +209,9 @@ fn transfer_tokens_native( }; // Finally publish Wormhole message using the Core Bridge. - post_token_bridge_message( + utils::cpi::post_token_bridge_message( ctx.accounts, - ctx.bumps["core_emitter"], + &ctx.accounts.core_message, nonce, token_transfer, ) diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/wrapped.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/wrapped.rs index 4424cc2de1..feaca6acde 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/wrapped.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens/wrapped.rs @@ -1,12 +1,11 @@ use crate::{ - constants::{EMITTER_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, + constants::{TRANSFER_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, error::TokenBridgeError, - legacy::TransferTokensArgs, - processor::{burn_wrapped_tokens, post_token_bridge_message}, - state::WrappedAsset, + legacy::instruction::TransferTokensArgs, + state::LegacyWrappedAsset, + utils, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{legacy::utils::LegacyAnchorized, sdk as core_bridge_sdk}; use ruint::aliases::U256; @@ -39,11 +38,15 @@ pub struct TransferTokensWrapped<'info> { )] wrapped_mint: AccountInfo<'info>, + /// Wrapped asset account, which is deserialized as its legacy representation. The latest + /// version has an additional field (sequence number), which may not deserialize if wrapped + /// metadata were not attested again to realloc this account. So we must deserialize this as the + /// legacy representation. #[account( - seeds = [WrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], - bump + seeds = [LegacyWrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], + bump, )] - wrapped_asset: Box>>, + wrapped_asset: Account<'info, LegacyAnchorized<0, LegacyWrappedAsset>>, /// CHECK: This authority is whom the source token account owner delegates spending approval for /// transferring native assets or burning wrapped assets. @@ -62,10 +65,7 @@ pub struct TransferTokensWrapped<'info> { core_message: Signer<'info>, /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] + /// This PDA address is checked in `post_token_bridge_message`. core_emitter: AccountInfo<'info>, /// CHECK: This account is needed for the Core Bridge program. @@ -83,26 +83,31 @@ pub struct TransferTokensWrapped<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, + token_program: Program<'info, anchor_spl::token::Token>, core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, - token_program: Program<'info, token::Token>, } -impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, TransferTokensArgs> - for TransferTokensWrapped<'info> -{ - const LOG_IX_NAME: &'static str = "LegacyTransferTokensWrapped"; +impl<'info> utils::cpi::Burn<'info> for TransferTokensWrapped<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } - const ANCHOR_IX_FN: fn(Context, TransferTokensArgs) -> Result<()> = - transfer_tokens_wrapped; -} + fn mint(&self) -> AccountInfo<'info> { + self.wrapped_mint.to_account_info() + } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> for TransferTokensWrapped<'info> { - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() + fn from(&self) -> Option> { + Some(self.src_token.to_account_info()) + } + + fn authority(&self) -> Option> { + Some(self.transfer_authority.to_account_info()) } } -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWrapped<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for TransferTokensWrapped<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -113,16 +118,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWrapped } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensWrapped<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -136,12 +141,26 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensWrappe } } +impl<'info> core_bridge_program::legacy::utils::ProcessLegacyInstruction<'info, TransferTokensArgs> + for TransferTokensWrapped<'info> +{ + const LOG_IX_NAME: &'static str = "LegacyTransferTokensWrapped"; + + const ANCHOR_IX_FN: fn(Context, TransferTokensArgs) -> Result<()> = + transfer_tokens_wrapped; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_transfer_tokens_account_infos(account_infos) + } +} + impl<'info> TransferTokensWrapped<'info> { fn constraints(args: &TransferTokensArgs) -> Result<()> { // Cannot configure a fee greater than the total transfer amount. - require_gte!( - args.amount, - args.relayer_fee, + require!( + args.relayer_fee <= args.amount, TokenBridgeError::InvalidRelayerFee ); @@ -164,13 +183,13 @@ fn transfer_tokens_wrapped( } = args; // Burn wrapped assets from the sender's account. - burn_wrapped_tokens( - &ctx.accounts.token_program, - &ctx.accounts.wrapped_mint, - &ctx.accounts.src_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], + utils::cpi::burn( + ctx.accounts, amount, + Some(&[&[ + TRANSFER_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["transfer_authority"]], + ]]), )?; // Prepare Wormhole message. Amounts do not need to be normalized because we are working with @@ -186,9 +205,9 @@ fn transfer_tokens_wrapped( }; // Finally publish Wormhole message using the Core Bridge. - post_token_bridge_message( + utils::cpi::post_token_bridge_message( ctx.accounts, - ctx.bumps["core_emitter"], + &ctx.accounts.core_message, nonce, token_transfer, ) diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/mod.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/mod.rs index d7d8cc242a..471f6a0393 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/mod.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/mod.rs @@ -3,3 +3,81 @@ pub use native::*; mod wrapped; pub use wrapped::*; + +use anchor_lang::prelude::*; + +/// The Anchor context orders the accounts as: +/// +/// 1. `payer` +/// 2. `_config` +/// 3. `src_token` +/// 4. `mint` OR `_src_owner` +/// 5. `custody_token` OR `wrapped_mint` +/// 6. `transfer_authority` OR `wrapped_asset` +/// 7. `custody_authority` OR `transfer_authority` +/// 8. `core_bridge_config` +/// 9. `core_message` +/// 10. `core_emitter` +/// 11. `core_emitter_sequence` +/// 12. `core_fee_collector` +/// 13. `_clock` +/// 14. `sender_authority` +/// 15. `_rent` <-- order unspecified +/// 16. `system_program` <-- order unspecified +/// 17. `token_program` <-- order unspecified +/// 18. `core_bridge_program` <-- order unspecified +/// +/// Because the legacy implementation did not require specifying where the Rent sysvar, System +/// program, SPL token program and Core Bridge program should be, we ensure that these accounts are +/// 15, 16, 17 and 18 respectively because the Anchor account context requires them to be in these +/// positions. +pub(super) fn order_transfer_tokens_with_payload_account_infos<'info>( + account_infos: &[AccountInfo<'info>], +) -> Result>> { + const NUM_ACCOUNTS: usize = 18; + const CORE_BRIDGE_PROGRAM_IDX: usize = NUM_ACCOUNTS - 1; + const TOKEN_PROGRAM_IDX: usize = CORE_BRIDGE_PROGRAM_IDX - 1; + const SYSTEM_PROGRAM_IDX: usize = TOKEN_PROGRAM_IDX - 1; + + let mut infos = account_infos.to_vec(); + + // This check is inclusive because Core Bridge program, System program and Token program can + // be in any order. + if infos.len() >= NUM_ACCOUNTS { + // System program needs to exist in these account infos. + let system_program_idx = infos + .iter() + .position(|info| info.key() == anchor_lang::system_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure System program is in the right index. + if system_program_idx != SYSTEM_PROGRAM_IDX { + infos.swap(SYSTEM_PROGRAM_IDX, system_program_idx); + } + + // Token program needs to exist in these account infos. + let token_program_idx = infos + .iter() + .position(|info| info.key() == anchor_spl::token::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if token_program_idx != TOKEN_PROGRAM_IDX { + infos.swap(TOKEN_PROGRAM_IDX, token_program_idx); + } + + // Core Bridge program needs to exist in these account infos. + let core_bridge_program_idx = infos + .iter() + .position(|info| info.key() == core_bridge_program::ID) + .ok_or(error!(ErrorCode::InvalidProgramId))?; + + // Make sure Token program is in the right index. + if core_bridge_program_idx != CORE_BRIDGE_PROGRAM_IDX { + infos.swap(CORE_BRIDGE_PROGRAM_IDX, core_bridge_program_idx); + } + } + + // Done. + Ok(infos) +} diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/native.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/native.rs index 93f7ec2243..23f29477b4 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/native.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/native.rs @@ -1,14 +1,12 @@ use crate::{ - constants::{ - CUSTODY_AUTHORITY_SEED_PREFIX, EMITTER_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX, - }, - legacy::TransferTokensWithPayloadArgs, - processor::{deposit_native_tokens, post_token_bridge_message}, + constants::{CUSTODY_AUTHORITY_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX}, + error::TokenBridgeError, + legacy::instruction::TransferTokensWithPayloadArgs, + utils::{self, TruncateAmount}, zero_copy::Mint, }; use anchor_lang::prelude::*; -use anchor_spl::token; -use core_bridge_program::sdk as core_bridge_sdk; +use core_bridge_program::sdk::{self as core_bridge_sdk, LoadZeroCopy}; use ruint::aliases::U256; use wormhole_raw_vaas::support::EncodedAmount; @@ -38,7 +36,7 @@ pub struct TransferTokensWithPayloadNative<'info> { seeds = [mint.key().as_ref()], bump, )] - custody_token: Box>, + custody_token: Account<'info, anchor_spl::token::TokenAccount>, /// CHECK: This authority is whom the source token account owner delegates spending approval for /// transferring native assets or burning wrapped assets. @@ -64,10 +62,7 @@ pub struct TransferTokensWithPayloadNative<'info> { core_message: Signer<'info>, /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] + /// This PDA address is checked in `post_token_bridge_message`. core_emitter: AccountInfo<'info>, /// CHECK: This account is needed for the Core Bridge program. @@ -90,31 +85,27 @@ pub struct TransferTokensWithPayloadNative<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, + token_program: Program<'info, anchor_spl::token::Token>, core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, - token_program: Program<'info, token::Token>, } -impl<'info> - core_bridge_program::legacy::utils::ProcessLegacyInstruction< - 'info, - TransferTokensWithPayloadArgs, - > for TransferTokensWithPayloadNative<'info> -{ - const LOG_IX_NAME: &'static str = "LegacyTransferTokensWithPayloadNative"; +impl<'info> utils::cpi::Transfer<'info> for TransferTokensWithPayloadNative<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } - const ANCHOR_IX_FN: fn(Context, TransferTokensWithPayloadArgs) -> Result<()> = - transfer_tokens_with_payload_native; -} + fn from(&self) -> Option> { + Some(self.src_token.to_account_info()) + } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> - for TransferTokensWithPayloadNative<'info> -{ - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() + fn authority(&self) -> Option> { + Some(self.transfer_authority.to_account_info()) } } -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWithPayloadNative<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for TransferTokensWithPayloadNative<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -125,16 +116,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWithPay } impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensWithPayloadNative<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -148,11 +139,35 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensWithPa } } +impl<'info> + core_bridge_program::legacy::utils::ProcessLegacyInstruction< + 'info, + TransferTokensWithPayloadArgs, + > for TransferTokensWithPayloadNative<'info> +{ + const LOG_IX_NAME: &'static str = "LegacyTransferTokensWithPayloadNative"; + + const ANCHOR_IX_FN: fn(Context, TransferTokensWithPayloadArgs) -> Result<()> = + transfer_tokens_with_payload_native; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_transfer_tokens_with_payload_account_infos(account_infos) + } +} + impl<'info> TransferTokensWithPayloadNative<'info> { fn constraints(ctx: &Context) -> Result<()> { // Make sure the mint authority is not the Token Bridge's. If it is, then this mint // originated from a foreign network. - crate::utils::require_native_mint(&ctx.accounts.mint) + let mint = Mint::load(&ctx.accounts.mint)?; + require!( + !crate::utils::is_wrapped_mint(&mint), + TokenBridgeError::WrappedAsset + ); + + Ok(()) } } @@ -176,26 +191,24 @@ fn transfer_tokens_with_payload_native( // want to spend compute units to re-derive the authority if cpi_program_id is Some(pubkey). let sender = crate::utils::new_sender_address(&ctx.accounts.sender_authority, cpi_program_id)?; + let mint = Mint::load(&ctx.accounts.mint).unwrap(); + // Deposit native assets from the source token account into the custody account. - let amount = deposit_native_tokens( - &ctx.accounts.token_program, - &ctx.accounts.mint, - &ctx.accounts.src_token, - &ctx.accounts.custody_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], - amount, + utils::cpi::transfer( + ctx.accounts, + ctx.accounts.custody_token.as_ref(), + mint.truncate_amount(amount), + Some(&[&[ + TRANSFER_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["transfer_authority"]], + ]]), )?; // Prepare Wormhole message. We need to normalize these amounts because we are working with // native assets. - let mint = &ctx.accounts.mint; - let token_address = mint.key().to_bytes(); - - let decimals = Mint::parse(&mint.data.borrow()).unwrap().decimals(); let token_transfer = crate::messages::TransferWithMessage { - norm_amount: EncodedAmount::norm(U256::from(amount), decimals).0, - token_address, + norm_amount: EncodedAmount::norm(U256::from(amount), mint.decimals()).0, + token_address: ctx.accounts.mint.key().to_bytes(), token_chain: core_bridge_sdk::SOLANA_CHAIN, redeemer, redeemer_chain, @@ -204,9 +217,9 @@ fn transfer_tokens_with_payload_native( }; // Finally publish Wormhole message using the Core Bridge. - post_token_bridge_message( + utils::cpi::post_token_bridge_message( ctx.accounts, - ctx.bumps["core_emitter"], + &ctx.accounts.core_message, nonce, token_transfer, ) diff --git a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/wrapped.rs b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/wrapped.rs index 00bfc83d44..d7ff47a97b 100644 --- a/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/wrapped.rs +++ b/solana/programs/token-bridge/src/legacy/processor/transfer_tokens_with_payload/wrapped.rs @@ -1,11 +1,10 @@ use crate::{ - constants::{EMITTER_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, - legacy::TransferTokensWithPayloadArgs, - processor::{burn_wrapped_tokens, post_token_bridge_message}, - state::WrappedAsset, + constants::{TRANSFER_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, + legacy::instruction::TransferTokensWithPayloadArgs, + state::LegacyWrappedAsset, + utils, }; use anchor_lang::prelude::*; -use anchor_spl::token; use core_bridge_program::{legacy::utils::LegacyAnchorized, sdk as core_bridge_sdk}; use ruint::aliases::U256; @@ -38,11 +37,15 @@ pub struct TransferTokensWithPayloadWrapped<'info> { )] wrapped_mint: AccountInfo<'info>, + /// Wrapped asset account, which is deserialized as its legacy representation. The latest + /// version has an additional field (sequence number), which may not deserialize if wrapped + /// metadata were not attested again to realloc this account. So we must deserialize this as the + /// legacy representation. #[account( - seeds = [WrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], - bump + seeds = [LegacyWrappedAsset::SEED_PREFIX, wrapped_mint.key().as_ref()], + bump, )] - wrapped_asset: Box>>, + wrapped_asset: Account<'info, LegacyAnchorized<0, LegacyWrappedAsset>>, /// CHECK: This authority is whom the source token account owner delegates spending approval for /// transferring native assets or burning wrapped assets. @@ -61,10 +64,7 @@ pub struct TransferTokensWithPayloadWrapped<'info> { core_message: Signer<'info>, /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] + /// This PDA address is checked in `post_token_bridge_message`. core_emitter: AccountInfo<'info>, /// CHECK: This account is needed for the Core Bridge program. @@ -87,31 +87,31 @@ pub struct TransferTokensWithPayloadWrapped<'info> { _rent: UncheckedAccount<'info>, system_program: Program<'info, System>, + token_program: Program<'info, anchor_spl::token::Token>, core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, - token_program: Program<'info, token::Token>, } -impl<'info> - core_bridge_program::legacy::utils::ProcessLegacyInstruction< - 'info, - TransferTokensWithPayloadArgs, - > for TransferTokensWithPayloadWrapped<'info> -{ - const LOG_IX_NAME: &'static str = "LegacyTransferTokensWithPayloadWrapped"; +impl<'info> utils::cpi::Burn<'info> for TransferTokensWithPayloadWrapped<'info> { + fn token_program(&self) -> AccountInfo<'info> { + self.token_program.to_account_info() + } - const ANCHOR_IX_FN: fn(Context, TransferTokensWithPayloadArgs) -> Result<()> = - transfer_tokens_with_payload_wrapped; -} + fn mint(&self) -> AccountInfo<'info> { + self.wrapped_mint.to_account_info() + } -impl<'info> core_bridge_sdk::cpi::InvokeCoreBridge<'info> - for TransferTokensWithPayloadWrapped<'info> -{ - fn core_bridge_program(&self) -> AccountInfo<'info> { - self.core_bridge_program.to_account_info() + fn from(&self) -> Option> { + Some(self.src_token.to_account_info()) + } + + fn authority(&self) -> Option> { + Some(self.transfer_authority.to_account_info()) } } -impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWithPayloadWrapped<'info> { +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> + for TransferTokensWithPayloadWrapped<'info> +{ fn payer(&self) -> AccountInfo<'info> { self.payer.to_account_info() } @@ -124,16 +124,16 @@ impl<'info> core_bridge_sdk::cpi::CreateAccount<'info> for TransferTokensWithPay impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> for TransferTokensWithPayloadWrapped<'info> { - fn core_bridge_config(&self) -> AccountInfo<'info> { - self.core_bridge_config.to_account_info() + fn core_bridge_program(&self) -> AccountInfo<'info> { + self.core_bridge_program.to_account_info() } - fn core_message(&self) -> AccountInfo<'info> { - self.core_message.to_account_info() + fn core_bridge_config(&self) -> AccountInfo<'info> { + self.core_bridge_config.to_account_info() } - fn core_emitter(&self) -> Option> { - Some(self.core_emitter.to_account_info()) + fn core_emitter_authority(&self) -> AccountInfo<'info> { + self.core_emitter.to_account_info() } fn core_emitter_sequence(&self) -> AccountInfo<'info> { @@ -147,6 +147,24 @@ impl<'info> core_bridge_sdk::cpi::PublishMessage<'info> } } +impl<'info> + core_bridge_program::legacy::utils::ProcessLegacyInstruction< + 'info, + TransferTokensWithPayloadArgs, + > for TransferTokensWithPayloadWrapped<'info> +{ + const LOG_IX_NAME: &'static str = "LegacyTransferTokensWithPayloadWrapped"; + + const ANCHOR_IX_FN: fn(Context, TransferTokensWithPayloadArgs) -> Result<()> = + transfer_tokens_with_payload_wrapped; + + fn order_account_infos<'a>( + account_infos: &'a [AccountInfo<'info>], + ) -> Result>> { + super::order_transfer_tokens_with_payload_account_infos(account_infos) + } +} + fn transfer_tokens_with_payload_wrapped( ctx: Context, args: TransferTokensWithPayloadArgs, @@ -167,13 +185,13 @@ fn transfer_tokens_with_payload_wrapped( let sender = crate::utils::new_sender_address(&ctx.accounts.sender_authority, cpi_program_id)?; // Burn wrapped assets from the source token account. - burn_wrapped_tokens( - &ctx.accounts.token_program, - &ctx.accounts.wrapped_mint, - &ctx.accounts.src_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], + utils::cpi::burn( + ctx.accounts, amount, + Some(&[&[ + TRANSFER_AUTHORITY_SEED_PREFIX, + &[ctx.bumps["transfer_authority"]], + ]]), )?; // Prepare Wormhole message. Amounts do not need to be normalized because we are working with @@ -190,9 +208,9 @@ fn transfer_tokens_with_payload_wrapped( }; // Finally publish Wormhole message using the Core Bridge. - post_token_bridge_message( + utils::cpi::post_token_bridge_message( ctx.accounts, - ctx.bumps["core_emitter"], + &ctx.accounts.core_message, nonce, token_transfer, ) diff --git a/solana/programs/token-bridge/src/legacy/state/claim.rs b/solana/programs/token-bridge/src/legacy/state/claim.rs deleted file mode 100644 index 34678874f8..0000000000 --- a/solana/programs/token-bridge/src/legacy/state/claim.rs +++ /dev/null @@ -1,20 +0,0 @@ -use anchor_lang::prelude::*; - -/// NOTE: This account's PDA seeds are inconsistent with how other Core Bridges save consumed VAAs. -/// This account uses a tuple of (emitter_chain, emitter_address, sequence) whereas other Core -/// Bridge implementations use the message hash. -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] -pub struct Claim { - /// This member is not necessary, but we must preserve it since the legacy bridge assumes this - /// serialization for consumed VAAs (it is set to true when a VAA has been claimed). The fact - /// that this account exists at all should be enough to protect against a replay attack. - pub is_complete: bool, -} - -impl core_bridge_program::legacy::utils::LegacyAccount<0> for Claim { - const DISCRIMINATOR: [u8; 0] = []; - - fn program_id() -> Pubkey { - crate::ID - } -} diff --git a/solana/programs/token-bridge/src/legacy/state/mod.rs b/solana/programs/token-bridge/src/legacy/state/mod.rs index ae88fd4066..c45c6e6e0a 100644 --- a/solana/programs/token-bridge/src/legacy/state/mod.rs +++ b/solana/programs/token-bridge/src/legacy/state/mod.rs @@ -1,9 +1,8 @@ -mod claim; mod config; -mod registered_emitter; -mod wrapped_asset; - -pub use claim::*; pub use config::*; + +mod registered_emitter; pub use registered_emitter::*; + +mod wrapped_asset; pub use wrapped_asset::*; diff --git a/solana/programs/token-bridge/src/legacy/state/wrapped_asset.rs b/solana/programs/token-bridge/src/legacy/state/wrapped_asset.rs index 55f840135f..3458ded87f 100644 --- a/solana/programs/token-bridge/src/legacy/state/wrapped_asset.rs +++ b/solana/programs/token-bridge/src/legacy/state/wrapped_asset.rs @@ -10,13 +10,13 @@ struct MetadataUri { } #[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] -pub struct WrappedAsset { +pub struct LegacyWrappedAsset { pub token_chain: u16, pub token_address: [u8; 32], pub native_decimals: u8, } -impl core_bridge_program::legacy::utils::LegacyAccount<0> for WrappedAsset { +impl core_bridge_program::legacy::utils::LegacyAccount<0> for LegacyWrappedAsset { const DISCRIMINATOR: [u8; 0] = []; fn program_id() -> Pubkey { @@ -24,7 +24,7 @@ impl core_bridge_program::legacy::utils::LegacyAccount<0> for WrappedAsset { } } -impl WrappedAsset { +impl LegacyWrappedAsset { pub const SEED_PREFIX: &'static [u8] = b"meta"; pub fn to_uri(&self) -> String { @@ -42,6 +42,32 @@ impl WrappedAsset { } } +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)] +pub struct WrappedAsset { + pub legacy: LegacyWrappedAsset, + pub last_updated_sequence: u64, +} + +impl std::ops::Deref for WrappedAsset { + type Target = LegacyWrappedAsset; + + fn deref(&self) -> &Self::Target { + &self.legacy + } +} + +impl WrappedAsset { + pub const SEED_PREFIX: &'static [u8] = LegacyWrappedAsset::SEED_PREFIX; +} + +impl core_bridge_program::legacy::utils::LegacyAccount<0> for WrappedAsset { + const DISCRIMINATOR: [u8; 0] = LegacyWrappedAsset::DISCRIMINATOR; + + fn program_id() -> Pubkey { + crate::ID + } +} + #[cfg(test)] mod test { use super::*; @@ -49,12 +75,15 @@ mod test { #[test] fn to_uri() { let asset = WrappedAsset { - token_chain: 420, - token_address: [ - 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, - 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, - ], - native_decimals: 18, + legacy: LegacyWrappedAsset { + token_chain: 420, + token_address: [ + 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, + 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, 222, 173, 190, 239, + ], + native_decimals: 18, + }, + last_updated_sequence: 69, }; let expected = r#"{ diff --git a/solana/programs/token-bridge/src/lib.rs b/solana/programs/token-bridge/src/lib.rs index 026c23a628..5cb562f2cc 100644 --- a/solana/programs/token-bridge/src/lib.rs +++ b/solana/programs/token-bridge/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![allow(clippy::result_large_err)] use anchor_lang::prelude::*; @@ -23,20 +24,13 @@ pub(crate) mod messages; mod processor; pub(crate) use processor::*; -pub mod state; - -pub mod utils; +pub mod sdk; -pub mod zero_copy; +pub mod state; -#[derive(Clone)] -pub struct TokenBridge; +pub(crate) mod utils; -impl Id for TokenBridge { - fn id() -> Pubkey { - ID - } -} +pub(crate) mod zero_copy; #[program] pub mod wormhole_token_bridge_solana { @@ -46,8 +40,8 @@ pub mod wormhole_token_bridge_solana { /// legacy register chain instruction (which is now deprecated). This instruction handler /// creates two [RegisteredEmitter](crate::legacy::state::RegisteredEmitter) accounts: one with /// a PDA address derived using the old way of [emitter_chain, emitter_address] and the more - /// secure way of [emitter_chain]. By creating both of these accounts, we can consider migrating - /// to the newly derived account and closing the legacy account in the future. + /// secure way of \[emitter_chain\]. By creating both of these accounts, we can consider + /// migrating to the newly derived account and closing the legacy account in the future. pub fn register_chain(ctx: Context) -> Result<()> { processor::register_chain(ctx) } @@ -55,13 +49,10 @@ pub mod wormhole_token_bridge_solana { /// Processor for securing an existing (legacy) /// [RegisteredEmitter](crate::legacy::state::RegisteredEmitter) by creating a new /// [RegisteredEmitter](crate::legacy::state::RegisteredEmitter) account with a PDA address with - /// seeds [emitter_chain]. We can consider migrating to the newly derived account and closing + /// seeds \[emitter_chain\]. We can consider migrating to the newly derived account and closing /// the legacy account in the future. - pub fn secure_registered_emitter( - ctx: Context, - directive: SecureRegisteredEmitterDirective, - ) -> Result<()> { - processor::secure_registered_emitter(ctx, directive) + pub fn secure_registered_emitter(ctx: Context) -> Result<()> { + processor::secure_registered_emitter(ctx) } /// Process legacy Token Bridge instructions. See [legacy](crate::legacy) for more info. diff --git a/solana/programs/token-bridge/src/messages.rs b/solana/programs/token-bridge/src/messages.rs index 32ff821100..062e007779 100644 --- a/solana/programs/token-bridge/src/messages.rs +++ b/solana/programs/token-bridge/src/messages.rs @@ -1,3 +1,6 @@ +//! Messages relevant to the Token Bridge across all networks. These messages are serialized and +//! then published via the Core Bridge program. + use anchor_lang::prelude::Pubkey; use ruint::aliases::U256; use wormhole_io::Writeable; diff --git a/solana/programs/token-bridge/src/processor/governance/mod.rs b/solana/programs/token-bridge/src/processor/governance/mod.rs index c2be7bf90e..25f083b75c 100644 --- a/solana/programs/token-bridge/src/processor/governance/mod.rs +++ b/solana/programs/token-bridge/src/processor/governance/mod.rs @@ -6,21 +6,25 @@ pub use secure_registered_emitter::*; use crate::error::TokenBridgeError; use anchor_lang::prelude::*; -use core_bridge_program::{constants::SOLANA_CHAIN, zero_copy::EncodedVaa}; -use wormhole_raw_vaas::token_bridge::TokenBridgeGovPayload; +use core_bridge_program::sdk::{self as core_bridge_sdk}; +use wormhole_raw_vaas::token_bridge::{TokenBridgeDecree, TokenBridgeGovPayload}; -pub fn require_valid_governance_encoded_vaa( - vaa_acc_data: &[u8], -) -> Result> { - let vaa_body = EncodedVaa::try_v1(vaa_acc_data).map(|vaa| vaa.body())?; +pub fn require_valid_governance_vaa<'ctx>( + vaa_key: &'ctx Pubkey, + vaa: &'ctx core_bridge_sdk::VaaAccount<'ctx>, +) -> Result> { + crate::utils::require_valid_vaa_key(vaa_key)?; + + let (emitter_address, emitter_chain, _) = vaa.try_emitter_info()?; require!( - vaa_body.emitter_chain() == SOLANA_CHAIN - && vaa_body.emitter_address() == crate::constants::GOVERNANCE_EMITTER, + emitter_chain == crate::constants::GOVERNANCE_CHAIN + && emitter_address == crate::constants::GOVERNANCE_EMITTER, TokenBridgeError::InvalidGovernanceEmitter ); // Because emitter_chain and emitter_address getters have succeeded, we can safely unwrap this // payload call. - TokenBridgeGovPayload::try_from(vaa_body.payload()) + TokenBridgeGovPayload::try_from(vaa.try_payload().unwrap()) + .map(|msg| msg.decree()) .map_err(|_| error!(TokenBridgeError::InvalidGovernanceVaa)) } diff --git a/solana/programs/token-bridge/src/processor/governance/register_chain.rs b/solana/programs/token-bridge/src/processor/governance/register_chain.rs index 0cb4501fba..90a4acc7f7 100644 --- a/solana/programs/token-bridge/src/processor/governance/register_chain.rs +++ b/solana/programs/token-bridge/src/processor/governance/register_chain.rs @@ -1,10 +1,8 @@ -use crate::{ - error::TokenBridgeError, - state::{Claim, RegisteredEmitter}, -}; +use crate::{error::TokenBridgeError, state::RegisteredEmitter}; use anchor_lang::prelude::*; use core_bridge_program::{ - legacy::utils::LegacyAnchorized, sdk::cpi::CoreBridge, zero_copy::EncodedVaa, + legacy::utils::LegacyAnchorized, + sdk::{self as core_bridge_sdk, LoadZeroCopy}, }; use wormhole_raw_vaas::token_bridge::TokenBridgeGovPayload; @@ -14,30 +12,19 @@ pub struct RegisterChain<'info> { payer: Signer<'info>, /// CHECK: We will be performing zero-copy deserialization in the instruction handler. - #[account( - mut, - owner = core_bridge_program::ID - )] + #[account(mut)] vaa: AccountInfo<'info>, - #[account( - init, - payer = payer, - space = Claim::INIT_SPACE, - seeds = [ - EncodedVaa::try_v1(&vaa.try_borrow_data()?)?.body().emitter_address().as_ref(), - &EncodedVaa::try_v1(&vaa.try_borrow_data()?)?.body().emitter_chain().to_be_bytes(), - &EncodedVaa::try_v1(&vaa.try_borrow_data()?)?.body().sequence().to_be_bytes(), - ], - bump, - )] - claim: Account<'info, LegacyAnchorized<0, Claim>>, + /// CHECK: Account representing that a VAA has been consumed. Seeds are checked when + /// [claim_vaa](core_bridge_sdk::cpi::claim_vaa) is called. + #[account(mut)] + claim: AccountInfo<'info>, #[account( init, payer = payer, space = RegisteredEmitter::INIT_SPACE, - seeds = [try_decree_foreign_chain(&vaa.try_borrow_data()?)?.to_be_bytes().as_ref()], + seeds = [try_decree_foreign_chain_bytes(&vaa)?.as_ref()], bump, )] registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, @@ -46,41 +33,60 @@ pub struct RegisterChain<'info> { /// both emitter chain and address to derive this PDA address. Having both of these as seeds /// potentially allows for multiple emitters to be registered for a given chain ID (when there /// should only be one). - /// - /// See the new `register_chain` instruction handler for the correct way to create this account. #[account( init, payer = payer, space = RegisteredEmitter::INIT_SPACE, seeds = [ - try_decree_foreign_chain(&vaa.try_borrow_data()?)?.to_be_bytes().as_ref(), - try_decree_foreign_emitter(&vaa.try_borrow_data()?)?.as_ref(), + try_decree_foreign_chain_bytes(&vaa)?.as_ref(), + try_decree_foreign_emitter(&vaa)?.as_ref(), ], bump, )] legacy_registered_emitter: Account<'info, LegacyAnchorized<0, RegisteredEmitter>>, system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, + core_bridge_program: Program<'info, core_bridge_sdk::cpi::CoreBridge>, +} + +impl<'info> core_bridge_sdk::cpi::system_program::CreateAccount<'info> for RegisterChain<'info> { + fn system_program(&self) -> AccountInfo<'info> { + self.system_program.to_account_info() + } + + fn payer(&self) -> AccountInfo<'info> { + self.payer.to_account_info() + } } impl<'info> RegisterChain<'info> { fn constraints(ctx: &Context) -> Result<()> { - super::require_valid_governance_encoded_vaa(&ctx.accounts.vaa.data.borrow()).map(|_| ()) + let vaa_acc_info = &ctx.accounts.vaa; + let vaa_key = vaa_acc_info.key(); + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let gov_payload = crate::processor::require_valid_governance_vaa(&vaa_key, &vaa)?; + + gov_payload + .register_chain() + .ok_or(error!(TokenBridgeError::InvalidGovernanceAction))?; + + // Done. + Ok(()) } } #[access_control(RegisterChain::constraints(&ctx))] pub fn register_chain(ctx: Context) -> Result<()> { - // Mark the claim as complete. The account only exists to ensure that the VAA is not processed, - // so this value does not matter. But the legacy program set this data to true. - ctx.accounts.claim.is_complete = true; + let vaa = core_bridge_sdk::VaaAccount::load(&ctx.accounts.vaa).unwrap(); + + // Create the claim account to provide replay protection. Because this instruction creates this + // account every time it is executed, this account cannot be created again with this emitter + // address, chain and sequence combination. + core_bridge_sdk::cpi::claim_vaa(ctx.accounts, &ctx.accounts.claim, &crate::ID, &vaa)?; // Deserialize and set data in registered emitter accounts. { - let acc_data = ctx.accounts.vaa.data.borrow(); - let encoded_vaa = EncodedVaa::parse(&acc_data).unwrap(); - let gov_payload = TokenBridgeGovPayload::try_from(encoded_vaa.v1().unwrap().payload()) + let gov_payload = TokenBridgeGovPayload::try_from(vaa.try_payload().unwrap()) .unwrap() .decree(); let decree = gov_payload.register_chain().unwrap(); @@ -96,42 +102,24 @@ pub fn register_chain(ctx: Context) -> Result<()> { .set_inner(registered.into()); } - // Determine if we can close the vaa account. - let payer = &ctx.accounts.payer; - let write_authority = EncodedVaa::parse(&ctx.accounts.vaa.data.borrow()) - .unwrap() - .write_authority(); - if payer.key() == write_authority { - core_bridge_program::cpi::process_encoded_vaa( - CpiContext::new( - ctx.accounts.core_bridge_program.to_account_info(), - core_bridge_program::cpi::accounts::ProcessEncodedVaa { - write_authority: payer.to_account_info(), - encoded_vaa: ctx.accounts.vaa.to_account_info(), - guardian_set: None, - }, - ), - core_bridge_program::ProcessEncodedVaaDirective::CloseVaaAccount, - ) - } else { - Ok(()) - } + // Done. + Ok(()) } -fn try_decree_foreign_chain(vaa_acc_data: &[u8]) -> Result { - let vaa = EncodedVaa::try_v1(vaa_acc_data)?; - let gov_payload = TokenBridgeGovPayload::try_from(vaa.body().payload()) +fn try_decree_foreign_chain_bytes(vaa_acc_info: &AccountInfo) -> Result<[u8; 2]> { + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let gov_payload = TokenBridgeGovPayload::try_from(vaa.try_payload()?) .map_err(|_| error!(TokenBridgeError::InvalidGovernanceVaa))?; gov_payload .decree() .register_chain() - .map(|decree| decree.foreign_chain()) + .map(|decree| decree.foreign_chain().to_be_bytes()) .ok_or(error!(TokenBridgeError::InvalidGovernanceAction)) } -fn try_decree_foreign_emitter(vaa_acc_data: &[u8]) -> Result<[u8; 32]> { - let vaa = EncodedVaa::try_v1(vaa_acc_data)?; - let gov_payload = TokenBridgeGovPayload::try_from(vaa.body().payload()) +fn try_decree_foreign_emitter(vaa_acc_info: &AccountInfo) -> Result<[u8; 32]> { + let vaa = core_bridge_sdk::VaaAccount::load(vaa_acc_info)?; + let gov_payload = TokenBridgeGovPayload::try_from(vaa.try_payload()?) .map_err(|_| error!(TokenBridgeError::InvalidGovernanceVaa))?; gov_payload .decree() diff --git a/solana/programs/token-bridge/src/processor/governance/secure_registered_emitter.rs b/solana/programs/token-bridge/src/processor/governance/secure_registered_emitter.rs index 97fde6707f..14b84226c4 100644 --- a/solana/programs/token-bridge/src/processor/governance/secure_registered_emitter.rs +++ b/solana/programs/token-bridge/src/processor/governance/secure_registered_emitter.rs @@ -1,4 +1,4 @@ -use crate::{error::TokenBridgeError, state::RegisteredEmitter}; +use crate::state::RegisteredEmitter; use anchor_lang::prelude::*; use core_bridge_program::legacy::utils::LegacyAnchorized; @@ -8,7 +8,7 @@ pub struct SecureRegisteredEmitter<'info> { payer: Signer<'info>, #[account( - init_if_needed, + init, payer = payer, space = RegisteredEmitter::INIT_SPACE, seeds = [legacy_registered_emitter.chain.to_be_bytes().as_ref()], @@ -34,32 +34,7 @@ pub struct SecureRegisteredEmitter<'info> { system_program: Program<'info, System>, } -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum SecureRegisteredEmitterDirective { - Init, - CloseLegacy, -} - -pub fn secure_registered_emitter( - ctx: Context, - directive: SecureRegisteredEmitterDirective, -) -> Result<()> { - match directive { - SecureRegisteredEmitterDirective::Init => init(ctx), - SecureRegisteredEmitterDirective::CloseLegacy => close_legacy(ctx), - } -} - -fn init(ctx: Context) -> Result<()> { - msg!("Directive: Init"); - - let registered = &mut ctx.accounts.registered_emitter; - require_eq!( - registered.chain, - 0, - TokenBridgeError::EmitterAlreadyRegistered - ); - +pub fn secure_registered_emitter(ctx: Context) -> Result<()> { let emitter = &ctx.accounts.legacy_registered_emitter; // Copy registered emitter account. @@ -74,17 +49,3 @@ fn init(ctx: Context) -> Result<()> { // Done. Ok(()) } - -fn close_legacy(ctx: Context) -> Result<()> { - msg!("Directive: CloseLegacy"); - - require_eq!( - ctx.accounts.legacy_registered_emitter.chain, - ctx.accounts.registered_emitter.chain, - TokenBridgeError::RegisteredEmitterMismatch - ); - - err!(TokenBridgeError::UnsupportedInstructionDirective) - - //ctx.accounts.legacy_registered_emitter.close(ctx.accounts.payer.to_account_info()) -} diff --git a/solana/programs/token-bridge/src/processor/mod.rs b/solana/programs/token-bridge/src/processor/mod.rs index 6e27e3d5de..21b14b1cfd 100644 --- a/solana/programs/token-bridge/src/processor/mod.rs +++ b/solana/programs/token-bridge/src/processor/mod.rs @@ -1,138 +1,2 @@ mod governance; pub use governance::*; - -// mod transfer_tokens; -// pub use transfer_tokens::*; - -// mod transfer_tokens_with_payload; -// pub use transfer_tokens_with_payload::*; - -use crate::{ - constants::{ - CUSTODY_AUTHORITY_SEED_PREFIX, EMITTER_SEED_PREFIX, MINT_AUTHORITY_SEED_PREFIX, - TRANSFER_AUTHORITY_SEED_PREFIX, - }, - utils::TruncateAmount, - zero_copy::Mint, -}; -use anchor_lang::prelude::*; -use anchor_spl::token; -use core_bridge_program::{sdk as core_bridge_sdk, types::Commitment}; -use wormhole_io::Writeable; - -pub fn post_token_bridge_message< - 'info, - I: core_bridge_sdk::cpi::PublishMessage<'info>, - W: Writeable, ->( - accounts: &I, - emitter_bump: u8, - nonce: u32, - message: W, -) -> Result<()> { - core_bridge_sdk::cpi::publish_message( - accounts, - core_bridge_sdk::cpi::PublishMessageDirective::Message { - nonce, - payload: message.to_vec(), - commitment: Commitment::Finalized, - }, - &[EMITTER_SEED_PREFIX, &[emitter_bump]], - None, - ) -} - -pub fn mint_wrapped_tokens<'info>( - token_program: &Program<'info, token::Token>, - wrapped_mint: &AccountInfo<'info>, - dst_token: &AccountInfo<'info>, - mint_authority: &AccountInfo<'info>, - mint_authority_bump: u8, - mint_amount: u64, -) -> Result<()> { - token::mint_to( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::MintTo { - mint: wrapped_mint.to_account_info(), - to: dst_token.to_account_info(), - authority: mint_authority.to_account_info(), - }, - &[&[MINT_AUTHORITY_SEED_PREFIX, &[mint_authority_bump]]], - ), - mint_amount, - ) -} - -pub fn burn_wrapped_tokens<'info>( - token_program: &Program<'info, token::Token>, - wrapped_mint: &AccountInfo<'info>, - src_token: &AccountInfo<'info>, - transfer_authority: &AccountInfo<'info>, - transfer_authority_bump: u8, - burn_amount: u64, -) -> Result<()> { - token::burn( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Burn { - mint: wrapped_mint.to_account_info(), - from: src_token.to_account_info(), - authority: transfer_authority.to_account_info(), - }, - &[&[TRANSFER_AUTHORITY_SEED_PREFIX, &[transfer_authority_bump]]], - ), - burn_amount, - ) -} - -pub fn withdraw_native_tokens<'info>( - token_program: &Program<'info, token::Token>, - custody_token: &AccountInfo<'info>, - dst_token: &AccountInfo<'info>, - custody_authority: &AccountInfo<'info>, - custody_authority_bump: u8, - transfer_amount: u64, -) -> Result<()> { - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: custody_token.to_account_info(), - to: dst_token.to_account_info(), - authority: custody_authority.to_account_info(), - }, - &[&[CUSTODY_AUTHORITY_SEED_PREFIX, &[custody_authority_bump]]], - ), - transfer_amount, - ) -} - -pub fn deposit_native_tokens<'info>( - token_program: &Program<'info, token::Token>, - mint: &AccountInfo<'info>, - src_token: &AccountInfo<'info>, - custody_token: &Account<'info, token::TokenAccount>, - transfer_authority: &AccountInfo<'info>, - transfer_authority_bump: u8, - raw_amount: u64, -) -> Result { - let transfer_amount = Mint::parse(&mint.data.borrow()) - .unwrap() - .truncate_amount(raw_amount); - - token::transfer( - CpiContext::new_with_signer( - token_program.to_account_info(), - token::Transfer { - from: src_token.to_account_info(), - to: custody_token.to_account_info(), - authority: transfer_authority.to_account_info(), - }, - &[&[TRANSFER_AUTHORITY_SEED_PREFIX, &[transfer_authority_bump]]], - ), - transfer_amount, - )?; - - Ok(transfer_amount) -} diff --git a/solana/programs/token-bridge/src/processor/transfer_tokens.rs b/solana/programs/token-bridge/src/processor/transfer_tokens.rs deleted file mode 100644 index 62945e8b82..0000000000 --- a/solana/programs/token-bridge/src/processor/transfer_tokens.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::{ - constants::{EMITTER_SEED_PREFIX, TRANSFER_AUTHORITY_SEED_PREFIX, WRAPPED_MINT_SEED_PREFIX}, - error::TokenBridgeError, - legacy::TransferTokensArgs, - processor::{burn_wrapped_tokens, post_token_bridge_message}, - state::WrappedAsset, -}; -use anchor_lang::prelude::*; -use anchor_spl::token::{Mint, Token, TokenAccount}; -use core_bridge_program::{ - self, - state::{Config as CoreBridgeConfig, EmitterSequence}, - CoreBridge, -}; -use ruint::aliases::U256; -use solana_program::program_option::COption; - -#[derive(Accounts)] -pub struct TransferTokens<'info> { - #[account(mut)] - payer: Signer<'info>, - - /// CHECK: Token Bridge never needed this account for this instruction. This account is a - /// placeholder just in case we can use a Token Bridge program configuration. - _config: UncheckedAccount<'info>, - - #[account(mut)] - src_token: Box>, - - /// CHECK: This account can either be Token Bridge's custody token account or Token Bridge's - /// wrapped mint account. Either way, this account must be mutable. - #[account(mut)] - custody_token_or_wrapped_mint: AccountInfo<'info>, - - /// CHECK: This account can either be a native SPL mint or Token Bridge's wrapped asset account. - /// Either way, this account is read-only. - native_mint_or_wrapped_asset: AccountInfo<'info>, - - /// CHECK: This authority is whom the source token account owner delegates spending approval for - /// transferring native assets or burning wrapped assets. - #[account( - seeds = [TRANSFER_AUTHORITY_SEED_PREFIX], - bump - )] - transfer_authority: AccountInfo<'info>, - - /// We need to deserialize this account to determine the Wormhole message fee. - #[account( - mut, - seeds = [CoreBridgeConfig::SEED_PREFIX], - bump, - seeds::program = core_bridge_program - )] - core_bridge_config: Account<'info, CoreBridgeConfig>, - - /// CHECK: This account is needed for the Core Bridge program. - #[account( - mut, - seeds = [b"msg", core_emitter_sequence.to_be_bytes().as_ref()], - bump, - )] - core_message: AccountInfo<'info>, - - /// CHECK: We need this emitter to invoke the Core Bridge program to send Wormhole messages. - #[account( - seeds = [EMITTER_SEED_PREFIX], - bump, - )] - core_emitter: AccountInfo<'info>, - - /// We let the Core Bridge program validate this PDA. But we deserialize this account for the - /// core message PDA. - #[account(mut)] - core_emitter_sequence: Account<'info, EmitterSequence>, - - /// CHECK: This account is needed for the Core Bridge program. - #[account(mut)] - core_fee_collector: Option>, - - system_program: Program<'info, System>, - core_bridge_program: Program<'info, CoreBridge>, - token_program: Program<'info, Token>, -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct TransferRelayable { - pub nonce: u32, - pub amount: u64, - pub relayer_fee: u64, - pub recipient: [u8; 32], - pub recipient_chain: u16, -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct TransferWithPayload { - pub nonce: u32, - pub amount: u64, - pub redeemer: [u8; 32], - pub redeemer_chain: u16, - pub payload: Vec, - pub cpi_program_id: Option, -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub enum TransferTokensDirective { - TransferRelayable(TransferRelayable), - TransferWithPayload(TransferWithPayload), -} - -pub fn transfer_tokens( - ctx: Context, - directive: TransferTokensDirective, -) -> Result<()> { - match directive { - TransferTokensDirective::TransferRelayable(args) => relayable(ctx, args), - TransferTokensDirective::TransferWithPayload(args) => unimplemented!(), - } -} - -fn relayable(ctx: Context, args: TransferRelayable) -> Result<()> { - let TransferRelayable { - nonce, - amount, - relayer_fee, - recipient, - recipient_chain, - } = args; - - require_gte!(amount, relayer_fee, TokenBridgeError::InvalidRelayerFee); - - let token_transfer = if is_wrapped(&ctx.accounts.custody_token_or_wrapped_mint) { - // Chicken and egg problem here. First we will deserialize the wrapped asset account so we - // can derive the wrapped mint PDA address. Then we will validate the wrapped asset PDA - // address with this wrapped mint pubkey. - let mut wrapped_asset = { - let info = &ctx.accounts.native_mint_or_wrapped_asset; - require_keys_eq!(info.owner, crate::ID, ErrorCode::ConstraintOwner); - - WrappedAsset::try_deserialize(&mut &info.try_borrow_data()?)? - }; - - // Burn wrapped assets from the sender's account. - burn_wrapped_tokens( - &ctx.accounts.token_program, - &ctx.accounts.wrapped_mint, - &ctx.accounts.src_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], - amount, - )?; - - // Prepare Wormhole message. Amounts do not need to be normalized because we are working with - // wrapped assets. - crate::messages::Transfer { - norm_amount: U256::from(amount), - token_address: wrapped_asset.token_address, - token_chain: wrapped_asset.token_chain, - recipient, - recipient_chain, - norm_relayer_fee: U256::from(relayer_fee), - } - } else { - // Deposit native assets from the sender's account into the custody account. - let amount = deposit_native_tokens( - &ctx.accounts.token_program, - &ctx.accounts.mint, - &ctx.accounts.src_token, - &ctx.accounts.custody_token, - &ctx.accounts.transfer_authority, - ctx.bumps["transfer_authority"], - amount, - )?; - }; - - // Finally publish Wormhole message using the Core Bridge. - post_token_bridge_message( - ctx.accounts, - ctx.bumps["core_emitter"], - nonce, - token_transfer, - ) -} - -fn is_wrapped(mint: &Account<'_, Mint>) -> bool { - if let COption::Some(mint_authority) = mint.mint_authority { - let (token_bridge_mint_authority, _) = Pubkey::find_program_address( - &[crate::constants::MINT_AUTHORITY_SEED_PREFIX], - &crate::ID, - ); - - if mint_authority == token_bridge_mint_authority { - true - } else { - false - } - } else { - false - } -} diff --git a/solana/programs/token-bridge/src/processor/transfer_tokens_with_payload/mod.rs b/solana/programs/token-bridge/src/processor/transfer_tokens_with_payload/mod.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/solana/programs/token-bridge/src/processor/transfer_tokens_with_payload/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/solana/programs/token-bridge/src/sdk/cpi/complete_transfer.rs b/solana/programs/token-bridge/src/sdk/cpi/complete_transfer.rs new file mode 100644 index 0000000000..3280fd0f0a --- /dev/null +++ b/solana/programs/token-bridge/src/sdk/cpi/complete_transfer.rs @@ -0,0 +1,218 @@ +use crate::{error::TokenBridgeError, zero_copy::Mint}; +use anchor_lang::prelude::*; +use core_bridge_program::sdk::LoadZeroCopy; + +pub trait CompleteTransfer<'info>: super::system_program::CreateAccount<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info>; + + /// SPL Token Program. + fn token_program(&self) -> AccountInfo<'info>; + + fn vaa(&self) -> AccountInfo<'info>; + + fn token_bridge_claim(&self) -> AccountInfo<'info>; + + fn token_bridge_registered_emitter(&self) -> AccountInfo<'info>; + + /// Destination Token Account (where tokens will be transferred to). For regular transfers, this + /// account belongs to the recipient, which may be encoded in the VAA as the recipient. + fn dst_token_account(&self) -> AccountInfo<'info>; + + /// Either of these types of mint: + /// - Native (read-only). + /// - Wrapped (mut, seeds = \["wrapped", token_chain, token_address\]). + fn mint(&self) -> AccountInfo<'info>; + + /// Custody Token Account (mut, seeds = \[mint.key\]. + /// + /// NOTE: This must be specified as `Some(custody_token_account)` if the mint is native. + fn token_bridge_custody_token_account(&self) -> Option> { + None + } + + /// Custody Authority (read-only, seeds = \["custody_signer"\]). + /// + /// NOTE: This must be specified as `Some(custody_authority)` if the mint is native. + fn token_bridge_custody_authority(&self) -> Option> { + None + } + + /// Wrapped Asset (read-only, seeds = \["meta", mint.key\]. + /// + /// NOTE: This must be specified as `Some(wrapped_asset)` if the mint is wrapped. + fn token_bridge_wrapped_asset(&self) -> Option> { + None + } + + fn token_bridge_mint_authority(&self) -> Option> { + None + } + + /// Redeemer Authority (read-only signer). In order to redeem a transfer as your program (the + /// encoded redeemer address is your program ID), use seeds = ["redeemer"]. Otherwise the + /// redeemer address of your custom signer must be specified (which will be the pubkey of this + /// account). + /// + /// NOTE: This account must be specified as `Some(redeemer_authority)` if the transfer redeemed + /// has a message payload associated with it. + fn redeemer_authority(&self) -> Option> { + None + } + + /// Relayer's token account. + fn payer_token(&self) -> Option> { + None + } + + /// Recipient, who is the owner of [dst_token_account](CompleteTransfer::dst_token_account). + /// This account only matters if the transfer redeemed is a relayable transfer (i.e. no message + /// associated with the transfer). + fn recipient(&self) -> Option> { + None + } +} + +pub fn complete_transfer<'info, A>(accounts: &A, signer_seeds: Option<&[&[&[u8]]]>) -> Result<()> +where + A: CompleteTransfer<'info>, +{ + // If whether this mint is wrapped is unspecified, we derive the mint authority, which will cost + // some compute units. + let is_wrapped_asset = crate::utils::is_wrapped_mint(&Mint::load(&accounts.mint())?); + + complete_transfer_specified(accounts, is_wrapped_asset, signer_seeds) +} + +pub fn complete_transfer_specified<'info, A>( + accounts: &A, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: CompleteTransfer<'info>, +{ + match accounts.redeemer_authority() { + Some(_) => handle_complete_transfer_with_payload(accounts, is_wrapped_asset, signer_seeds), + None => handle_complete_transfer(accounts, is_wrapped_asset, signer_seeds), + } +} + +fn handle_complete_transfer<'info, A>( + accounts: &A, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: CompleteTransfer<'info>, +{ + let payer_token = accounts + .payer_token() + .ok_or(error!(TokenBridgeError::PayerTokenRequired))?; + + if is_wrapped_asset { + crate::legacy::cpi::complete_transfer_wrapped(CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::CompleteTransferWrapped { + payer: accounts.payer(), + vaa: accounts.vaa(), + claim: accounts.token_bridge_claim(), + registered_emitter: accounts.token_bridge_registered_emitter(), + recipient_token: accounts.dst_token_account(), + payer_token, + wrapped_mint: accounts.mint(), + wrapped_asset: accounts + .token_bridge_wrapped_asset() + .ok_or(error!(TokenBridgeError::WrappedAssetRequired))?, + mint_authority: accounts + .token_bridge_mint_authority() + .ok_or(error!(TokenBridgeError::MintAuthorityRequired))?, + recipient: accounts.recipient(), + system_program: accounts.system_program(), + token_program: accounts.token_program(), + }, + signer_seeds.unwrap_or_default(), + )) + } else { + crate::legacy::cpi::complete_transfer_native(CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::CompleteTransferNative { + payer: accounts.payer(), + vaa: accounts.vaa(), + claim: accounts.token_bridge_claim(), + registered_emitter: accounts.token_bridge_registered_emitter(), + recipient_token: accounts.dst_token_account(), + payer_token, + custody_token: accounts + .token_bridge_custody_token_account() + .ok_or(error!(TokenBridgeError::CustodyTokenAccountRequired))?, + mint: accounts.mint(), + custody_authority: accounts + .token_bridge_custody_authority() + .ok_or(error!(TokenBridgeError::CustodyAuthorityRequired))?, + recipient: accounts.recipient(), + system_program: accounts.system_program(), + token_program: accounts.token_program(), + }, + signer_seeds.unwrap_or_default(), + )) + } +} + +fn handle_complete_transfer_with_payload<'info, A>( + accounts: &A, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: CompleteTransfer<'info>, +{ + let redeemer_authority = accounts + .redeemer_authority() + .ok_or(error!(TokenBridgeError::RedeemerAuthorityRequired))?; + + if is_wrapped_asset { + crate::legacy::cpi::complete_transfer_with_payload_wrapped(CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::CompleteTransferWithPayloadWrapped { + payer: accounts.payer(), + vaa: accounts.vaa(), + claim: accounts.token_bridge_claim(), + registered_emitter: accounts.token_bridge_registered_emitter(), + dst_token: accounts.dst_token_account(), + redeemer_authority, + wrapped_mint: accounts.mint(), + wrapped_asset: accounts + .token_bridge_wrapped_asset() + .ok_or(error!(TokenBridgeError::WrappedAssetRequired))?, + mint_authority: accounts + .token_bridge_mint_authority() + .ok_or(error!(TokenBridgeError::MintAuthorityRequired))?, + system_program: accounts.system_program(), + token_program: accounts.token_program(), + }, + signer_seeds.unwrap_or_default(), + )) + } else { + crate::legacy::cpi::complete_transfer_with_payload_native(CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::CompleteTransferWithPayloadNative { + payer: accounts.payer(), + vaa: accounts.vaa(), + claim: accounts.token_bridge_claim(), + registered_emitter: accounts.token_bridge_registered_emitter(), + dst_token: accounts.dst_token_account(), + redeemer_authority, + custody_token: accounts + .token_bridge_custody_token_account() + .ok_or(error!(TokenBridgeError::CustodyTokenAccountRequired))?, + mint: accounts.mint(), + custody_authority: accounts + .token_bridge_custody_authority() + .ok_or(error!(TokenBridgeError::CustodyAuthorityRequired))?, + system_program: accounts.system_program(), + token_program: accounts.token_program(), + }, + signer_seeds.unwrap_or_default(), + )) + } +} diff --git a/solana/programs/token-bridge/src/sdk/cpi/mod.rs b/solana/programs/token-bridge/src/sdk/cpi/mod.rs new file mode 100644 index 0000000000..1b165b00c5 --- /dev/null +++ b/solana/programs/token-bridge/src/sdk/cpi/mod.rs @@ -0,0 +1,20 @@ +//! CPI builders. Methods useful for interacting with the Core Bridge program from another program. + +#[doc(inline)] +pub use core_bridge_program::sdk::cpi::system_program; + +/// Sub-module for SPL Token program interaction. +pub mod token { + pub use crate::utils::cpi::{ + burn, burn_from, mint_to, transfer, transfer_from, Burn, MintTo, Transfer, + }; +} + +mod complete_transfer; +pub use complete_transfer::*; + +mod transfer_tokens; +pub use transfer_tokens::*; + +/// Wormhole Token Bridge Program. +pub type TokenBridge = crate::program::WormholeTokenBridgeSolana; diff --git a/solana/programs/token-bridge/src/sdk/cpi/transfer_tokens.rs b/solana/programs/token-bridge/src/sdk/cpi/transfer_tokens.rs new file mode 100644 index 0000000000..86e962d412 --- /dev/null +++ b/solana/programs/token-bridge/src/sdk/cpi/transfer_tokens.rs @@ -0,0 +1,352 @@ +use crate::{ + error::TokenBridgeError, + legacy::instruction::{TransferTokensArgs, TransferTokensWithPayloadArgs}, +}; +use anchor_lang::prelude::*; +use core_bridge_program::sdk::{self as core_bridge_sdk, LoadZeroCopy}; + +/// Trait for invoking one of the ways you can transfer assets to another network using the Token +/// Bridge program. +/// +/// NOTE: A sender's address can either be a program's ID or a custom PDA address for transfers with +/// a message payload. If the sender address is a program ID, then the seeds for the sender +/// authority must be \["sender"\]. +pub trait TransferTokens<'info>: core_bridge_sdk::cpi::PublishMessage<'info> { + fn token_bridge_program(&self) -> AccountInfo<'info>; + + /// SPL Token Program. + fn token_program(&self) -> AccountInfo<'info>; + + /// Core Bridge message account, which the Token Bridge program needs to publish its messages + /// via Wormhole. + fn core_message(&self) -> AccountInfo<'info>; + + /// Source Token Account (where tokens will be transferred from). + fn src_token_account(&self) -> AccountInfo<'info>; + + /// Either of these types of mint: + /// - Native (read-only). + /// - Wrapped (mut, seeds = \["wrapped", token_chain, token_address\]). + /// + /// NOTE: If your instruction accepts either wrapped or native mints, you must specify this + /// account with `#[account(mut)]`. + fn mint(&self) -> AccountInfo<'info>; + + /// Transfer Authority (read-only, seeds = \["authority_signer"\]). + fn token_bridge_transfer_authority(&self) -> AccountInfo<'info>; + + /// Custody Token Account (mut, seeds = \[mint.key\]. + /// + /// NOTE: This must be specified as `Some(custody_token_account)` if the mint is native. + fn token_bridge_custody_token_account(&self) -> Option> { + None + } + + /// Custody Authority (read-only, seeds = \["custody_signer"\]). + /// + /// NOTE: This must be specified as `Some(custody_authority)` if the mint is native. + fn token_bridge_custody_authority(&self) -> Option> { + None + } + + /// Wrapped Asset (read-only, seeds = \["meta", mint.key\]. + /// + /// NOTE: This must be specified as `Some(wrapped_asset)` if the mint is wrapped. + fn token_bridge_wrapped_asset(&self) -> Option> { + None + } + + /// Sender Authority (read-only signer). In order for the program ID to be encoded as the sender + /// address, use seeds = ["sender"]. + /// + /// NOTE: This must be specified as `Some(sender_authority)` if are using either + /// [SignerTransferWithPayload](TransferTokensDirective::SignerTransferWithPayload) or + /// [ProgramTransferWithPayload](TransferTokensDirective::ProgramTransferWithPayload). + fn sender_authority(&self) -> Option> { + None + } +} + +/// Direcrtive used to determine how to transfer assets. +pub enum TransferTokensDirective { + /// Ordinary transfer with relay. If a relayer fee greater than zero is specified, this amount + /// is deducted from the transfer amount to pay the redeemer of this transfer. This is useful to + /// incentivize relayers to redeem your transfer (only if it is cost-effective for them to do + /// so). + Transfer { + nonce: u32, + amount: u64, + relayer_fee: u64, + recipient: [u8; 32], + recipient_chain: u16, + }, + /// Transfer with custom message payload. The sender address is the program ID of your program. + /// + /// NOTE: [sender_authority](TransferTokens::sender_authority) must use seeds = \["sender"\]. + ProgramTransferWithPayload { + program_id: Pubkey, + nonce: u32, + amount: u64, + redeemer: [u8; 32], + redeemer_chain: u16, + payload: Vec, + }, + /// Transfer with custom message payload. The sender address is the pubkey of the + /// [sender_authority](TransferTokens::sender_authority). + SignerTransferWithPayload { + nonce: u32, + amount: u64, + redeemer: [u8; 32], + redeemer_chain: u16, + payload: Vec, + }, +} + +/// SDK method for transferring assets to another network with the Token Bridge program. +/// +/// This method will handle any of the following directives: +/// +/// * Transfer with relay. +/// * Transfer with message payload, whose sender is your program ID. +/// * Transfer with message payload, whose sender is either a keypair pubkey or PDA address. +/// +/// Same requirements as [transfer_tokens_specified] except that this method will determine whether +/// the asset is wrapped or not by checking the [mint](TransferTokens::mint) account. **This will +/// cost a modest amount of compute units for the convenience of determining whether the mint is +/// Token Bridge wrapped or not.** +pub fn transfer_tokens<'info, A>( + accounts: &A, + directive: TransferTokensDirective, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: TransferTokens<'info>, +{ + // If whether this mint is wrapped is unspecified, we derive the mint authority, which will cost + // some compute units. + let is_wrapped_asset = + crate::utils::is_wrapped_mint(&crate::zero_copy::Mint::load(&accounts.mint())?); + + transfer_tokens_specified(accounts, directive, is_wrapped_asset, signer_seeds) +} + +/// SDK method for transferring assets to another network with the Token Bridge program. +/// +/// This method will handle any of the following directives: +/// * Transfer with relay. +/// * Transfer with message payload, whose sender is your program ID. +/// * Transfer with message payload, whose sender is either a keypair pubkey or PDA address. +/// +/// The accounts must implement [TransferTokens]. +/// +/// Sender authority seeds are needed to act as a signer for the transfer with payload directives. +/// These seeds are either the seeds of a PDA or specifically seeds = \["sender"\] if the program ID +/// is the sender address. +/// +/// Core message seeds are optional and are only needed if the integrating program is using a PDA +/// for this account. Otherwise, a keypair can be used. +pub fn transfer_tokens_specified<'info, A>( + accounts: &A, + directive: TransferTokensDirective, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: TransferTokens<'info>, +{ + match directive { + TransferTokensDirective::Transfer { + nonce, + amount, + relayer_fee, + recipient, + recipient_chain, + } => handle_transfer_tokens( + accounts, + TransferTokensArgs { + nonce, + amount, + relayer_fee, + recipient, + recipient_chain, + }, + is_wrapped_asset, + signer_seeds, + ), + TransferTokensDirective::ProgramTransferWithPayload { + program_id, + nonce, + amount, + redeemer, + redeemer_chain, + payload, + } => handle_transfer_tokens_with_payload( + accounts, + TransferTokensWithPayloadArgs { + nonce, + amount, + redeemer, + redeemer_chain, + payload, + cpi_program_id: Some(program_id), + }, + is_wrapped_asset, + signer_seeds, + ), + TransferTokensDirective::SignerTransferWithPayload { + nonce, + amount, + redeemer, + redeemer_chain, + payload, + } => handle_transfer_tokens_with_payload( + accounts, + TransferTokensWithPayloadArgs { + nonce, + amount, + redeemer, + redeemer_chain, + payload, + cpi_program_id: None, + }, + is_wrapped_asset, + signer_seeds, + ), + } +} + +fn handle_transfer_tokens<'info, A>( + accounts: &A, + args: TransferTokensArgs, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: TransferTokens<'info>, +{ + if is_wrapped_asset { + crate::legacy::cpi::transfer_tokens_wrapped( + CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::TransferTokensWrapped { + payer: accounts.payer(), + src_token: accounts.src_token_account(), + wrapped_mint: accounts.mint(), + wrapped_asset: accounts + .token_bridge_wrapped_asset() + .ok_or(error!(TokenBridgeError::WrappedAssetRequired))?, + transfer_authority: accounts.token_bridge_transfer_authority(), + core_bridge_config: accounts.core_bridge_config(), + core_message: accounts.core_message(), + core_emitter: accounts.core_emitter_authority(), + core_emitter_sequence: accounts.core_emitter_sequence(), + core_fee_collector: accounts.core_fee_collector(), + system_program: accounts.system_program(), + token_program: accounts.token_program(), + core_bridge_program: accounts.core_bridge_program(), + }, + signer_seeds.unwrap_or_default(), + ), + args, + ) + } else { + crate::legacy::cpi::transfer_tokens_native( + CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::TransferTokensNative { + payer: accounts.payer(), + src_token: accounts.src_token_account(), + mint: accounts.mint(), + custody_token: accounts + .token_bridge_custody_token_account() + .ok_or(error!(TokenBridgeError::CustodyTokenAccountRequired))?, + transfer_authority: accounts.token_bridge_transfer_authority(), + custody_authority: accounts + .token_bridge_custody_authority() + .ok_or(error!(TokenBridgeError::CustodyAuthorityRequired))?, + core_bridge_config: accounts.core_bridge_config(), + core_message: accounts.core_message(), + core_emitter: accounts.core_emitter_authority(), + core_emitter_sequence: accounts.core_emitter_sequence(), + core_fee_collector: accounts.core_fee_collector(), + system_program: accounts.system_program(), + token_program: accounts.token_program(), + core_bridge_program: accounts.core_bridge_program(), + }, + signer_seeds.unwrap_or_default(), + ), + args, + ) + } +} + +fn handle_transfer_tokens_with_payload<'info, A>( + accounts: &A, + args: TransferTokensWithPayloadArgs, + is_wrapped_asset: bool, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: TransferTokens<'info>, +{ + if is_wrapped_asset { + crate::legacy::cpi::transfer_tokens_with_payload_wrapped( + CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::TransferTokensWithPayloadWrapped { + payer: accounts.payer(), + src_token: accounts.src_token_account(), + wrapped_mint: accounts.mint(), + wrapped_asset: accounts + .token_bridge_wrapped_asset() + .ok_or(error!(TokenBridgeError::WrappedAssetRequired))?, + transfer_authority: accounts.token_bridge_transfer_authority(), + core_bridge_config: accounts.core_bridge_config(), + core_message: accounts.core_message(), + core_emitter: accounts.core_emitter_authority(), + core_emitter_sequence: accounts.core_emitter_sequence(), + core_fee_collector: accounts.core_fee_collector(), + sender_authority: accounts + .sender_authority() + .ok_or(error!(TokenBridgeError::SenderAuthorityRequired))?, + system_program: accounts.system_program(), + token_program: accounts.token_program(), + core_bridge_program: accounts.core_bridge_program(), + }, + signer_seeds.unwrap_or_default(), + ), + args, + ) + } else { + crate::legacy::cpi::transfer_tokens_with_payload_native( + CpiContext::new_with_signer( + accounts.token_bridge_program(), + crate::legacy::cpi::TransferTokensWithPayloadNative { + payer: accounts.payer(), + src_token: accounts.src_token_account(), + mint: accounts.mint(), + custody_token: accounts + .token_bridge_custody_token_account() + .ok_or(error!(TokenBridgeError::CustodyTokenAccountRequired))?, + transfer_authority: accounts.token_bridge_transfer_authority(), + custody_authority: accounts + .token_bridge_custody_authority() + .ok_or(error!(TokenBridgeError::CustodyAuthorityRequired))?, + core_bridge_config: accounts.core_bridge_config(), + core_message: accounts.core_message(), + core_emitter: accounts.core_emitter_authority(), + core_emitter_sequence: accounts.core_emitter_sequence(), + core_fee_collector: accounts.core_fee_collector(), + sender_authority: accounts + .sender_authority() + .ok_or(error!(TokenBridgeError::SenderAuthorityRequired))?, + system_program: accounts.system_program(), + token_program: accounts.token_program(), + core_bridge_program: accounts.core_bridge_program(), + }, + signer_seeds.unwrap_or_default(), + ), + args, + ) + } +} diff --git a/solana/programs/token-bridge/src/sdk/mod.rs b/solana/programs/token-bridge/src/sdk/mod.rs new file mode 100644 index 0000000000..078c57a94b --- /dev/null +++ b/solana/programs/token-bridge/src/sdk/mod.rs @@ -0,0 +1,34 @@ +//! **ATTENTION INTEGRATORS!** Token Bridge Program developer kit. It is recommended to use +//! [sdk::cpi](crate::sdk::cpi) for invoking Token Bridge instructions as opposed to the +//! code-generated Anchor CPI (found in [cpi](crate::cpi)) and legacy CPI (found in +//! [legacy::cpi](crate::legacy::cpi)). + +pub use core_bridge_program::sdk as core_bridge_sdk; + +#[doc(inline)] +pub use crate::{ + constants::{PROGRAM_REDEEMER_SEED_PREFIX, PROGRAM_SENDER_SEED_PREFIX}, + legacy::instruction::{TransferTokensArgs, TransferTokensWithPayloadArgs}, + state, + zero_copy::{Mint, TokenAccount}, +}; + +/// Set of structs mirroring the structs deriving Accounts, where each field is a Pubkey. This +/// is useful for specifying accounts for a client. +pub mod accounts { + + #[doc(inline)] + pub use crate::{accounts::*, legacy::accounts::*}; +} + +/// CPI builders. Methods useful for interacting with the Token Bridge program from another program. +#[cfg(feature = "cpi")] +pub mod cpi; + +/// Instruction builders. These should be used directly when one wants to serialize instruction +/// data when speciying instructions on a client. +pub mod instruction { + + #[doc(inline)] + pub use crate::{accounts, instruction::*, legacy::instruction as legacy}; +} diff --git a/solana/programs/token-bridge/src/state/mod.rs b/solana/programs/token-bridge/src/state/mod.rs index d9b727d9d5..177cd7f917 100644 --- a/solana/programs/token-bridge/src/state/mod.rs +++ b/solana/programs/token-bridge/src/state/mod.rs @@ -1 +1,3 @@ +//! Account schemas for the Token Bridge Program. + pub use crate::legacy::state::*; diff --git a/solana/programs/token-bridge/src/utils/cpi.rs b/solana/programs/token-bridge/src/utils/cpi.rs new file mode 100644 index 0000000000..b09f1ee2ce --- /dev/null +++ b/solana/programs/token-bridge/src/utils/cpi.rs @@ -0,0 +1,222 @@ +use crate::constants::EMITTER_SEED_PREFIX; +use anchor_lang::prelude::*; +use anchor_spl::token; +use core_bridge_program::sdk as core_bridge_sdk; +use wormhole_io::Writeable; + +/// Trait for invoking the SPL Token program's mint instruction. +pub trait MintTo<'info> { + fn token_program(&self) -> AccountInfo<'info>; + + /// Mint of the asset being burned. Must be mutable because the supply will change after + /// invoking [mint_to]. + fn mint(&self) -> AccountInfo<'info>; + + /// Authority associated with the mint that permits minting new assets. + fn mint_authority(&self) -> AccountInfo<'info>; +} + +/// Method for invoking the SPL Token program's mint instruction. This method may be useful if you +/// do not want to create CPI contexts repetitively in your instruction handler. +pub fn mint_to<'info, A>( + accounts: &A, + to: &AccountInfo<'info>, + mint_amount: u64, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: MintTo<'info>, +{ + token::mint_to( + CpiContext::new_with_signer( + accounts.token_program(), + token::MintTo { + mint: accounts.mint(), + to: to.to_account_info(), + authority: accounts.mint_authority(), + }, + signer_seeds.unwrap_or_default(), + ), + mint_amount, + ) +} + +/// Trait for invoking the SPL Token program's burn instruction. +pub trait Burn<'info> { + fn token_program(&self) -> AccountInfo<'info>; + + /// Mint of the asset being burned. Must be mutable because the supply will change after + /// invoking [burn]. + fn mint(&self) -> AccountInfo<'info>; + + /// Optional token account from which the asset amount is burned. This account must be + /// `Some(token_account)` if you invoke the [burn] method. It is not required if you invoke + /// [burn_from] since this account is specified in the method. + fn from(&self) -> Option> { + None + } + + /// Optional authority, which permits burning assets from a token account. This account must be + /// `Some(authority)` if you invoke the [burn] method. It is not required if you invoke + /// [burn_from] since this account is specified in the method. + fn authority(&self) -> Option> { + None + } +} + +/// Method for invoking the SPL Token program's burn instruction. This method may be useful if you +/// do not want to create CPI contexts repetitively in your instruction handler. +pub fn burn<'info, A>( + accounts: &A, + burn_amount: u64, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: Burn<'info>, +{ + burn_from( + accounts, + accounts + .from() + .as_ref() + .ok_or(error!(ErrorCode::AccountNotEnoughKeys))?, + accounts + .authority() + .as_ref() + .ok_or(error!(ErrorCode::AccountNotEnoughKeys))?, + burn_amount, + signer_seeds, + ) +} + +/// Method for invoking the SPL Token program's burn instruction by specifying the token account +/// (and its authority) associated with the asset being burned. This method may be useful if you +/// do not want to create CPI contexts repetitively in your instruction handler. +pub fn burn_from<'info, A>( + accounts: &A, + from: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + burn_amount: u64, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: Burn<'info>, +{ + token::burn( + CpiContext::new_with_signer( + accounts.token_program(), + token::Burn { + mint: accounts.mint(), + from: from.to_account_info(), + authority: authority.to_account_info(), + }, + signer_seeds.unwrap_or_default(), + ), + burn_amount, + ) +} + +/// Trait for invoking the SPL Token program's transfer instruction. +pub trait Transfer<'info> { + fn token_program(&self) -> AccountInfo<'info>; + + /// Optional token account from which the asset amount is removed. This account must be + /// `Some(token_account)` if you invoke the [transfer] method. It is not required if you invoke + /// [transfer_from] since this account is specified in the method. + fn from(&self) -> Option> { + None + } + + /// Optional authority, which permits removing assets from a token account. This account must be + /// `Some(authority)` if you invoke the [transfer] method. It is not required if you invoke + /// [transfer_from] since this account is specified in the method. + fn authority(&self) -> Option> { + None + } +} + +/// Method for invoking the SPL Token program's transfer instruction. This method may be useful if +/// you do not want to create CPI contexts repetitively in your instruction handler. +pub fn transfer<'info, A>( + accounts: &A, + to: &AccountInfo<'info>, + transfer_amount: u64, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: Transfer<'info>, +{ + transfer_from( + accounts, + accounts + .from() + .as_ref() + .ok_or(error!(ErrorCode::AccountNotEnoughKeys))?, + accounts + .authority() + .as_ref() + .ok_or(error!(ErrorCode::AccountNotEnoughKeys))?, + to, + transfer_amount, + signer_seeds, + ) +} + +/// Method for invoking the SPL Token program's transfer instruction by specifying the token account +/// (and its authority) associated with the asset being transferred. This method may be useful if +/// you do not want to create CPI contexts repetitively in your instruction handler. +pub fn transfer_from<'info, A>( + accounts: &A, + from: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + to: &AccountInfo<'info>, + transfer_amount: u64, + signer_seeds: Option<&[&[&[u8]]]>, +) -> Result<()> +where + A: Transfer<'info>, +{ + token::transfer( + CpiContext::new_with_signer( + accounts.token_program(), + token::Transfer { + from: from.to_account_info(), + to: to.to_account_info(), + authority: authority.to_account_info(), + }, + signer_seeds.unwrap_or_default(), + ), + transfer_amount, + ) +} + +pub fn post_token_bridge_message<'info, A, W>( + accounts: &A, + core_message: &AccountInfo<'info>, + nonce: u32, + message: W, +) -> Result<()> +where + A: core_bridge_sdk::cpi::PublishMessage<'info>, + W: Writeable, +{ + // Validate core emitter pubkey. + let (expected_core_emitter, emitter_bump) = + Pubkey::find_program_address(&[EMITTER_SEED_PREFIX], &crate::ID); + require_keys_eq!( + accounts.core_emitter_authority().key(), + expected_core_emitter, + ErrorCode::ConstraintSeeds, + ); + + core_bridge_sdk::cpi::publish_message( + accounts, + core_message, + core_bridge_sdk::cpi::PublishMessageDirective::Message { + nonce, + payload: message.to_vec(), + commitment: core_bridge_sdk::types::Commitment::Finalized, + }, + Some(&[&[EMITTER_SEED_PREFIX, &[emitter_bump]]]), + ) +} diff --git a/solana/programs/token-bridge/src/utils/mod.rs b/solana/programs/token-bridge/src/utils/mod.rs index 982acbab5b..f4c50a1926 100644 --- a/solana/programs/token-bridge/src/utils/mod.rs +++ b/solana/programs/token-bridge/src/utils/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod cpi; + mod token; pub use token::*; @@ -19,7 +21,11 @@ pub fn new_sender_address( &[crate::constants::PROGRAM_SENDER_SEED_PREFIX], &program_id, ); - require_eq!(sender_authority.key(), expected_authority); + require_eq!( + sender_authority.key(), + expected_authority, + crate::error::TokenBridgeError::InvalidProgramSender + ); Ok(program_id) } None => Ok(sender_authority.key()), diff --git a/solana/programs/token-bridge/src/utils/token.rs b/solana/programs/token-bridge/src/utils/token.rs index 70a7717ac0..0de1da9717 100644 --- a/solana/programs/token-bridge/src/utils/token.rs +++ b/solana/programs/token-bridge/src/utils/token.rs @@ -1,36 +1,22 @@ use crate::{ constants::{MAX_DECIMALS, MINT_AUTHORITY_SEED_PREFIX}, - error::TokenBridgeError, zero_copy::Mint, }; use anchor_lang::prelude::*; -/// With an account meant to be a Token Program mint account, make sure it is not a mint that the -/// Token Bridge program controls. -pub fn require_native_mint(mint: &AccountInfo) -> Result<()> { - // This may be redundant because this mint account being owned by the Token Program is - // associated with either a transfer between two token accounts (which requires that this - // account be a valid mint) and deriving metadata PDA to create and update token metadata. - require_eq!( - *mint.owner, - anchor_spl::token::ID, - ErrorCode::ConstraintMintTokenProgram - ); - - // If there is a mint authority, make sure it is not the Token Bridge's mint authority, which - // controls burn and mint for its wrapped assets. - if let Some(mint_authority) = Mint::parse(&mint.try_borrow_data()?)?.mint_authority() { +/// Basically check whether the mint authority is the Token Bridge's mint authority. +/// +/// NOTE: This method does not guarantee that the mint is a mint created by the Token Bridge program +/// via `create_or_update_wrapped` instruction because someone can transfer mint authority for +/// another mint to the Token Bridge's mint authority. +pub fn is_wrapped_mint(mint: &Mint) -> bool { + if let Some(mint_authority) = mint.mint_authority() { let (token_bridge_mint_authority, _) = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED_PREFIX], &crate::ID); - require_keys_neq!( - mint_authority, - token_bridge_mint_authority, - TokenBridgeError::WrappedAsset - ); + mint_authority == token_bridge_mint_authority + } else { + false } - - // Done. - Ok(()) } /// Convenient trait to determine amount truncation for encoded token transfer amounts. diff --git a/solana/programs/token-bridge/src/utils/vaa.rs b/solana/programs/token-bridge/src/utils/vaa.rs index 9817bd97fd..07d21d7101 100644 --- a/solana/programs/token-bridge/src/utils/vaa.rs +++ b/solana/programs/token-bridge/src/utils/vaa.rs @@ -1,6 +1,6 @@ use crate::{error::TokenBridgeError, state::RegisteredEmitter, ID}; use anchor_lang::prelude::*; -use core_bridge_program::legacy::utils::LegacyAnchorized; +use core_bridge_program::{legacy::utils::LegacyAnchorized, sdk as core_bridge_sdk}; use wormhole_raw_vaas::token_bridge::TokenBridgeMessage; // Static list of invalid VAA Message accounts. @@ -15,7 +15,7 @@ const INVALID_POSTED_VAA_KEYS: [&str; 7] = [ ]; /// We disallow certain posted VAA accounts from being used to redeem Token Bridge transfers. -pub fn require_valid_posted_vaa_key(acc_key: &Pubkey) -> Result<()> { +pub fn require_valid_vaa_key(acc_key: &Pubkey) -> Result<()> { // IYKYK. require!( !INVALID_POSTED_VAA_KEYS.contains(&acc_key.to_string().as_str()), @@ -30,15 +30,14 @@ pub fn require_valid_posted_vaa_key(acc_key: &Pubkey) -> Result<()> { /// - Transfer (Payload ID == 1) /// - Attestation (Payload ID == 2) /// - Transfer with Message (Payload ID == 3) -pub fn require_valid_posted_token_bridge_vaa<'ctx>( - vaa_acc_key: &Pubkey, - vaa: &core_bridge_program::zero_copy::PostedVaaV1<'ctx>, +pub fn require_valid_token_bridge_vaa<'ctx>( + vaa_acc_key: &'ctx Pubkey, + vaa: &'ctx core_bridge_sdk::VaaAccount<'ctx>, registered_emitter: &'ctx Account<'_, LegacyAnchorized<0, RegisteredEmitter>>, ) -> Result> { - require_valid_posted_vaa_key(vaa_acc_key)?; + require_valid_vaa_key(vaa_acc_key)?; - let emitter_chain = vaa.emitter_chain(); - let emitter_address = vaa.emitter_address(); + let (emitter_address, emitter_chain, _) = vaa.try_emitter_info()?; let emitter_key = registered_emitter.key(); // Validate registered emitter PDA address. @@ -66,6 +65,6 @@ pub fn require_valid_posted_token_bridge_vaa<'ctx>( } // Make sure we are working with a valid Token Bridge message. - TokenBridgeMessage::parse(vaa.payload()) + TokenBridgeMessage::try_from(vaa.try_payload().unwrap()) .map_err(|_| error!(TokenBridgeError::CannotParseMessage)) } diff --git a/solana/programs/token-bridge/src/zero_copy/mint.rs b/solana/programs/token-bridge/src/zero_copy/mint.rs index 742bfb0639..b3d42ec372 100644 --- a/solana/programs/token-bridge/src/zero_copy/mint.rs +++ b/solana/programs/token-bridge/src/zero_copy/mint.rs @@ -1,37 +1,48 @@ +use std::cell::Ref; + use anchor_lang::prelude::{ - err, error, require, require_eq, require_keys_eq, ErrorCode, Pubkey, Result, + error, require, require_eq, require_keys_eq, AccountInfo, ErrorCode, Pubkey, Result, }; use crate::utils::TruncateAmount; /// This implements a zero-copy deserialization for the Token Program's mint account. All struct /// field doc strings are shamelessly copied from the SPL Token docs. -pub struct Mint<'a>(&'a [u8]); +pub struct Mint<'a>(Ref<'a, &'a mut [u8]>); impl<'a> Mint<'a> { /// Optional authority used to mint new tokens. The mint authority may only be provided during /// mint creation. If no mint authority is present then the mint has a fixed supply and no /// further tokens may be minted. pub fn mint_authority(&self) -> Option { - match u32::from_le_bytes(self.0[..4].try_into().unwrap()) { - 0 => None, + match self.0[..4] { + [0, 0, 0, 0] => None, _ => Some(Pubkey::try_from(&self.0[4..36]).unwrap()), } } - pub fn require_mint_authority( - acc_data: &'a [u8], - mint_authority: Option<&Pubkey>, - ) -> Result<()> { - match (Self::parse(acc_data)?.mint_authority(), mint_authority) { - (Some(actual), Some(expected)) => { - require_keys_eq!(actual, *expected, ErrorCode::ConstraintMintMintAuthority); - Ok(()) - } - (None, None) => Ok(()), - _ => err!(ErrorCode::ConstraintMintMintAuthority), - } - } + // pub fn require_mint_authority( + // acc_info: &'a AccountInfo, + // mint_authority: Option<&Pubkey>, + // ) -> Result<()> { + // let mint = Self::parse(acc_info)?; + // match mint.mint_authority() { + // Some(actual) => { + // let expected = + // mint_authority.ok_or(error!(ErrorCode::ConstraintMintMintAuthority))?; + // require_keys_eq!(actual, *expected, ErrorCode::ConstraintMintMintAuthority); + // } + // None => { + // require!( + // mint_authority.is_none(), + // ErrorCode::ConstraintMintMintAuthority + // ); + // } + // } + + // // Done. + // Ok(()) + // } /// Total supply of tokens. pub fn supply(&self) -> u64 { @@ -50,20 +61,29 @@ impl<'a> Mint<'a> { /// Optional authority to freeze token accounts. pub fn freeze_authority(&self) -> Option { - match u32::from_le_bytes(self.0[46..50].try_into().unwrap()) { - 0 => None, + match self.0[46..50] { + [0, 0, 0, 0] => None, _ => Some(Pubkey::try_from(&self.0[50..82]).unwrap()), } } +} + +impl<'a> core_bridge_program::sdk::LoadZeroCopy<'a> for Mint<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!( + *acc_info.owner, + anchor_spl::token::ID, + ErrorCode::ConstraintTokenTokenProgram + ); - pub fn parse(span: &'a [u8]) -> Result { + let data = acc_info.try_borrow_data()?; require_eq!( - span.len(), + data.len(), anchor_spl::token::Mint::LEN, ErrorCode::AccountDidNotDeserialize ); - let mint = Self(span); + let mint = Self(data); require!(mint.is_initialized(), ErrorCode::AccountNotInitialized); Ok(mint) diff --git a/solana/programs/token-bridge/src/zero_copy/token_account.rs b/solana/programs/token-bridge/src/zero_copy/token_account.rs index b05d1d5da9..120d01fddf 100644 --- a/solana/programs/token-bridge/src/zero_copy/token_account.rs +++ b/solana/programs/token-bridge/src/zero_copy/token_account.rs @@ -1,12 +1,14 @@ +use std::cell::Ref; + use anchor_lang::prelude::{ - error, require, require_eq, require_keys_eq, ErrorCode, Pubkey, Result, + error, require, require_eq, require_keys_eq, AccountInfo, ErrorCode, Pubkey, Result, }; use anchor_spl::token::spl_token::state; use solana_program::program_pack::Pack; /// This implements a zero-copy deserialization for the Token Program's token account. All struct /// field doc strings are shamelessly copied from the SPL Token docs. -pub struct TokenAccount<'a>(&'a [u8]); +pub struct TokenAccount<'a>(Ref<'a, &'a mut [u8]>); impl<'a> TokenAccount<'a> { /// The mint associated with this account @@ -14,25 +16,11 @@ impl<'a> TokenAccount<'a> { Pubkey::try_from(&self.0[0..32]).unwrap() } - pub fn require_mint(acc_data: &'a [u8], mint: &Pubkey) -> Result<()> { - let token = Self::parse(acc_data)?; - require_keys_eq!(token.mint(), *mint, ErrorCode::ConstraintTokenMint); - - Ok(()) - } - /// The owner of this account. pub fn owner(&self) -> Pubkey { Pubkey::try_from(&self.0[32..64]).unwrap() } - pub fn require_owner(acc_data: &'a [u8], owner: &Pubkey) -> Result<()> { - let token = Self::parse(acc_data)?; - require_keys_eq!(token.owner(), *owner, ErrorCode::ConstraintTokenOwner); - - Ok(()) - } - /// The amount of tokens this account holds. pub fn amount(&self) -> u64 { u64::from_le_bytes(self.0[64..72].try_into().unwrap()) @@ -41,8 +29,8 @@ impl<'a> TokenAccount<'a> { /// If `delegate` is `Some` then `delegated_amount` represents /// the amount authorized by the delegate pub fn delegate(&self) -> Option { - match u32::from_le_bytes(self.0[72..76].try_into().unwrap()) { - 0 => None, + match self.0[72..76] { + [0, 0, 0, 0] => None, _ => Some(Pubkey::try_from(&self.0[76..108]).unwrap()), } } @@ -79,15 +67,24 @@ impl<'a> TokenAccount<'a> { _ => Some(Pubkey::try_from(&self.0[133..165]).unwrap()), } } +} + +impl<'a> core_bridge_program::sdk::LoadZeroCopy<'a> for TokenAccount<'a> { + fn load(acc_info: &'a AccountInfo) -> Result { + require_keys_eq!( + *acc_info.owner, + anchor_spl::token::ID, + ErrorCode::ConstraintTokenTokenProgram + ); - pub fn parse(span: &'a [u8]) -> Result { + let data = acc_info.try_borrow_data()?; require_eq!( - span.len(), + data.len(), state::Account::LEN, ErrorCode::AccountDidNotDeserialize ); - let token = Self(span); + let token = Self(data); require!( token.state() != state::AccountState::Uninitialized, ErrorCode::AccountNotInitialized diff --git a/solana/scripts/prune_idl_types.ts b/solana/scripts/prune_idl_types.ts new file mode 100644 index 0000000000..ea2544ab26 --- /dev/null +++ b/solana/scripts/prune_idl_types.ts @@ -0,0 +1,51 @@ +import * as fs from "fs"; + +const ROOT = `${__dirname}/..`; +const CORE_BRIDGE_IDL = `${ROOT}/target/idl/wormhole_core_bridge_solana.json`; +const CORE_BRIDGE_TYPES = `${ROOT}/target/types/wormhole_core_bridge_solana.ts`; + +const IGNORE_TYPES = ['"name": "MessageAccount"', '"name": "VaaAccount"', '"name": "VaaVersion"']; + +main(); + +function main() { + if (!fs.existsSync(CORE_BRIDGE_IDL)) { + throw new Error("Core Bridge IDL non-existent"); + } + if (!fs.existsSync(CORE_BRIDGE_TYPES)) { + throw new Error("Core Bridge types non-existent"); + } + + const idl = fs.readFileSync(CORE_BRIDGE_IDL, "utf8").split("\n"); + const types = fs.readFileSync(CORE_BRIDGE_TYPES, "utf8").split("\n"); + for (const matchStr of IGNORE_TYPES) { + while (spliceType(idl, matchStr)); + while (spliceType(types, matchStr)); + } + fs.writeFileSync(CORE_BRIDGE_IDL, idl.join("\n"), "utf8"); + fs.writeFileSync(CORE_BRIDGE_TYPES, types.join("\n"), "utf8"); +} + +function spliceType(lines: string[], matchStr: string) { + let lineNumber = 0; + let start = -1; + let spaces = -1; + for (const line of lines) { + if (line.includes(matchStr)) { + start = lineNumber - 1; + spaces = line.indexOf('"') - 2; + } else if (start > -1) { + if (line == "}".padStart(spaces + 1, " ")) { + lines[start - 1] = lines[start - 1].replace("},", "}"); + lines.splice(start, lineNumber - start + 1); + return true; + } else if (line == "},".padStart(spaces + 2, " ")) { + lines.splice(start, lineNumber - start + 1); + return true; + } + } + ++lineNumber; + } + + return false; +} diff --git a/solana/scripts/tsconfig.json b/solana/scripts/tsconfig.json new file mode 100644 index 0000000000..d72b66283e --- /dev/null +++ b/solana/scripts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["es6"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "resolveJsonModule": true, + "sourceMap": true, + "inlineSourceMap": false, + "inlineSources": true, + "declaration": false, + "noEmit": false, + "outDir": "./.ts-node" + } +} diff --git a/solana/tests/01__coreBridge/006__postMessage.ts b/solana/tests/01__coreBridge/006__postMessage.ts index 0071e64041..32cdc6a40e 100644 --- a/solana/tests/01__coreBridge/006__postMessage.ts +++ b/solana/tests/01__coreBridge/006__postMessage.ts @@ -5,8 +5,10 @@ import { createIfNeeded, expectDeepEqual, expectIxErr, + expectIxOk, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; +import { expect } from "chai"; describe("Core Bridge -- Legacy Instruction: Post Message", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -102,6 +104,76 @@ describe("Core Bridge -- Legacy Instruction: Post Message", () => { new anchor.BN(0) ); }); + + it("Invoke `post_message` with System program at Index == 8", async () => { + const emitter = anchor.web3.Keypair.generate(); + const message = anchor.web3.Keypair.generate(); + const forkMessage = anchor.web3.Keypair.generate(); + + const transferFeeIx = await coreBridge.transferMessageFeeIx(program, payer.publicKey); + const forkTransferFeeIx = await coreBridge.transferMessageFeeIx( + forkedProgram, + payer.publicKey + ); + + const ix = coreBridge.legacyPostMessageIx( + program, + { payer: payer.publicKey, message: message.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expectDeepEqual(ix.keys[7].pubkey, anchor.web3.SystemProgram.programId); + ix.keys[7].pubkey = ix.keys[8].pubkey; + ix.keys[8].pubkey = anchor.web3.SystemProgram.programId; + + const forkIx = coreBridge.legacyPostMessageIx( + forkedProgram, + { payer: payer.publicKey, message: forkMessage.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expectDeepEqual(forkIx.keys[7].pubkey, anchor.web3.SystemProgram.programId); + forkIx.keys[7].pubkey = forkIx.keys[8].pubkey; + forkIx.keys[8].pubkey = anchor.web3.SystemProgram.programId; + + await expectIxOk( + connection, + [transferFeeIx, forkTransferFeeIx, ix, forkIx], + [payer, emitter, message, forkMessage] + ); + }); + + it("Invoke `post_message` with Num Accounts == 8", async () => { + const emitter = anchor.web3.Keypair.generate(); + const message = anchor.web3.Keypair.generate(); + const forkMessage = anchor.web3.Keypair.generate(); + + const transferFeeIx = await coreBridge.transferMessageFeeIx(program, payer.publicKey); + const forkTransferFeeIx = await coreBridge.transferMessageFeeIx( + forkedProgram, + payer.publicKey + ); + + const ix = coreBridge.legacyPostMessageIx( + program, + { payer: payer.publicKey, message: message.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expect(ix.keys).has.length(9); + ix.keys.pop(); + + const forkIx = coreBridge.legacyPostMessageIx( + forkedProgram, + { payer: payer.publicKey, message: forkMessage.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expect(forkIx.keys).has.length(9); + forkIx.keys.pop(); + + await expectIxOk( + connection, + [transferFeeIx, forkTransferFeeIx, ix, forkIx], + [payer, emitter, message, forkMessage] + ); + }); }); describe("New implementation", () => { diff --git a/solana/tests/01__coreBridge/008__postMessageUnreliable.ts b/solana/tests/01__coreBridge/008__postMessageUnreliable.ts index 9a9044a265..2749d34401 100644 --- a/solana/tests/01__coreBridge/008__postMessageUnreliable.ts +++ b/solana/tests/01__coreBridge/008__postMessageUnreliable.ts @@ -1,6 +1,13 @@ import * as anchor from "@coral-xyz/anchor"; import { expect } from "chai"; -import { InvalidAccountConfig, createIfNeeded, expectIxErr, expectIxOkDetails } from "../helpers"; +import { + InvalidAccountConfig, + createIfNeeded, + expectDeepEqual, + expectIxErr, + expectIxOk, + expectIxOkDetails, +} from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import { transferMessageFeeIx } from "../helpers/coreBridge/utils"; @@ -411,6 +418,76 @@ describe("Core Bridge -- Instruction: Post Message Unreliable", () => { ); expect(feeCollectorData.lamports).to.equal(forkFeeCollectorData.lamports); }); + + it("Invoke `post_message_unreliable` with System program at Index == 8", async () => { + const emitter = anchor.web3.Keypair.generate(); + const message = anchor.web3.Keypair.generate(); + const forkMessage = anchor.web3.Keypair.generate(); + + const transferFeeIx = await coreBridge.transferMessageFeeIx(program, payer.publicKey); + const forkTransferFeeIx = await coreBridge.transferMessageFeeIx( + forkedProgram, + payer.publicKey + ); + + const ix = coreBridge.legacyPostMessageUnreliableIx( + program, + { payer: payer.publicKey, message: message.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expectDeepEqual(ix.keys[7].pubkey, anchor.web3.SystemProgram.programId); + ix.keys[7].pubkey = ix.keys[8].pubkey; + ix.keys[8].pubkey = anchor.web3.SystemProgram.programId; + + const forkIx = coreBridge.legacyPostMessageUnreliableIx( + forkedProgram, + { payer: payer.publicKey, message: forkMessage.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expectDeepEqual(forkIx.keys[7].pubkey, anchor.web3.SystemProgram.programId); + forkIx.keys[7].pubkey = forkIx.keys[8].pubkey; + forkIx.keys[8].pubkey = anchor.web3.SystemProgram.programId; + + await expectIxOk( + connection, + [transferFeeIx, forkTransferFeeIx, ix, forkIx], + [payer, emitter, message, forkMessage] + ); + }); + + it("Invoke `post_message_unreliable` with Num Accounts == 8", async () => { + const emitter = anchor.web3.Keypair.generate(); + const message = anchor.web3.Keypair.generate(); + const forkMessage = anchor.web3.Keypair.generate(); + + const transferFeeIx = await coreBridge.transferMessageFeeIx(program, payer.publicKey); + const forkTransferFeeIx = await coreBridge.transferMessageFeeIx( + forkedProgram, + payer.publicKey + ); + + const ix = coreBridge.legacyPostMessageUnreliableIx( + program, + { payer: payer.publicKey, message: message.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expect(ix.keys).has.length(9); + ix.keys.pop(); + + const forkIx = coreBridge.legacyPostMessageUnreliableIx( + forkedProgram, + { payer: payer.publicKey, message: forkMessage.publicKey, emitter: emitter.publicKey }, + defaultArgs() + ); + expect(forkIx.keys).has.length(9); + forkIx.keys.pop(); + + await expectIxOk( + connection, + [transferFeeIx, forkTransferFeeIx, ix, forkIx], + [payer, emitter, message, forkMessage] + ); + }); }); describe("New implementation", () => { diff --git a/solana/tests/01__coreBridge/010__setMessageFee.ts b/solana/tests/01__coreBridge/010__setMessageFee.ts index c8e67fe24a..96d499c4fa 100644 --- a/solana/tests/01__coreBridge/010__setMessageFee.ts +++ b/solana/tests/01__coreBridge/010__setMessageFee.ts @@ -12,9 +12,9 @@ import { invokeVerifySignaturesAndPostVaa, parallelPostVaa, createInvalidCoreGovernanceVaaFromEth, + GOVERNANCE_EMITTER_ADDRESS, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; // Mock governance emitter and guardian. const GUARDIAN_SET_INDEX = 0; @@ -50,8 +50,6 @@ describe("Core Bridge -- Legacy Instruction: Set Message Fee", () => { label: "claim", contextName: "claim", errorMsg: "ConstraintSeeds", - dataLength: 1, - owner: program.programId, }, ]; diff --git a/solana/tests/01__coreBridge/012__transferFees.ts b/solana/tests/01__coreBridge/012__transferFees.ts index 57ef1ed1db..7219d17eb0 100644 --- a/solana/tests/01__coreBridge/012__transferFees.ts +++ b/solana/tests/01__coreBridge/012__transferFees.ts @@ -2,7 +2,6 @@ import { parseVaa } from "@certusone/wormhole-sdk"; import { GovernanceEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; import { expect } from "chai"; -import { expectIxOk } from "../../old-tests/helpers"; import { ETHEREUM_DEADBEEF_TOKEN_ADDRESS, GUARDIAN_KEYS, @@ -13,9 +12,10 @@ import { expectIxOkDetails, invokeVerifySignaturesAndPostVaa, parallelPostVaa, + expectIxOk, + GOVERNANCE_EMITTER_ADDRESS, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; // Mock governance emitter and guardian. const GUARDIAN_SET_INDEX = 0; @@ -51,8 +51,6 @@ describe("Core Bridge -- Legacy Instruction: Transfer Fees", () => { label: "claim", contextName: "claim", errorMsg: "ConstraintSeeds", - dataLength: 1, - owner: program.programId, }, ]; @@ -94,7 +92,7 @@ describe("Core Bridge -- Legacy Instruction: Transfer Fees", () => { forkedProgram, { payer: payer.publicKey, - recipient: recipient, + recipient, }, new anchor.BN(amount), payer @@ -122,20 +120,32 @@ describe("Core Bridge -- Legacy Instruction: Transfer Fees", () => { expect(feeCollectorData!.lamports).to.equal(forkFeeCollectorData!.lamports); // Save the signed VAA for later. + localVariables.set("amount", amount); localVariables.set("signedVaa", signedVaa); + localVariables.set("recipient", recipient); }); }); describe("New implementation", () => { it("Cannot Invoke `transfer_fees` with Same VAA", async () => { + const amount = localVariables.get("amount") as number; const signedVaa = localVariables.get("signedVaa") as Buffer; + const recipient = localVariables.get("recipient") as anchor.web3.PublicKey; + + const transferIx = anchor.web3.SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: coreBridge.feeCollectorPda(program.programId), + lamports: amount, + }); + //await expectIxOk(connection, [transferIx], [payer]); await expectIxErr( connection, [ + transferIx, coreBridge.legacyTransferFeesIx( program, - { payer: payer.publicKey, recipient: anchor.web3.Keypair.generate().publicKey }, + { payer: payer.publicKey, recipient }, parseVaa(signedVaa) ), ], diff --git a/solana/tests/01__coreBridge/014__guardianSetUpdate.ts b/solana/tests/01__coreBridge/014__guardianSetUpdate.ts index 4713ac8914..85e2581aeb 100644 --- a/solana/tests/01__coreBridge/014__guardianSetUpdate.ts +++ b/solana/tests/01__coreBridge/014__guardianSetUpdate.ts @@ -20,9 +20,9 @@ import { parallelPostVaa, range, sleep, + GOVERNANCE_EMITTER_ADDRESS, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; // Mock governance emitter and guardian. const GUARDIAN_SET_INDEX = 0; @@ -59,8 +59,6 @@ describe("Core Bridge -- Legacy Instruction: Guardian Set Update", () => { label: "claim", contextName: "claim", errorMsg: "ConstraintSeeds", - dataLength: 1, - owner: program.programId, }, ]; @@ -317,7 +315,7 @@ describe("Core Bridge -- Legacy Instruction: Guardian Set Update", () => { connection, [sigVerifyIx, verifyIx], [payer, signatureSet], - "PostVaaGuardianSetExpired" + "GuardianSetExpired" ); }); diff --git a/solana/tests/01__coreBridge/098__upgradeContract.ts b/solana/tests/01__coreBridge/098__upgradeContract.ts index ce371b5320..7fe87da45c 100644 --- a/solana/tests/01__coreBridge/098__upgradeContract.ts +++ b/solana/tests/01__coreBridge/098__upgradeContract.ts @@ -4,17 +4,15 @@ import * as anchor from "@coral-xyz/anchor"; import { execSync } from "child_process"; import * as fs from "fs"; import { + ETHEREUM_DEADBEEF_TOKEN_ADDRESS, + GOVERNANCE_EMITTER_ADDRESS, GUARDIAN_KEYS, expectIxErr, expectIxOk, invokeVerifySignaturesAndPostVaa, loadProgramBpf, - ETHEREUM_DEADBEEF_TOKEN_ADDRESS, - createAccountIx, - BPF_LOADER_UPGRADEABLE_PROGRAM_ID, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; const ARTIFACTS_PATH = `${__dirname}/../artifacts/wormhole_core_bridge_solana.so`; @@ -73,6 +71,18 @@ describe("Core Bridge -- Legacy Instruction: Upgrade Contract", () => { const signedVaa = defaultVaa(implementation); await sendTx(program, payer, signedVaa); + }); + + it("Deploy Same Implementation and Invoke `upgrade_contract` with Another VAA", async () => { + const implementation = loadProgramBpf( + ARTIFACTS_PATH, + coreBridge.upgradeAuthorityPda(program.programId) + ); + + // Create the signed VAA. + const signedVaa = defaultVaa(implementation); + + await sendTx(program, payer, signedVaa); // Save for later. localVariables.set("signedVaa", signedVaa); @@ -96,18 +106,6 @@ describe("Core Bridge -- Legacy Instruction: Upgrade Contract", () => { ); }); - it("Deploy Same Implementation and Invoke `upgrade_contract` with Another VAA", async () => { - const implementation = loadProgramBpf( - ARTIFACTS_PATH, - coreBridge.upgradeAuthorityPda(program.programId) - ); - - // Create the signed VAA. - const signedVaa = defaultVaa(implementation); - - await sendTx(program, payer, signedVaa); - }); - it("Cannot Invoke `upgrade_contract` with Implementation Mismatch", async () => { const implementation = anchor.web3.Keypair.generate().publicKey; const anotherImplementation = anchor.web3.Keypair.generate().publicKey; diff --git a/solana/tests/01__coreBridge/102__processMessageV1.ts b/solana/tests/01__coreBridge/102__processMessageV1.ts index 5f8c522cbc..0b326a4a32 100644 --- a/solana/tests/01__coreBridge/102__processMessageV1.ts +++ b/solana/tests/01__coreBridge/102__processMessageV1.ts @@ -17,79 +17,60 @@ describe("Core Bridge -- Instruction: Process Message V1", () => { describe("Invalid Interaction", () => { const messageSize = 69; - it("Cannot Invoke `process_message_v1` with Different Emitter Authority", async () => { + it("Cannot Invoke `write_message_v1` with Different Emitter Authority", async () => { const someoneElse = anchor.web3.Keypair.generate(); const { draftMessage } = await initMessageV1(program, payer, messageSize); - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: someoneElse.publicKey, draftMessage, - closeAccountDestination: null, }, - { write: { index: 0, data: Buffer.from("Nope.") } } + { index: 0, data: Buffer.from("Nope.") } ); await expectIxErr(connection, [ix], [payer, someoneElse], "EmitterAuthorityMismatch"); }); - it("Cannot Invoke `process_message_v1` to Close Draft Message without `close_account_destination`", async () => { + it("Cannot Invoke `write_message_v1` with Nonsensical Index", async () => { const { draftMessage, emitterAuthority } = await initMessageV1(program, payer, messageSize); - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage, - closeAccountDestination: null, }, - { closeMessageAccount: {} } - ); - await expectIxErr(connection, [ix], [payer, emitterAuthority], "AccountNotEnoughKeys"); - }); - - it("Cannot Invoke `process_message_v1` with Nonsensical Index", async () => { - const { draftMessage, emitterAuthority } = await initMessageV1(program, payer, messageSize); - - const ix = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage, - closeAccountDestination: null, - }, - { write: { index: messageSize, data: Buffer.from("Nope.") } } + { index: messageSize, data: Buffer.from("Nope.") } ); await expectIxErr(connection, [ix], [payer, emitterAuthority], "DataOverflow"); }); - it("Cannot Invoke `process_message_v1` with Too Much Data", async () => { + it("Cannot Invoke `write_message_v1` with Too Much Data", async () => { const { draftMessage, emitterAuthority } = await initMessageV1(program, payer, messageSize); - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage, - closeAccountDestination: null, }, - { write: { index: 0, data: Buffer.alloc(messageSize + 1, "Nope.") } } + { index: 0, data: Buffer.alloc(messageSize + 1, "Nope.") } ); await expectIxErr(connection, [ix], [payer, emitterAuthority], "DataOverflow"); }); - it("Cannot Invoke `process_message_v1` with No Data", async () => { + it("Cannot Invoke `write_message_v1` with No Data", async () => { const { draftMessage, emitterAuthority } = await initMessageV1(program, payer, messageSize); - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage, - closeAccountDestination: null, }, - { write: { index: 0, data: Buffer.alloc(0) } } + { index: 0, data: Buffer.alloc(0) } ); await expectIxErr(connection, [ix], [payer, emitterAuthority], "InvalidInstructionArgument"); }); @@ -110,18 +91,17 @@ describe("Core Bridge -- Instruction: Process Message V1", () => { for (let start = 0; start < messageSize; start += chunkSize) { const end = Math.min(start + chunkSize, messageSize); - it(`Invoke \`process_message_v1\` to Write (Range: ${start}..${end})`, async () => { + it(`Invoke \`write_message_v1\` to Write (Range: ${start}..${end})`, async () => { const emitterAuthority = localVariables.get("emitterAuthority") as anchor.web3.Keypair; const draftMessage = localVariables.get("draftMessage") as anchor.web3.PublicKey; - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage, - closeAccountDestination: null, }, - { write: { index: start, data: message.subarray(start, end) } } + { index: start, data: message.subarray(start, end) } ); await expectIxOk(connection, [ix], [payer, emitterAuthority]); @@ -147,55 +127,44 @@ describe("Core Bridge -- Instruction: Process Message V1", () => { }); } - it("Invoke `process_message_v1` to Finalize", async () => { + it("Invoke `finalize_message_v1` to Finalize", async () => { const emitterAuthority = localVariables.get("emitterAuthority") as anchor.web3.Keypair; const draftMessage = localVariables.get("draftMessage") as anchor.web3.PublicKey; - const ix = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage, - closeAccountDestination: null, - }, - { finalize: {} } - ); + const ix = await coreBridge.finalizeMessageV1Ix(program, { + emitterAuthority: emitterAuthority.publicKey, + draftMessage, + }); await expectIxOk(connection, [ix], [payer, emitterAuthority]); }); - it("Cannot Invoke `process_message_v1` to Finalize Again", async () => { + it("Cannot Invoke `finalize_message_v1` to Finalize Again", async () => { const emitterAuthority = localVariables.get("emitterAuthority") as anchor.web3.Keypair; const draftMessage = localVariables.get("draftMessage") as anchor.web3.PublicKey; - const ix = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage, - closeAccountDestination: null, - }, - { finalize: {} } - ); + const ix = await coreBridge.finalizeMessageV1Ix(program, { + emitterAuthority: emitterAuthority.publicKey, + draftMessage, + }); await expectIxErr(connection, [ix], [payer, emitterAuthority], "NotInWritingStatus"); }); - it("Cannot Invoke `process_message_v1` to Write After Finalize", async () => { + it("Cannot Invoke `write_message_v1` to Write After Finalize", async () => { const emitterAuthority = localVariables.get("emitterAuthority") as anchor.web3.Keypair; const draftMessage = localVariables.get("draftMessage") as anchor.web3.PublicKey; - const ix = await coreBridge.processMessageV1Ix( + const ix = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage, - closeAccountDestination: null, }, - { finalize: {} } + { index: 0, data: Buffer.from("Nope.") } ); await expectIxErr(connection, [ix], [payer, emitterAuthority], "NotInWritingStatus"); }); - it("Invoke `process_message_v1` to Close Message", async () => { + it("Invoke `close_message_v1` to Close Message", async () => { const emitterAuthority = localVariables.get("emitterAuthority") as anchor.web3.Keypair; const draftMessage = localVariables.get("draftMessage") as anchor.web3.PublicKey; @@ -206,15 +175,11 @@ describe("Core Bridge -- Instruction: Process Message V1", () => { .getAccountInfo(draftMessage) .then((acct) => acct.lamports); - const ix = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage, - closeAccountDestination, - }, - { closeMessageAccount: {} } - ); + const ix = await coreBridge.closeMessageV1Ix(program, { + emitterAuthority: emitterAuthority.publicKey, + draftMessage, + closeAccountDestination, + }); await expectIxOk(connection, [ix], [payer, emitterAuthority]); const balanceAfter = await connection.getBalance(closeAccountDestination); diff --git a/solana/tests/01__coreBridge/104__postMessage.ts b/solana/tests/01__coreBridge/104__postMessage.ts index 928bb1b9db..c55e195338 100644 --- a/solana/tests/01__coreBridge/104__postMessage.ts +++ b/solana/tests/01__coreBridge/104__postMessage.ts @@ -1,11 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; -import { - createAccountIx, - expectDeepEqual, - expectIxErr, - expectIxOk, - expectIxOkDetails, -} from "../helpers"; +import { createAccountIx, expectIxErr, expectIxOk } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; // Test variables. @@ -82,6 +76,46 @@ describe("Core Bridge -- Legacy Instruction: Post Message (Prepared)", () => { ); await expectIxErr(connection, [ix], [payer], "MessageAlreadyPublished"); }); + + it("Invoke `post_message` With 30Kb Payload", async () => { + const { draftMessage, createIx, initIx } = await createAndInitMessageV1Ixes( + program, + payer, + 30 * 1_024, + payer, + 420, // nonce + "confirmed" + ); + + const finalizeIx = await coreBridge.finalizeMessageV1Ix(program, { + emitterAuthority: payer.publicKey, + draftMessage: draftMessage.publicKey, + }); + + const transferFeeIx = await coreBridge.transferMessageFeeIx(program, payer.publicKey); + + const emitterSequence = coreBridge.EmitterSequence.address( + program.programId, + payer.publicKey + ); + + const postIx = coreBridge.legacyPostMessageIx( + program, + { + message: draftMessage.publicKey, + emitter: null, + emitterSequence, + payer: payer.publicKey, + }, + { nonce: 0, commitment: "confirmed", payload: Buffer.alloc(0) }, + { message: false } + ); + await expectIxOk( + connection, + [createIx, initIx, finalizeIx, transferFeeIx, postIx], + [payer, draftMessage] + ); + }); }); }); @@ -133,29 +167,21 @@ async function everythingOk( return { message: draftMessage, emitter: emitterAuthority.publicKey }; } -async function initAndProcessMessageV1( +async function createAndInitMessageV1Ixes( program: coreBridge.CoreBridgeProgram, payer: anchor.web3.Keypair, - payload: Buffer, + messageLen: number, + emitterAuthority: anchor.web3.Keypair, nonce: number, - commitment: anchor.web3.Commitment, - emitterAuthority?: anchor.web3.Keypair + commitment: anchor.web3.Commitment ) { - if (emitterAuthority === undefined) { - emitterAuthority = anchor.web3.Keypair.generate(); - } - - const messageLen = payload.length; - - const connection = program.provider.connection; - const draftMessage = anchor.web3.Keypair.generate(); const createIx = await createAccountIx( program.provider.connection, program.programId, payer, draftMessage, - 95 + payload.length + 95 + messageLen ); const initIx = await coreBridge.initMessageV1Ix( @@ -164,15 +190,42 @@ async function initAndProcessMessageV1( { nonce, commitment, cpiProgramId: null } ); + return { draftMessage, createIx, initIx }; +} + +async function initAndProcessMessageV1( + program: coreBridge.CoreBridgeProgram, + payer: anchor.web3.Keypair, + payload: Buffer, + nonce: number, + commitment: anchor.web3.Commitment, + emitterAuthority?: anchor.web3.Keypair +) { + if (emitterAuthority === undefined) { + emitterAuthority = anchor.web3.Keypair.generate(); + } + + const messageLen = payload.length; + + const { draftMessage, createIx, initIx } = await createAndInitMessageV1Ixes( + program, + payer, + messageLen, + emitterAuthority, + nonce, + commitment + ); + + const connection = program.provider.connection; + const endAfterInit = 740; - const firstProcessIx = await coreBridge.processMessageV1Ix( + const firstProcessIx = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage: draftMessage.publicKey, - closeAccountDestination: null, }, - { write: { index: 0, data: payload.subarray(0, endAfterInit) } } + { index: 0, data: payload.subarray(0, endAfterInit) } ); if (messageLen > endAfterInit) { @@ -186,41 +239,30 @@ async function initAndProcessMessageV1( for (let start = endAfterInit; start < messageLen; start += chunkSize) { const end = Math.min(start + chunkSize, messageLen); - const writeIx = await coreBridge.processMessageV1Ix( + const writeIx = await coreBridge.writeMessageV1Ix( program, { emitterAuthority: emitterAuthority.publicKey, draftMessage: draftMessage.publicKey, - closeAccountDestination: null, }, - { write: { index: start, data: payload.subarray(start, end) } } + { index: start, data: payload.subarray(start, end) } ); if (end == messageLen) { - const finalizeIx = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage: draftMessage.publicKey, - closeAccountDestination: null, - }, - { finalize: {} } - ); + const finalizeIx = await coreBridge.finalizeMessageV1Ix(program, { + emitterAuthority: emitterAuthority.publicKey, + draftMessage: draftMessage.publicKey, + }); await expectIxOk(connection, [writeIx, finalizeIx], [payer, emitterAuthority]); } else { await expectIxOk(connection, [writeIx], [payer, emitterAuthority]); } } } else { - const finalizeIx = await coreBridge.processMessageV1Ix( - program, - { - emitterAuthority: emitterAuthority.publicKey, - draftMessage: draftMessage.publicKey, - closeAccountDestination: null, - }, - { finalize: {} } - ); + const finalizeIx = await coreBridge.finalizeMessageV1Ix(program, { + emitterAuthority: emitterAuthority.publicKey, + draftMessage: draftMessage.publicKey, + }); await expectIxOk( connection, diff --git a/solana/tests/01__coreBridge/110__initEncodedVaa.ts b/solana/tests/01__coreBridge/110__initEncodedVaa.ts index 762aedf6d1..222a21f1d4 100644 --- a/solana/tests/01__coreBridge/110__initEncodedVaa.ts +++ b/solana/tests/01__coreBridge/110__initEncodedVaa.ts @@ -67,7 +67,7 @@ describe("Core Bridge -- Instruction: Init Encoded VAA", () => { expectDeepEqual(encodedVaaData, { status: coreBridge.ProcessingStatus.Writing, writeAuthority: payer.publicKey, - version: coreBridge.VaaVersion.Unset, + version: 0, buf: Buffer.alloc(vaaSize), }); diff --git a/solana/tests/01__coreBridge/112__processEncodedVaa.ts b/solana/tests/01__coreBridge/112__processEncodedVaa.ts index 9223ab7e14..ce2e6adf39 100644 --- a/solana/tests/01__coreBridge/112__processEncodedVaa.ts +++ b/solana/tests/01__coreBridge/112__processEncodedVaa.ts @@ -3,7 +3,6 @@ import { MockEmitter, MockGuardians, } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../../old-tests/helpers"; import * as anchor from "@coral-xyz/anchor"; import { ComputeBudgetProgram } from "@solana/web3.js"; import { expect } from "chai"; @@ -15,6 +14,7 @@ import { expectIxOk, processVaa, parallelPostVaa, + GOVERNANCE_EMITTER_ADDRESS, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import { parseVaa } from "@certusone/wormhole-sdk"; @@ -52,103 +52,90 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { const vaaSize = signedVaa.length; const chunkSize = 912; // Max that can fit in a transaction. - it("Cannot Invoke `process_encoded_vaa` with Different Write Authority (Write)", async () => { + it("Cannot Invoke `write_encoded_vaa` with Different Write Authority", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); const someoneElse = anchor.web3.Keypair.generate(); - const ix = await coreBridge.processEncodedVaaIx( + const ix = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: someoneElse.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: 0, data: Buffer.from("Nope.") } } + { index: 0, data: Buffer.from("Nope.") } ); await expectIxErr(connection, [ix], [payer, someoneElse], "WriteAuthorityMismatch"); }); - it("Cannot Invoke `process_encoded_vaa` with Different Write Authority (CloseVaaAccount)", async () => { + it("Cannot Invoke `close_encoded_vaa` with Different Write Authority", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); const someoneElse = anchor.web3.Keypair.generate(); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: someoneElse.publicKey, - encodedVaa, - guardianSet: null, - }, - { closeVaaAccount: {} } - ); + const ix = await coreBridge.closeEncodedVaaIx(program, { + writeAuthority: someoneElse.publicKey, + encodedVaa, + }); await expectIxErr(connection, [ix], [payer, someoneElse], "WriteAuthorityMismatch"); }); - it("Cannot Invoke `process_encoded_vaa` with Expired Guardian Set", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Expired Guardian Set", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); const expiredGuardianSetIndex = 0; expect(GUARDIAN_SET_INDEX).is.greaterThan(expiredGuardianSetIndex); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, expiredGuardianSetIndex), - }, - { write: { index: 0, data: Buffer.from("Nope.") } } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, expiredGuardianSetIndex), + }); await expectIxErr(connection, [ix], [payer], "GuardianSetExpired"); }); - it("Cannot Invoke `process_encoded_vaa` with Nonsensical Index", async () => { + it("Cannot Invoke `write_encoded_vaa` with Nonsensical Index", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); - const ix = await coreBridge.processEncodedVaaIx( + const ix = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: vaaSize, data: Buffer.from("Nope.") } } + { index: vaaSize, data: Buffer.from("Nope.") } ); await expectIxErr(connection, [ix], [payer], "DataOverflow"); }); - it("Cannot Invoke `process_encoded_vaa` with Too Much Data", async () => { + it("Cannot Invoke `write_encoded_vaa` with Too Much Data", async () => { const smallVaaSize = 69; const encodedVaa = await initEncodedVaa(program, payer, smallVaaSize); - const ix = await coreBridge.processEncodedVaaIx( + const ix = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: 0, data: Buffer.alloc(smallVaaSize + 1, "Nope.") } } + { index: 0, data: Buffer.alloc(smallVaaSize + 1, "Nope.") } ); await expectIxErr(connection, [ix], [payer], "DataOverflow"); }); - it("Cannot Invoke `process_encoded_vaa` with No Data", async () => { + it("Cannot Invoke `write_encoded_vaa` with No Data", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); - const ix = await coreBridge.processEncodedVaaIx( + const ix = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: 0, data: Buffer.alloc(0) } } + { index: 0, data: Buffer.alloc(0) } ); await expectIxErr(connection, [ix], [payer], "InvalidInstructionArgument"); }); - it("Cannot Invoke `process_encoded_vaa` with Invalid VAA Version", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Invalid VAA Version", async () => { let signedVaa = defaultVaa(); // Spoof the VAA version number. @@ -165,19 +152,15 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, ix], [payer], "InvalidVaaVersion"); }); - it("Cannot Invoke `process_encoded_vaa` with Non-Increasing Guardian Index", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Non-Increasing Guardian Index", async () => { let signedVaa = defaultVaa(); // Change the second guardian index to the same value as the first. @@ -194,19 +177,15 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, ix], [payer], "InvalidGuardianIndex"); }); - it("Cannot Invoke `process_encoded_vaa` with Non-Existent Guardian Index", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Non-Existent Guardian Index", async () => { let signedVaa = defaultVaa(); // Change the first guardian index to a value that doesn't exist in the @@ -224,19 +203,15 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, ix], [payer], "InvalidGuardianIndex"); }); - it("Cannot Invoke `process_encoded_vaa` with Invalid Guardian Key Recovery", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Invalid Guardian Key Recovery", async () => { let signedVaa = defaultVaa(); // Change the recovery key of the first signature to zero, this will cause the key @@ -254,19 +229,15 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, ix], [payer], "InvalidGuardianKeyRecovery"); }); - it("Cannot Invoke `process_encoded_vaa` with Invalid Signature", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` with Invalid Signature", async () => { let signedVaa = defaultVaa(); // Change the recovery key of the first signature to an invalid value, @@ -284,36 +255,27 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, ix], [payer], "InvalidSignature"); }); - it.skip("Cannot Invoke `process_encoded_vaa` to Verify Signatures on Nonsensical Encoded Vaa", async () => { + it.skip("Cannot Invoke `verify_encoded_vaa_v1` to Verify Signatures on Nonsensical Encoded Vaa", async () => { // TODO }); - it("Invoke `process_encoded_vaa` to Close Encoded VAA", async () => { + it("Invoke `close_encoded_vaa` to Close Encoded VAA", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); const balanceBefore = await connection.getBalance(payer.publicKey); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: null, - }, - { closeVaaAccount: {} } - ); + const ix = await coreBridge.closeEncodedVaaIx(program, { + writeAuthority: payer.publicKey, + encodedVaa, + }); await expectIxOk(connection, [ix], [payer]); const balanceAfter = await connection.getBalance(payer.publicKey); @@ -335,17 +297,16 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { for (let start = 0; start < vaaSize; start += chunkSize) { const end = Math.min(start + chunkSize, vaaSize); - it(`Invoke \`process_encoded_vaa\` to Write Part of VAA (Range: ${start}..${end})`, async () => { + it(`Invoke \`write_encoded_vaa\` to Write Part of VAA (Range: ${start}..${end})`, async () => { const encodedVaa = localVariables.get("encodedVaa") as anchor.web3.PublicKey; - const ix = await coreBridge.processEncodedVaaIx( + const ix = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: start, data: signedVaa.subarray(start, end) } } + { index: start, data: signedVaa.subarray(start, end) } ); await expectIxOk(connection, [ix], [payer]); @@ -356,72 +317,44 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { expectDeepEqual(encodedVaaData, { status: coreBridge.ProcessingStatus.Writing, writeAuthority: payer.publicKey, - version: coreBridge.VaaVersion.Unset, + version: 0, buf: expectedBuf, }); }); } - it("Cannot Invoke `process_encoded_vaa` to Verify Signatures without Guardian Set", async () => { - const encodedVaa = localVariables.get("encodedVaa") as anchor.web3.PublicKey; - - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: null, - }, - { verifySignaturesV1: {} } - ); - await expectIxErr(connection, [ix], [payer], "AccountNotEnoughKeys"); - }); - - it("Invoke `process_encoded_vaa` to Verify Signatures", async () => { + it("Invoke `verify_encoded_vaa_v1` to Verify Signatures", async () => { const encodedVaa = localVariables.get("encodedVaa") as anchor.web3.PublicKey; // This directive requires more than the usual 200k. const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxOk(connection, [computeIx, ix], [payer]); }); - it("Cannot Invoke `process_encoded_vaa` to Verify Signatures Again", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` to Verify Signatures Again", async () => { const encodedVaa = localVariables.get("encodedVaa") as anchor.web3.PublicKey; - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [ix], [payer], "VaaAlreadyVerified"); }); - it("Invoke `process_encoded_vaa` to Close Encoded VAA After Verifying Signatures", async () => { + it("Invoke `close_encoded_vaa` to Close Encoded VAA After Verifying Signatures", async () => { const encodedVaa = await initEncodedVaa(program, payer, vaaSize); - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: null, - }, - { closeVaaAccount: {} } - ); + const ix = await coreBridge.closeEncodedVaaIx(program, { + writeAuthority: payer.publicKey, + encodedVaa, + }); //const txDetails = await expectIxOkDetails(connection, [ix], [payer]); await expectIxOk(connection, [ix], [payer]); @@ -429,7 +362,7 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { expect(encodedVaaData).is.null; }); - it("Cannot Invoke `process_encoded_vaa` to Verify Signatures without Quorum", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` to Verify Signatures without Quorum", async () => { const guardianIndices = [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12]; expect(guardianIndices).has.length.lessThan((2 * guardians.signers.length) / 3); @@ -437,30 +370,25 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { const encodedVaa = await initEncodedVaa(program, payer, badVaa.length); const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }); - const writeIx = await coreBridge.processEncodedVaaIx( + const writeIx = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa, - guardianSet: null, }, - { write: { index: 0, data: badVaa } } + { index: 0, data: badVaa } ); - const verifyIx = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), - }, - { verifySignaturesV1: {} } - ); + const verifyIx = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, GUARDIAN_SET_INDEX), + }); await expectIxErr(connection, [computeIx, writeIx, verifyIx], [payer], "NoQuorum"); }); - it("Cannot Invoke `process_encoded_vaa` to Verify Signatures with Mismatching Guardian Set", async () => { + it("Cannot Invoke `verify_encoded_vaa_v1` to Verify Signatures with Mismatching Guardian Set", async () => { // Sign a VAA with the current guardian set. const encodedVaa = await processVaa( program, @@ -489,15 +417,11 @@ describe("Core Bridge -- Instruction: Process Encoded Vaa", () => { const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); // Create the instruction using the newest guardian set. - const ix = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa, - guardianSet: coreBridge.GuardianSet.address(program.programId, newGuardianSet), - }, - { verifySignaturesV1: {} } - ); + const ix = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + guardianSet: coreBridge.GuardianSet.address(program.programId, newGuardianSet), + }); await expectIxErr(connection, [computeIx, ix], [payer], "GuardianSetMismatch"); // Revert the guardian set by updating to the original set. diff --git a/solana/tests/01__coreBridge/114__postVaaV1.ts b/solana/tests/01__coreBridge/114__postVaaV1.ts index 375a207258..f1567cfaa1 100644 --- a/solana/tests/01__coreBridge/114__postVaaV1.ts +++ b/solana/tests/01__coreBridge/114__postVaaV1.ts @@ -35,84 +35,50 @@ describe("Core Bridge -- Instruction: Post VAA V1", () => { }); describe("Ok", () => { - it("Cannot Invoke `post_vaa_v1` without Correct Write Authority", async () => { - const signedVaa = defaultVaa(); - - const vaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); - - const someoneElse = anchor.web3.Keypair.generate(); - await transferLamports(connection, payer, someoneElse.publicKey); - - const ix = await coreBridge.postVaaV1Ix( - program, - { - writeAuthority: someoneElse.publicKey, - vaa, - }, - { tryOnce: {} } - ); - await expectIxErr(connection, [ix], [someoneElse], "ConstraintHasOne"); - }); - it("Cannot Invoke `post_vaa_v1` with Incorrect Post VAA PDA", async () => { const signedVaa = defaultVaa(); - const vaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); + const encodedVaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); - const ix = await coreBridge.postVaaV1Ix( - program, - { - writeAuthority: payer.publicKey, - vaa, - postedVaa: coreBridge.PostedVaaV1.address(program.programId, new Array(32)), - }, - { tryOnce: {} } - ); + const ix = await coreBridge.postVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + postedVaa: coreBridge.PostedVaaV1.address(program.programId, new Array(32)), + }); await expectIxErr(connection, [ix], [payer], "ConstraintSeeds"); }); it("Cannot Invoke `post_vaa_v1` with Unverified VAA", async () => { const signedVaa = defaultVaa(); - const vaa = await processVaa( + const encodedVaa = await processVaa( program, payer, signedVaa, GUARDIAN_SET_INDEX, false // verify ); - const vaaData = await coreBridge.EncodedVaa.fetch(program, vaa); + const vaaData = await coreBridge.EncodedVaa.fetch(program, encodedVaa); expect(vaaData.status).not.equals(coreBridge.ProcessingStatus.Verified); - const ix = await coreBridge.postVaaV1Ix( - program, - { - writeAuthority: payer.publicKey, - vaa, - }, - { tryOnce: {} } - ); - await expectIxErr(connection, [ix], [payer], "InvalidVaaVersion"); + const ix = await coreBridge.postVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + }); + await expectIxErr(connection, [ix], [payer], "UnverifiedVaa"); }); it("Invoke `post_vaa_v1`", async () => { const signedVaa = defaultVaa(); - const vaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); + const encodedVaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); - const ix = await coreBridge.postVaaV1Ix( - program, - { - writeAuthority: payer.publicKey, - vaa, - }, - { tryOnce: {} } - ); + const ix = await coreBridge.postVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + }); await expectIxOk(connection, [ix], [payer]); - const vaaAccount = await connection.getAccountInfo(vaa); - expect(vaaAccount).is.null; - const postedVaaData = await coreBridge.PostedVaaV1.fromPda( connection, program.programId, @@ -124,7 +90,7 @@ describe("Core Bridge -- Instruction: Post VAA V1", () => { signatureSet: anchor.web3.PublicKey.default, guardianSetIndex: GUARDIAN_SET_INDEX, nonce: 420, - sequence: new anchor.BN(3), + sequence: new anchor.BN(2), emitterChain: 69, emitterAddress: Array.from(Buffer.alloc(32, "deadbeef")), payload: Buffer.alloc(2 * 1_024, "Somebody set us up the bomb. "), @@ -136,16 +102,12 @@ describe("Core Bridge -- Instruction: Post VAA V1", () => { it("Cannot Invoke `post_vaa_v1` with Same VAA", async () => { const signedVaa = localVariables.get("signedVaa") as Buffer; - const vaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); + const encodedVaa = await processVaa(program, payer, signedVaa, GUARDIAN_SET_INDEX); - const ix = await coreBridge.postVaaV1Ix( - program, - { - writeAuthority: payer.publicKey, - vaa, - }, - { tryOnce: {} } - ); + const ix = await coreBridge.postVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa, + }); await expectIxErr(connection, [ix], [payer], "already in use"); }); }); diff --git a/solana/tests/02__tokenBridge/002__createOrUpdateWrapped.ts b/solana/tests/02__tokenBridge/002__createOrUpdateWrapped.ts index 72a308d0fd..26b96dd302 100644 --- a/solana/tests/02__tokenBridge/002__createOrUpdateWrapped.ts +++ b/solana/tests/02__tokenBridge/002__createOrUpdateWrapped.ts @@ -30,6 +30,8 @@ import { expectIxErr, invokeVerifySignaturesAndPostVaa, expectIxOkDetails, + ETHEREUM_TOKEN_ALREADY_CREATED, + processVaa, } from "../helpers"; import * as tokenBridge from "../helpers/tokenBridge"; @@ -44,6 +46,8 @@ const ethereumTokenBridge = new MockTokenBridge( ); const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); +const localVariables = new Map(); + describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -85,14 +89,23 @@ describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { wrappedAsset, dataV1, decomposedMetadata: metadata, - } = await expectCorrectData(program, parsed); + } = await expectCorrectData( + program, + parsed, + false // fork + ); const { wrappedAsset: forkWrappedAsset, dataV1: forkDataV1, decomposedMetadata: forkMetadata, - } = await expectCorrectData(forkedProgram, parsed); + } = await expectCorrectData( + forkedProgram, + parsed, + true // fork + ); - expectDeepEqual(wrappedAsset, forkWrappedAsset); + const { lastUpdatedSequence, ...prunedWrappedAsset } = wrappedAsset; + expectDeepEqual(prunedWrappedAsset, forkWrappedAsset); expectDeepEqual(metadata, forkMetadata); expect(dataV1.symbol).equals(forkDataV1.symbol); @@ -110,6 +123,8 @@ describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { canonicalAddress: "0x000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nativeDecimals: 18, }); + + localVariables.set("signedVaa", signedVaa); }); it("Invoke `create_or_update_wrapped` to Update Asset", async () => { @@ -143,8 +158,16 @@ describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { ); // Check metadata. - const { dataV1 } = await expectCorrectData(program, parsed); - const { dataV1: forkDataV1 } = await expectCorrectData(forkedProgram, parsed); + const { dataV1 } = await expectCorrectData( + program, + parsed, + false // fork + ); + const { dataV1: forkDataV1 } = await expectCorrectData( + forkedProgram, + parsed, + true // fork + ); expect(dataV1.symbol).equals(forkDataV1.symbol); @@ -174,14 +197,23 @@ describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { wrappedAsset, dataV1, decomposedMetadata: metadata, - } = await expectCorrectData(program, parsed); + } = await expectCorrectData( + program, + parsed, + false // fork + ); const { wrappedAsset: forkWrappedAsset, dataV1: forkDataV1, decomposedMetadata: forkMetadata, - } = await expectCorrectData(forkedProgram, parsed); + } = await expectCorrectData( + forkedProgram, + parsed, + true // fork + ); - expectDeepEqual(wrappedAsset, forkWrappedAsset); + const { lastUpdatedSequence, ...prunedWrappedAsset } = wrappedAsset; + expectDeepEqual(prunedWrappedAsset, forkWrappedAsset); expectDeepEqual(metadata, forkMetadata); expect(dataV1.symbol).equals(forkDataV1.symbol); @@ -216,6 +248,152 @@ describe("Token Bridge -- Legacy Instruction: Create or Update Wrapped", () => { }); describe("New Implementation", () => { + it("Cannot Invoke `create_or_update_wrapped` on Same Posted VAA", async () => { + const signedVaa = localVariables.get("signedVaa") as Buffer; + + const ix = tokenBridge.legacyCreateOrUpdateWrappedIx( + program, + { payer: payer.publicKey }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + + it("Cannot Invoke `create_or_update_wrapped` on Same VAA Buffer using Encoded Vaa", async () => { + const signedVaa = localVariables.get("signedVaa") as Buffer; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCreateOrUpdateWrappedIx( + program, + { payer: payer.publicKey, vaa: encodedVaa }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + + it("Invoke `create_or_update_wrapped` to Update Previously Created Wrapped Asset", async () => { + // Make sure metadata has already been created. + { + const mint = tokenBridge.wrappedMintPda( + program.programId, + 2, + Array.from(ETHEREUM_TOKEN_ALREADY_CREATED) + ); + const metadata = tokenBridge.tokenMetadataPda(mint); + const info = await connection.getAccountInfo(metadata); + expect(info).is.not.null; + } + + { + const mint = tokenBridge.wrappedMintPda( + forkedProgram.programId, + 2, + Array.from(ETHEREUM_TOKEN_ALREADY_CREATED) + ); + const metadata = tokenBridge.tokenMetadataPda(mint); + const info = await connection.getAccountInfo(metadata); + expect(info).is.not.null; + } + + // Check that wrapped asset accounts have the old schema and have the same data. + { + const [wrappedAsset, forkedWrappedAsset] = await Promise.all( + [program, forkedProgram].map(async (prog) => { + return tokenBridge.WrappedAsset.fromPda( + connection, + prog.programId, + tokenBridge.wrappedMintPda( + prog.programId, + 2, + Array.from(ETHEREUM_TOKEN_ALREADY_CREATED) + ) + ); + }) + ); + + expect(wrappedAsset.lastUpdatedSequence).is.undefined; + expectDeepEqual(wrappedAsset, forkedWrappedAsset); + } + + // Generate an out-of-sequence VAA for the next test. + const oosSignedVaa = defaultVaa({ + symbol: `OOS`, + name: `Out of Sequence`, + address: ETHEREUM_TOKEN_ALREADY_CREATED, + }); + + localVariables.set("oosSignedVaa", oosSignedVaa); + + // Now update the wrapped asset account on the new implementation. + const signedVaa = defaultVaa({ + symbol: "FRESH", + name: "So fresh and so clean clean.", + address: ETHEREUM_TOKEN_ALREADY_CREATED, + }); + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + // Create the instruction. + const parsed = parseVaa(signedVaa); + const ix = tokenBridge.legacyCreateOrUpdateWrappedIx( + program, + { payer: payer.publicKey, vaa: encodedVaa }, + parsed + ); + + await expectIxOk(connection, [ix], [payer]); + + // Check account realloc'ed. + const mint = tokenBridge.wrappedMintPda( + program.programId, + 2, + Array.from(ETHEREUM_TOKEN_ALREADY_CREATED) + ); + const wrappedAsset = await tokenBridge.WrappedAsset.fromPda( + connection, + program.programId, + mint + ); + expectDeepEqual(wrappedAsset, { + tokenChain: 2, + tokenAddress: Array.from(ETHEREUM_TOKEN_ALREADY_CREATED), + nativeDecimals: 18, + lastUpdatedSequence: new anchor.BN(parsed.sequence.toString()), + }); + }); + + it("Cannot Invoke `create_or_update_wrapped` on Out-of-Sequence VAA", async () => { + const oosSignedVaa = localVariables.get("oosSignedVaa") as Buffer; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + oosSignedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCreateOrUpdateWrappedIx( + program, + { payer: payer.publicKey, vaa: encodedVaa }, + parseVaa(oosSignedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "AttestationOutOfSequence"); + }); + it("Cannot Invoke `create_or_update_wrapped` (Invalid Token Bridge VAA)", async () => { // Create a bogus token transfer VAA. const published = ethereumTokenBridge.publishTransferTokens( @@ -343,7 +521,11 @@ async function parallelTxOk( return parsed; } -async function expectCorrectData(program: tokenBridge.TokenBridgeProgram, parsed: ParsedVaa) { +async function expectCorrectData( + program: tokenBridge.TokenBridgeProgram, + parsed: ParsedVaa, + fork: boolean +) { const programId = program.programId; const connection = program.provider.connection; @@ -365,11 +547,20 @@ async function expectCorrectData(program: tokenBridge.TokenBridgeProgram, parsed // Check wrapped asset. const wrappedAsset = await tokenBridge.WrappedAsset.fromPda(connection, programId, mint); - expectDeepEqual(wrappedAsset, { - tokenChain, - tokenAddress: Array.from(tokenAddress), - nativeDecimals, - }); + if (fork) { + expectDeepEqual(wrappedAsset, { + tokenChain, + tokenAddress: Array.from(tokenAddress), + nativeDecimals, + }); + } else { + expectDeepEqual(wrappedAsset, { + tokenChain, + tokenAddress: Array.from(tokenAddress), + nativeDecimals, + lastUpdatedSequence: new anchor.BN(parsed.sequence.toString()), + }); + } // Check Token Metadata. const metadata = await Metadata.fromAccountAddress( @@ -389,9 +580,9 @@ async function expectCorrectData(program: tokenBridge.TokenBridgeProgram, parsed const { symbol: expectedSymbol } = dataV1; if (symbol.length >= 10) { - expect(symbol.substring(0, 10)).to.equal(expectedSymbol); + expect(symbol.substring(0, 10)).equals(expectedSymbol); } else { - expect(symbol.padEnd(10, "\x00")).to.equal(expectedSymbol); + expect(symbol.padEnd(10, "\x00")).equals(expectedSymbol); } return { wrappedAsset, decomposedMetadata, dataV1 }; diff --git a/solana/tests/02__tokenBridge/003__attestToken.ts b/solana/tests/02__tokenBridge/003__attestToken.ts index ffab4d0947..4ec30f78a0 100644 --- a/solana/tests/02__tokenBridge/003__attestToken.ts +++ b/solana/tests/02__tokenBridge/003__attestToken.ts @@ -1,9 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; -import { NATIVE_MINT } from "@solana/spl-token"; +import { NATIVE_MINT, createMint } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; -import { expectIxOk, WrappedMintInfo, WRAPPED_MINT_INFO_MAX_ONE, expectIxErr } from "../helpers"; +import { + expectIxOk, + WrappedMintInfo, + WRAPPED_MINT_INFO_MAX_ONE, + expectIxErr, + expectDeepEqual, + MINT_INFO_8, +} from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import * as tokenBridge from "../helpers/tokenBridge"; +import { parseAttestMetaPayload } from "@certusone/wormhole-sdk"; describe("Token Bridge -- Legacy Instruction: Attest Token", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -18,7 +26,68 @@ describe("Token Bridge -- Legacy Instruction: Attest Token", () => { const wrappedMaxMint: WrappedMintInfo = WRAPPED_MINT_INFO_MAX_ONE; describe("Ok", () => { - it("Invoke `attest_token`", async () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 12, + }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 13, + }, + ]; + + const possibleIndices = [11, 12, 13]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`attest_token\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const args = defaultArgs(); + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyAttestTokenIx( + program, + { + payer: payer.publicKey, + mint: NATIVE_MINT, + coreMessage: coreMessage.publicKey, + }, + args + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkCoreMessage = anchor.web3.Keypair.generate(); + const forkedIx = tokenBridge.legacyAttestTokenIx( + forkedProgram, + { + payer: payer.publicKey, + mint: NATIVE_MINT, + coreMessage: forkCoreMessage.publicKey, + }, + args + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await Promise.all([ + expectIxOk(connection, [ix], [payer, coreMessage]), + expectIxOk(connection, [forkedIx], [payer, forkCoreMessage]), + ]); + }); + } + } + + it("Invoke `attest_token` for Mint with Metadata", async () => { const [coreMessage, forkedCoreMessage] = await parallelTxOk( program, forkedProgram, @@ -28,10 +97,16 @@ describe("Token Bridge -- Legacy Instruction: Attest Token", () => { ); // Verify message payload. - coreBridge.expectEqualMessageAccounts(wormholeProgram, coreMessage, forkedCoreMessage, false); + await coreBridge.expectEqualMessageAccounts( + wormholeProgram, + coreMessage, + forkedCoreMessage, + false, + false // sameEmitter + ); }); - it("Invoke `attest_token` Again", async () => { + it("Invoke `attest_token` Again with Same Mint", async () => { const [coreMessage, forkedCoreMessage] = await parallelTxOk( program, forkedProgram, @@ -41,7 +116,13 @@ describe("Token Bridge -- Legacy Instruction: Attest Token", () => { ); // Verify message payload. - coreBridge.expectEqualMessageAccounts(wormholeProgram, coreMessage, forkedCoreMessage, false); + await coreBridge.expectEqualMessageAccounts( + wormholeProgram, + coreMessage, + forkedCoreMessage, + false, + false // sameEmitter + ); }); }); @@ -68,6 +149,25 @@ describe("Token Bridge -- Legacy Instruction: Attest Token", () => { // Send the transaction. await expectIxErr(connection, [ix], [payer, coreMessage], "WrappedAsset"); }); + + it("Cannot Invoke `attest_token` with Mint without Metadata", async () => { + const mint = await createMint(connection, payer, payer.publicKey, null, 9); + + // Create the instruction. + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyAttestTokenIx( + program, + { + coreMessage: coreMessage.publicKey, + payer: payer.publicKey, + mint, + }, + defaultArgs() + ); + + // Send the transaction. + await expectIxErr(connection, [ix], [payer, coreMessage], "AccountNotInitialized"); + }); }); }); diff --git a/solana/tests/02__tokenBridge/004__transferTokensNative.ts b/solana/tests/02__tokenBridge/004__transferTokensNative.ts index 919d3a3c9d..1693bdf891 100644 --- a/solana/tests/02__tokenBridge/004__transferTokensNative.ts +++ b/solana/tests/02__tokenBridge/004__transferTokensNative.ts @@ -1,5 +1,6 @@ import * as anchor from "@coral-xyz/anchor"; import { + TOKEN_PROGRAM_ID, createAssociatedTokenAccount, getAssociatedTokenAddressSync, mintTo, @@ -9,7 +10,9 @@ import { MINT_INFO_8, MINT_INFO_9, MintInfo, + expectDeepEqual, expectIxErr, + expectIxOk, expectIxOkDetails, getTokenBalances, } from "../helpers"; @@ -37,6 +40,87 @@ describe("Token Bridge -- Legacy Instruction: Transfer Tokens (Native)", () => { }); describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 14, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 15 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 16, + }, + ]; + + const possibleIndices = [13, 14, 15, 16]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`transfer_tokens_native\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { mint } = MINT_INFO_8; + const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); + + const amount = new anchor.BN(10); + const approveIx = tokenBridge.approveTransferAuthorityIx( + program, + srcToken, + payer.publicKey, + amount + ); + + const args = defaultArgs(amount, new anchor.BN(0)); + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyTransferTokensNativeIx( + program, + { + payer: payer.publicKey, + srcToken, + mint, + coreMessage: coreMessage.publicKey, + }, + args + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkCoreMessage = anchor.web3.Keypair.generate(); + const forkedApproveIx = tokenBridge.approveTransferAuthorityIx( + forkedProgram, + srcToken, + payer.publicKey, + amount + ); + const forkedIx = tokenBridge.legacyTransferTokensNativeIx( + forkedProgram, + { + payer: payer.publicKey, + srcToken, + mint, + coreMessage: forkCoreMessage.publicKey, + }, + args + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await Promise.all([ + expectIxOk(connection, [approveIx, ix], [payer, coreMessage]), + expectIxOk(connection, [forkedApproveIx, forkedIx], [payer, forkCoreMessage]), + ]); + }); + } + } + for (const { mint, decimals } of mints) { const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); diff --git a/solana/tests/02__tokenBridge/006__transferTokensWithPayloadNative.ts b/solana/tests/02__tokenBridge/006__transferTokensWithPayloadNative.ts index c4c3d85dad..6d351c76a6 100644 --- a/solana/tests/02__tokenBridge/006__transferTokensWithPayloadNative.ts +++ b/solana/tests/02__tokenBridge/006__transferTokensWithPayloadNative.ts @@ -1,10 +1,12 @@ import * as anchor from "@coral-xyz/anchor"; -import { getAssociatedTokenAddressSync, mintTo } from "@solana/spl-token"; +import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, mintTo } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { MINT_INFO_8, MINT_INFO_9, MintInfo, + expectDeepEqual, + expectIxOk, expectIxOkDetails, getTokenBalances, } from "../helpers"; @@ -33,6 +35,89 @@ describe("Token Bridge -- Legacy Instruction: Transfer Tokens with Payload (Nati }); describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 15, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 16 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 17, + }, + ]; + + const possibleIndices = [14, 15, 16, 17]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`transfer_tokens_with_payload_native\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { mint } = MINT_INFO_8; + const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); + + const amount = new anchor.BN(10); + const approveIx = tokenBridge.approveTransferAuthorityIx( + program, + srcToken, + payer.publicKey, + amount + ); + + const args = defaultArgs(amount); + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyTransferTokensWithPayloadNativeIx( + program, + { + payer: payer.publicKey, + srcToken, + mint, + coreMessage: coreMessage.publicKey, + senderAuthority: payer.publicKey, + }, + args + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkCoreMessage = anchor.web3.Keypair.generate(); + const forkedApproveIx = tokenBridge.approveTransferAuthorityIx( + forkedProgram, + srcToken, + payer.publicKey, + amount + ); + const forkedIx = tokenBridge.legacyTransferTokensWithPayloadNativeIx( + forkedProgram, + { + payer: payer.publicKey, + srcToken, + mint, + coreMessage: forkCoreMessage.publicKey, + senderAuthority: payer.publicKey, + }, + args + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await Promise.all([ + expectIxOk(connection, [approveIx, ix], [payer, coreMessage]), + expectIxOk(connection, [forkedApproveIx, forkedIx], [payer, forkCoreMessage]), + ]); + }); + } + } + for (const { mint, decimals } of mints) { const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); diff --git a/solana/tests/02__tokenBridge/012__completeTransferNative.ts b/solana/tests/02__tokenBridge/012__completeTransferNative.ts index 6db66e3e9f..e0cb8a1854 100644 --- a/solana/tests/02__tokenBridge/012__completeTransferNative.ts +++ b/solana/tests/02__tokenBridge/012__completeTransferNative.ts @@ -7,6 +7,7 @@ import { import { MockGuardians, MockTokenBridge } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; import { + TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, getOrCreateAssociatedTokenAccount, } from "@solana/spl-token"; @@ -19,11 +20,14 @@ import { MINT_INFO_9, MintInfo, createAssociatedTokenAccountOffCurve, + expectDeepEqual, expectIxErr, + expectIxOk, expectIxOkDetails, getTokenBalances, invokeVerifySignaturesAndPostVaa, parallelPostVaa, + processVaa, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import * as tokenBridge from "../helpers/tokenBridge"; @@ -36,6 +40,8 @@ const dummyTokenBridge = new MockTokenBridge( ); const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); +const localVariables = new Map(); + describe("Token Bridge -- Legacy Instruction: Complete Transfer (Native)", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -50,6 +56,90 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Native)", () => const mints: MintInfo[] = [MINT_INFO_8, MINT_INFO_9]; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 11, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 12 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 13, + }, + ]; + + const possibleIndices = [10, 11, 12, 13]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`complete_transfer_native\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { mint } = MINT_INFO_8; + const recipient = anchor.web3.Keypair.generate(); + const recipientToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + recipient.publicKey + ); + + const amount = new anchor.BN(10); + const signedVaa = getSignedTransferVaa( + mint, + BigInt(amount.toString()), + BigInt(0), + recipientToken.address + ); + + // Process the VAA for the new implementation. + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + // And post the VAA. + const parsed = await parallelPostVaa(connection, payer, signedVaa); + const ix = tokenBridge.legacyCompleteTransferNativeIx( + program, + { + payer: payer.publicKey, + vaa: encodedVaa, + recipientToken: recipientToken.address, + mint, + }, + parsed + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkedIx = tokenBridge.legacyCompleteTransferNativeIx( + forkedProgram, + { + payer: payer.publicKey, + recipientToken: recipientToken.address, + mint, + }, + parsed + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await expectIxOk(connection, [ix, forkedIx], [payer]); + }); + } + } + for (const { mint, decimals } of mints) { it(`Invoke \`complete_transfer_native\` (${decimals} Decimals, No Fee)`, async () => { // Create recipient token account. @@ -104,6 +194,11 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Native)", () => fee * BigInt(10 ** (decimals - 8)) ), ]); + + // Save for later + localVariables.set(`signedVaa${decimals}`, signedVaa); + localVariables.set(`recipientToken${decimals}`, recipientToken.address); + localVariables.set(`mint${decimals}`, mint); }); it(`Invoke \`complete_transfer_native\` (${decimals} Decimals, With Fee)`, async () => { @@ -250,6 +345,41 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Native)", () => }); describe("New Implementation", () => { + it("Cannot Invoke `complete_transfer_native` on Same VAA", async () => { + const signedVaa = localVariables.get("signedVaa9") as Buffer; + const recipientToken = localVariables.get("recipientToken9") as anchor.web3.PublicKey; + const mint = localVariables.get("mint9") as anchor.web3.PublicKey; + + const ix = tokenBridge.legacyCompleteTransferNativeIx( + program, + { payer: payer.publicKey, recipientToken, mint }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + + it("Cannot Invoke `complete_transfer_native` on Same VAA Buffer using Encoded Vaa", async () => { + const signedVaa = localVariables.get("signedVaa9") as Buffer; + const recipientToken = localVariables.get("recipientToken9") as anchor.web3.PublicKey; + const mint = localVariables.get("mint9") as anchor.web3.PublicKey; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCompleteTransferNativeIx( + program, + { payer: payer.publicKey, vaa: encodedVaa, recipientToken, mint }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + for (const { mint, decimals } of mints) { it(`Cannot Invoke \`complete_transfer_native\` (${decimals} Decimals, Recipient == Wallet Address with Rent Sysvar)`, async () => { // Create recipient token account. diff --git a/solana/tests/02__tokenBridge/014__completeTransferWithPayloadNative.ts b/solana/tests/02__tokenBridge/014__completeTransferWithPayloadNative.ts index 9636b4bde6..b1a0739b05 100644 --- a/solana/tests/02__tokenBridge/014__completeTransferWithPayloadNative.ts +++ b/solana/tests/02__tokenBridge/014__completeTransferWithPayloadNative.ts @@ -1,5 +1,6 @@ import * as anchor from "@coral-xyz/anchor"; import { + TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, getOrCreateAssociatedTokenAccount, } from "@solana/spl-token"; @@ -15,6 +16,9 @@ import { expectIxErr, airdrop, ETHEREUM_DEADBEEF_TOKEN_ADDRESS, + expectDeepEqual, + expectIxOk, + processVaa, } from "../helpers"; import { CHAIN_ID_SOLANA, @@ -38,6 +42,8 @@ const dummyTokenBridge = new MockTokenBridge( ); const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); +const localVariables = new Map(); + describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Native)", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -52,6 +58,84 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const mints: MintInfo[] = [MINT_INFO_8, MINT_INFO_9]; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 12, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 13 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 14, + }, + ]; + + const possibleIndices = [11, 12, 13, 14]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`complete_transfer_with_payload_native\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { mint } = MINT_INFO_8; + const dstToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ); + + const amount = new anchor.BN(10); + const signedVaa = getSignedTransferVaa(mint, BigInt(amount.toString()), payer.publicKey); + + // Process the VAA for the new implementation. + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + // And post the VAA. + const parsed = await parallelPostVaa(connection, payer, signedVaa); + const ix = tokenBridge.legacyCompleteTransferWithPayloadNativeIx( + program, + { + payer: payer.publicKey, + vaa: encodedVaa, + dstToken: dstToken.address, + mint, + }, + parsed + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkedIx = tokenBridge.legacyCompleteTransferWithPayloadNativeIx( + forkedProgram, + { + payer: payer.publicKey, + dstToken: dstToken.address, + mint, + }, + parsed + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await expectIxOk(connection, [ix, forkedIx], [payer]); + }); + } + } + for (const { mint, decimals } of mints) { it(`Invoke \`complete_transfer_with_payload_native\` (${decimals} Decimals, Redeemer == Redeemer Authority)`, async () => { // Create recipient token account. @@ -61,7 +145,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const amount = BigInt(699999); // Create the signed transfer VAA. - const signedVaa = getSignedTransferVaa(mint, amount, payer.publicKey, "0xdeadbeef"); + const signedVaa = getSignedTransferVaa(mint, amount, payer.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances(program, forkedProgram, payerToken); @@ -86,6 +170,11 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na recipientBalancesBefore, tokenBridge.TransferDirection.In ); + + // Save for later + localVariables.set(`signedVaa${decimals}`, signedVaa); + localVariables.set(`dstToken${decimals}`, payerToken); + localVariables.set(`mint${decimals}`, mint); }); it(`Invoke \`complete_transfer_with_payload_native\` (${decimals} Decimals, Redeemer != Redeemer Authority)`, async () => { @@ -102,7 +191,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const amount = BigInt(699999); // Create the signed transfer VAA. - const signedVaa = getSignedTransferVaa(mint, amount, recipient.publicKey, "0xdeadbeef"); + const signedVaa = getSignedTransferVaa(mint, amount, recipient.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances( @@ -138,6 +227,41 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na }); describe("New Implementation", () => { + it("Cannot Invoke `complete_transfer_with_payload_native` on Same VAA", async () => { + const signedVaa = localVariables.get("signedVaa9") as Buffer; + const dstToken = localVariables.get("dstToken9") as anchor.web3.PublicKey; + const mint = localVariables.get("mint9") as anchor.web3.PublicKey; + + const ix = tokenBridge.legacyCompleteTransferWithPayloadNativeIx( + program, + { payer: payer.publicKey, dstToken, mint }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + + it("Cannot Invoke `complete_transfer_with_payload_native` on Same VAA Buffer using Encoded Vaa", async () => { + const signedVaa = localVariables.get("signedVaa9") as Buffer; + const dstToken = localVariables.get("dstToken9") as anchor.web3.PublicKey; + const mint = localVariables.get("mint9") as anchor.web3.PublicKey; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCompleteTransferWithPayloadNativeIx( + program, + { payer: payer.publicKey, vaa: encodedVaa, dstToken, mint }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + for (const { mint, decimals } of mints) { it(`Cannot Invoke \`complete_transfer_with_payload_native\` (${decimals} Decimals, Invalid Mint)`, async () => { // Create recipient token account. @@ -155,8 +279,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const signedVaa = getSignedTransferVaa( anchor.web3.Keypair.generate().publicKey, // Pass bogus mint amount, - payer.publicKey, - "0xdeadbeef" + payer.publicKey ); // Post the VAA. @@ -193,7 +316,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na mint, amount, payer.publicKey, - "0xdeadbeef", + undefined, CHAIN_ID_ETH // Pass invalid target chain. ); @@ -234,7 +357,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na mint, amount, payer.publicKey, - "0xdeadbeef", + undefined, undefined, CHAIN_ID_ETH // Specify a token chain that is not Solana. ); @@ -274,8 +397,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const signedVaa = getSignedTransferVaa( mint, amount, - anchor.web3.Keypair.generate().publicKey, // Create random redeemer. - "0xdeadbeef" + anchor.web3.Keypair.generate().publicKey // Create random redeemer. ); // Post the VAA. @@ -311,7 +433,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na const amount = BigInt(699999); // Create the signed transfer VAA with random "to" (redeemer). - const signedVaa = getSignedTransferVaa(mint, amount, payer.publicKey, "0xdeadbeef"); + const signedVaa = getSignedTransferVaa(mint, amount, payer.publicKey); // Post the VAA. await invokeVerifySignaturesAndPostVaa(wormholeProgram, payer, signedVaa); @@ -379,8 +501,8 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Na function getSignedTransferVaa( mint: anchor.web3.PublicKey, amount: bigint, - recipient: anchor.web3.PublicKey, - payload: string, + redeemer: anchor.web3.PublicKey, + payload?: Buffer, targetChain?: number, tokenChain?: number ): Buffer { @@ -389,9 +511,9 @@ function getSignedTransferVaa( tokenChain ?? CHAIN_ID_SOLANA, amount, targetChain ?? CHAIN_ID_SOLANA, - recipient.toBuffer().toString("hex"), // TARGET CONTRACT (redeemer) + redeemer.toBuffer().toString("hex"), // TARGET CONTRACT (redeemer) Buffer.from(tryNativeToUint8Array(ETHEREUM_TOKEN_BRIDGE_ADDRESS, 2)), - Buffer.from(payload.substring(2), "hex"), + payload ?? Buffer.from("deadbeef", "hex"), 0 // Batch ID ); return guardians.addSignatures(vaaBytes, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); diff --git a/solana/tests/02__tokenBridge/022__completeTransferWrapped.ts b/solana/tests/02__tokenBridge/022__completeTransferWrapped.ts index 6995988f7d..b60e9e1529 100644 --- a/solana/tests/02__tokenBridge/022__completeTransferWrapped.ts +++ b/solana/tests/02__tokenBridge/022__completeTransferWrapped.ts @@ -6,7 +6,7 @@ import { } from "@certusone/wormhole-sdk"; import { MockGuardians, MockTokenBridge } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; -import { Account, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; +import { Account, TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; import { expect } from "chai"; import { ETHEREUM_DEADBEEF_TOKEN_ADDRESS, @@ -18,11 +18,14 @@ import { WRAPPED_MINT_INFO_MAX_ONE, WrappedMintInfo, createAssociatedTokenAccountOffCurve, + expectDeepEqual, expectIxErr, + expectIxOk, expectIxOkDetails, getTokenBalances, invokeVerifySignaturesAndPostVaa, parallelPostVaa, + processVaa, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import * as tokenBridge from "../helpers/tokenBridge"; @@ -36,6 +39,8 @@ const dummyTokenBridge = new MockTokenBridge( ); const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); +const localVariables = new Map(); + describe("Token Bridge -- Legacy Instruction: Complete Transfer (Wrapped)", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -51,6 +56,111 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Wrapped)", () = const wrappedMaxMint: WrappedMintInfo = WRAPPED_MINT_INFO_MAX_ONE; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 11, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 12 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 13, + }, + ]; + + const possibleIndices = [10, 11, 12, 13]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`complete_transfer_wrapped\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { chain, address } = WRAPPED_MINT_INFO_8; + const recipient = anchor.web3.Keypair.generate(); + + const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); + const recipientToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + recipient.publicKey + ); + + const forkMint = tokenBridge.wrappedMintPda( + forkedProgram.programId, + chain, + Array.from(address) + ); + const forkRecipientToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + forkMint, + recipient.publicKey + ); + + const amount = new anchor.BN(10); + const signedVaa = getSignedTransferVaa( + address, + BigInt(amount.toString()), + BigInt(0), + recipientToken.address + ); + const forkSignedVaa = getSignedTransferVaa( + address, + BigInt(amount.toString()), + BigInt(0), + forkRecipientToken.address + ); + + // Process the VAA for the new implementation. + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + // And post the VAA. + const parsed = await parallelPostVaa(connection, payer, signedVaa); + const ix = tokenBridge.legacyCompleteTransferWrappedIx( + program, + { + payer: payer.publicKey, + vaa: encodedVaa, + recipientToken: recipientToken.address, + wrappedMint: mint, + }, + parsed + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkParsed = await parallelPostVaa(connection, payer, forkSignedVaa); + const forkedIx = tokenBridge.legacyCompleteTransferWrappedIx( + forkedProgram, + { + payer: payer.publicKey, + recipientToken: forkRecipientToken.address, + wrappedMint: forkMint, + }, + forkParsed + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await expectIxOk(connection, [ix, forkedIx], [payer]); + }); + } + } + for (const { chain, decimals, address } of wrappedMints) { it(`Invoke \`complete_transfer_wrapped\` (${decimals} Decimals, No Fee)`, async () => { const [mint, forkMint] = [program, forkedProgram].map((program) => @@ -127,6 +237,10 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Wrapped)", () = fee ), ]); + + // Save for later + localVariables.set(`signedVaa${decimals}`, signedVaa); + localVariables.set(`recipientToken${decimals}`, recipientToken.address); }); it(`Invoke \`complete_transfer_wrapped\` (${decimals} Decimals, With Fee)`, async () => { @@ -528,6 +642,39 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer (Wrapped)", () = }); describe("New Implementation", () => { + it("Cannot Invoke `complete_transfer_wrapped` on Same VAA", async () => { + const signedVaa = localVariables.get("signedVaa8") as Buffer; + const recipientToken = localVariables.get("recipientToken8") as anchor.web3.PublicKey; + + const ix = tokenBridge.legacyCompleteTransferWrappedIx( + program, + { payer: payer.publicKey, recipientToken }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + + it("Cannot Invoke `complete_transfer_wrapped` on Same VAA Buffer using Encoded Vaa", async () => { + const signedVaa = localVariables.get("signedVaa8") as Buffer; + const recipientToken = localVariables.get("recipientToken8") as anchor.web3.PublicKey; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCompleteTransferWrappedIx( + program, + { payer: payer.publicKey, vaa: encodedVaa, recipientToken }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + for (const { chain, decimals, address } of wrappedMints) { it(`Invoke \`complete_transfer_wrapped\` (${decimals} Decimals, Recipient == Wallet Address with Rent Sysvar)`, async () => { const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); diff --git a/solana/tests/02__tokenBridge/024__completeTransferWithPayloadWrapped.ts b/solana/tests/02__tokenBridge/024__completeTransferWithPayloadWrapped.ts index 6b435f4e86..62123a0686 100644 --- a/solana/tests/02__tokenBridge/024__completeTransferWithPayloadWrapped.ts +++ b/solana/tests/02__tokenBridge/024__completeTransferWithPayloadWrapped.ts @@ -1,5 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; -import { Account, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; +import { Account, TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; import { ETHEREUM_TOKEN_BRIDGE_ADDRESS, WRAPPED_MINT_INFO_MAX_TWO, @@ -13,6 +13,9 @@ import { invokeVerifySignaturesAndPostVaa, ETHEREUM_TOKEN_ADDRESS_MAX_ONE, ETHEREUM_DEADBEEF_TOKEN_ADDRESS, + expectDeepEqual, + expectIxOk, + processVaa, } from "../helpers"; import { CHAIN_ID_SOLANA, @@ -36,6 +39,8 @@ const dummyTokenBridge = new MockTokenBridge( ); const guardians = new MockGuardians(GUARDIAN_SET_INDEX, GUARDIAN_KEYS); +const localVariables = new Map(); + describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wrapped)", () => { anchor.setProvider(anchor.AnchorProvider.env()); @@ -51,6 +56,101 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const wrappedMaxMint: WrappedMintInfo = WRAPPED_MINT_INFO_MAX_TWO; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 12, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 13 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 14, + }, + ]; + + const possibleIndices = [11, 12, 13, 14]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`complete_transfer_with_payload_wrapped\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { chain, address } = WRAPPED_MINT_INFO_8; + + const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); + const dstToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + mint, + payer.publicKey + ); + + const forkMint = tokenBridge.wrappedMintPda( + forkedProgram.programId, + chain, + Array.from(address) + ); + const forkDstToken = await getOrCreateAssociatedTokenAccount( + connection, + payer, + forkMint, + payer.publicKey + ); + + const amount = new anchor.BN(10); + const signedVaa = getSignedTransferVaa( + address, + BigInt(amount.toString()), + payer.publicKey + ); + + // Process the VAA for the new implementation. + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + // And post the VAA. + const parsed = await parallelPostVaa(connection, payer, signedVaa); + const ix = tokenBridge.legacyCompleteTransferWithPayloadWrappedIx( + program, + { + payer: payer.publicKey, + vaa: encodedVaa, + dstToken: dstToken.address, + wrappedMint: mint, + }, + parsed + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkedIx = tokenBridge.legacyCompleteTransferWithPayloadWrappedIx( + forkedProgram, + { + payer: payer.publicKey, + dstToken: forkDstToken.address, + wrappedMint: forkMint, + }, + parsed + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await expectIxOk(connection, [ix, forkedIx], [payer]); + }); + } + } for (const { chain, decimals, address } of wrappedMints) { it(`Invoke \`complete_transfer_with_payload_wrapped\` (${decimals} Decimals, Redeemer == Redeemer Authority)`, async () => { const [mint, forkMint] = [program, forkedProgram].map((program) => @@ -66,12 +166,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = BigInt(699999); // Create the signed transfer VAA. - const signedVaa = await getSignedTransferVaa( - address, - amount, - payer.publicKey, - "0xdeadbeef" - ); + const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances( @@ -87,7 +182,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr forkedProgram, { dstToken: payerToken, - forkRecipientToken: forkPayerToken, + forkDstToken: forkPayerToken, redeemerAuthority: payer, }, signedVaa, @@ -103,6 +198,10 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr tokenBridge.TransferDirection.In, amount ); + + // Save for later + localVariables.set(`signedVaa${decimals}`, signedVaa); + localVariables.set(`dstToken${decimals}`, payerToken.address); }); it(`Invoke \`complete_transfer_with_payload_wrapped\` (${decimals} Decimals, Redeemer != Redeemer Authority)`, async () => { @@ -111,7 +210,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr ); // Create recipient token account. const recipient = anchor.web3.Keypair.generate(); - const [dstToken, forkRecipientToken] = await Promise.all([ + const [dstToken, forkDstToken] = await Promise.all([ getOrCreateAssociatedTokenAccount(connection, payer, mint, recipient.publicKey), getOrCreateAssociatedTokenAccount(connection, payer, forkMint, recipient.publicKey), ]); @@ -120,14 +219,14 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = BigInt(699999); // Create the signed transfer VAA. - const signedVaa = getSignedTransferVaa(address, amount, recipient.publicKey, "0xdeadbeef"); + const signedVaa = getSignedTransferVaa(address, amount, recipient.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances( program, forkedProgram, dstToken.address, - forkRecipientToken.address + forkDstToken.address ); // Complete the transfer. @@ -136,7 +235,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr forkedProgram, { dstToken, - forkRecipientToken, + forkDstToken, redeemerAuthority: recipient, }, signedVaa, @@ -147,7 +246,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr await tokenBridge.expectCorrectWrappedTokenBalanceChanges( connection, dstToken.address, - forkRecipientToken.address, + forkDstToken.address, recipientBalancesBefore, tokenBridge.TransferDirection.In, amount @@ -168,12 +267,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = BigInt(1); // Create the signed transfer VAA. - const signedVaa = await getSignedTransferVaa( - address, - amount, - payer.publicKey, - "0xdeadbeef" - ); + const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances( @@ -189,7 +283,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr forkedProgram, { dstToken: payerToken, - forkRecipientToken: forkPayerToken, + forkDstToken: forkPayerToken, redeemerAuthority: payer, }, signedVaa, @@ -225,7 +319,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = Buffer.alloc(8, "ffffffff", "hex").readBigUInt64BE() - BigInt(1); // Create the signed transfer VAA. - const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey, "0xdeadbeef"); + const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey); // Fetch balances before. const recipientBalancesBefore = await getTokenBalances( @@ -241,7 +335,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr forkedProgram, { dstToken: payerToken, - forkRecipientToken: forkPayerToken, + forkDstToken: forkPayerToken, redeemerAuthority: payer, }, signedVaa, @@ -261,6 +355,38 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr }); describe("New Implementation", () => { + it("Cannot Invoke `complete_transfer_wrapped` on Same VAA", async () => { + const signedVaa = localVariables.get("signedVaa8") as Buffer; + const dstToken = localVariables.get("dstToken8") as anchor.web3.PublicKey; + + const ix = tokenBridge.legacyCompleteTransferWithPayloadWrappedIx( + program, + { payer: payer.publicKey, dstToken }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + it("Cannot Invoke `complete_transfer_wrapped` on Same VAA Buffer using Encoded Vaa", async () => { + const signedVaa = localVariables.get("signedVaa8") as Buffer; + const dstToken = localVariables.get("dstToken8") as anchor.web3.PublicKey; + + const encodedVaa = await processVaa( + tokenBridge.getCoreBridgeProgram(program), + payer, + signedVaa, + GUARDIAN_SET_INDEX + ); + + const ix = tokenBridge.legacyCompleteTransferWithPayloadWrappedIx( + program, + { payer: payer.publicKey, vaa: encodedVaa, dstToken }, + parseVaa(signedVaa) + ); + + await expectIxErr(connection, [ix], [payer], "already in use"); + }); + for (const { chain, decimals, address } of wrappedMints) { it(`Cannot Invoke \`complete_transfer_with_payload_wrapped)\` (${decimals} Decimals, Invalid Mint)`, async () => { const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); @@ -280,8 +406,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const signedVaa = await getSignedTransferVaa( ETHEREUM_TOKEN_ADDRESS_MAX_ONE, // Pass invalid address. amount, - payer.publicKey, - "0xdeadbeef" + payer.publicKey ); // Complete the transfer. @@ -321,7 +446,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr address, amount, payer.publicKey, - "0xdeadbeef", + undefined, CHAIN_ID_ETH // Pass invalid target chain. ); @@ -365,7 +490,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr ETHEREUM_TOKEN_ADDRESS_MAX_ONE, // Pass invalid address. amount, payer.publicKey, - "0xdeadbeef", + undefined, undefined, CHAIN_ID_SOLANA // Pass a token chain that is not ETH. ); @@ -408,7 +533,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = Buffer.alloc(8, "ffffffff", "hex").readBigUInt64BE() + BigInt(10000); // Create the signed transfer VAA. Specify an amount that is > u64::MAX. - const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey, "0xdeadbeef"); + const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey); // Complete the transfer. await invokeVerifySignaturesAndPostVaa(wormholeProgram, payer, signedVaa); @@ -448,8 +573,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const signedVaa = await getSignedTransferVaa( address, amount, - anchor.web3.Keypair.generate().publicKey, // Pass invalid redeemer authority. - "0xdeadbeef" + anchor.web3.Keypair.generate().publicKey // Pass invalid redeemer authority. ); // Complete the transfer. @@ -488,7 +612,7 @@ describe("Token Bridge -- Legacy Instruction: Complete Transfer With Payload (Wr const amount = BigInt(42069); // Create the signed transfer VAA. - const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey, "0xdeadbeef"); + const signedVaa = await getSignedTransferVaa(address, amount, payer.publicKey); // Complete the transfer. await invokeVerifySignaturesAndPostVaa(wormholeProgram, payer, signedVaa); @@ -558,7 +682,7 @@ function getSignedTransferVaa( tokenAddress: Uint8Array, amount: bigint, recipient: anchor.web3.PublicKey, - payload: string, + payload?: Buffer, targetChain?: number, tokenChain?: number ): Buffer { @@ -569,7 +693,7 @@ function getSignedTransferVaa( targetChain ?? CHAIN_ID_SOLANA, recipient.toBuffer().toString("hex"), // TARGET CONTRACT (redeemer) Buffer.from(tryNativeToUint8Array(ETHEREUM_TOKEN_BRIDGE_ADDRESS, 2)), - Buffer.from(payload.substring(2), "hex"), + payload ?? Buffer.from("deadbeef", "hex"), 0 // Batch ID ); return guardians.addSignatures(vaaBytes, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); @@ -580,14 +704,14 @@ async function parallelTxDetails( forkedProgram: tokenBridge.TokenBridgeProgram, tokenAccounts: { dstToken: Account; - forkRecipientToken: Account; + forkDstToken: Account; redeemerAuthority: anchor.web3.Keypair; }, signedVaa, payer: anchor.web3.Keypair ) { const connection = program.provider.connection; - const { dstToken, forkRecipientToken, redeemerAuthority } = tokenAccounts; + const { dstToken, forkDstToken, redeemerAuthority } = tokenAccounts; // Post the VAA. const parsed = await parallelPostVaa(connection, payer, signedVaa); @@ -606,7 +730,7 @@ async function parallelTxDetails( forkedProgram, { payer: payer.publicKey, - dstToken: forkRecipientToken.address, + dstToken: forkDstToken.address, redeemerAuthority: redeemerAuthority.publicKey, }, parsed diff --git a/solana/tests/02__tokenBridge/026__transferTokensWrapped.ts b/solana/tests/02__tokenBridge/026__transferTokensWrapped.ts index 8469273eee..59f5c5964e 100644 --- a/solana/tests/02__tokenBridge/026__transferTokensWrapped.ts +++ b/solana/tests/02__tokenBridge/026__transferTokensWrapped.ts @@ -1,11 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; -import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + getOrCreateAssociatedTokenAccount, +} from "@solana/spl-token"; import { WRAPPED_MINT_INFO_7, WRAPPED_MINT_INFO_8, WRAPPED_MINT_INFO_MAX_ONE, WrappedMintInfo, + expectDeepEqual, expectIxErr, + expectIxOk, expectIxOkDetails, getTokenBalances, } from "../helpers"; @@ -30,6 +36,96 @@ describe("Token Bridge -- Legacy Instruction: Transfer Tokens (Wrapped)", () => const wrappedMaxMint: WrappedMintInfo = WRAPPED_MINT_INFO_MAX_ONE; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 14, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 15 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 16, + }, + ]; + + const possibleIndices = [13, 14, 15, 16]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`transfer_tokens_wrapped\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { chain, address } = WRAPPED_MINT_INFO_8; + + const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); + const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); + + const forkMint = tokenBridge.wrappedMintPda( + forkedProgram.programId, + chain, + Array.from(address) + ); + const forkSrcToken = getAssociatedTokenAddressSync(forkMint, payer.publicKey); + + const amount = new anchor.BN(10); + const approveIx = tokenBridge.approveTransferAuthorityIx( + program, + srcToken, + payer.publicKey, + amount + ); + + const args = defaultArgs(amount, new anchor.BN(0)); + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyTransferTokensWrappedIx( + program, + { + payer: payer.publicKey, + srcToken, + wrappedMint: mint, + coreMessage: coreMessage.publicKey, + }, + args + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkCoreMessage = anchor.web3.Keypair.generate(); + const forkedApproveIx = tokenBridge.approveTransferAuthorityIx( + forkedProgram, + forkSrcToken, + payer.publicKey, + amount + ); + const forkedIx = tokenBridge.legacyTransferTokensWrappedIx( + forkedProgram, + { + payer: payer.publicKey, + srcToken: forkSrcToken, + wrappedMint: forkMint, + coreMessage: forkCoreMessage.publicKey, + }, + args + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await Promise.all([ + expectIxOk(connection, [approveIx, ix], [payer, coreMessage]), + expectIxOk(connection, [forkedApproveIx, forkedIx], [payer, forkCoreMessage]), + ]); + }); + } + } + for (const { chain, decimals, address } of wrappedMints) { it(`Invoke \`transfer_tokens_wrapped\` (${decimals} Decimals)`, async () => { const [mint, forkMint] = [program, forkedProgram].map((program) => diff --git a/solana/tests/02__tokenBridge/028__transferTokensWithPayloadWrapped.ts b/solana/tests/02__tokenBridge/028__transferTokensWithPayloadWrapped.ts index 1a39321247..98ece2e495 100644 --- a/solana/tests/02__tokenBridge/028__transferTokensWithPayloadWrapped.ts +++ b/solana/tests/02__tokenBridge/028__transferTokensWithPayloadWrapped.ts @@ -1,5 +1,9 @@ import * as anchor from "@coral-xyz/anchor"; -import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token"; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + getOrCreateAssociatedTokenAccount, +} from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { WrappedMintInfo, @@ -8,6 +12,8 @@ import { WRAPPED_MINT_INFO_MAX_TWO, expectIxOkDetails, getTokenBalances, + expectDeepEqual, + expectIxOk, } from "../helpers"; import * as tokenBridge from "../helpers/tokenBridge"; @@ -25,6 +31,95 @@ describe("Token Bridge -- Legacy Instruction: Transfer Tokens with Payload (Wrap const wrappedMaxMint: WrappedMintInfo = WRAPPED_MINT_INFO_MAX_TWO; describe("Ok", () => { + const unorderedPrograms = [ + { + name: "System", + pubkey: anchor.web3.SystemProgram.programId, + forkPubkey: anchor.web3.SystemProgram.programId, + idx: 15, + }, + { name: "Token", pubkey: TOKEN_PROGRAM_ID, forkPubkey: TOKEN_PROGRAM_ID, idx: 16 }, + { + name: "Core Bridge", + pubkey: tokenBridge.coreBridgeProgramId(program), + forkPubkey: tokenBridge.coreBridgeProgramId(forkedProgram), + idx: 17, + }, + ]; + + const possibleIndices = [14, 15, 16, 17]; + + for (const { name, pubkey, forkPubkey, idx } of unorderedPrograms) { + for (const possibleIdx of possibleIndices) { + if (possibleIdx == idx) { + continue; + } + + it(`Invoke \`transfer_tokens_with_payload_wrapped\` with ${name} Program at Index == ${possibleIdx}`, async () => { + const { chain, address } = WRAPPED_MINT_INFO_8; + + const mint = tokenBridge.wrappedMintPda(program.programId, chain, Array.from(address)); + const srcToken = getAssociatedTokenAddressSync(mint, payer.publicKey); + + const forkMint = tokenBridge.wrappedMintPda( + forkedProgram.programId, + chain, + Array.from(address) + ); + const forkSrcToken = getAssociatedTokenAddressSync(forkMint, payer.publicKey); + + const amount = new anchor.BN(10); + const approveIx = tokenBridge.approveTransferAuthorityIx( + program, + srcToken, + payer.publicKey, + amount + ); + + const args = defaultArgs(amount); + const coreMessage = anchor.web3.Keypair.generate(); + const ix = tokenBridge.legacyTransferTokensWithPayloadWrappedIx( + program, + { + payer: payer.publicKey, + srcToken, + wrappedMint: mint, + coreMessage: coreMessage.publicKey, + }, + args + ); + expectDeepEqual(ix.keys[idx].pubkey, pubkey); + ix.keys[idx].pubkey = ix.keys[possibleIdx].pubkey; + ix.keys[possibleIdx].pubkey = pubkey; + + const forkCoreMessage = anchor.web3.Keypair.generate(); + const forkedApproveIx = tokenBridge.approveTransferAuthorityIx( + forkedProgram, + forkSrcToken, + payer.publicKey, + amount + ); + const forkedIx = tokenBridge.legacyTransferTokensWithPayloadWrappedIx( + forkedProgram, + { + payer: payer.publicKey, + srcToken: forkSrcToken, + wrappedMint: forkMint, + coreMessage: forkCoreMessage.publicKey, + }, + args + ); + expectDeepEqual(forkedIx.keys[idx].pubkey, forkPubkey); + forkedIx.keys[idx].pubkey = forkedIx.keys[possibleIdx].pubkey; + forkedIx.keys[possibleIdx].pubkey = forkPubkey; + + await Promise.all([ + expectIxOk(connection, [approveIx, ix], [payer, coreMessage]), + expectIxOk(connection, [forkedApproveIx, forkedIx], [payer, forkCoreMessage]), + ]); + }); + } + } const transferAuthority = anchor.web3.Keypair.generate(); for (const { chain, decimals, address } of wrappedMints) { diff --git a/solana/tests/02__tokenBridge/098__upgradeContract.ts b/solana/tests/02__tokenBridge/098__upgradeContract.ts index 8fd72c9f0d..76f906815d 100644 --- a/solana/tests/02__tokenBridge/098__upgradeContract.ts +++ b/solana/tests/02__tokenBridge/098__upgradeContract.ts @@ -1,29 +1,28 @@ import { - parseVaa, - CHAIN_ID_SOLANA, CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + parseVaa, tryNativeToHexString, } from "@certusone/wormhole-sdk"; import { GovernanceEmitter, - MockGuardians, MockEmitter, + MockGuardians, } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; import { execSync } from "child_process"; import * as fs from "fs"; import { + ETHEREUM_TOKEN_BRIDGE_ADDRESS, + GOVERNANCE_EMITTER_ADDRESS, GUARDIAN_KEYS, + TOKEN_BRIDGE_GOVERNANCE_MODULE, expectIxErr, expectIxOk, invokeVerifySignaturesAndPostVaa, loadProgramBpf, - ETHEREUM_DEADBEEF_TOKEN_ADDRESS, - TOKEN_BRIDGE_GOVERNANCE_MODULE, - ETHEREUM_TOKEN_BRIDGE_ADDRESS, } from "../helpers"; import * as tokenBridge from "../helpers/tokenBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; const ARTIFACTS_PATH = `${__dirname}/../artifacts/wormhole_token_bridge_solana.so`; diff --git a/solana/tests/02__tokenBridge/100__registerChain.ts b/solana/tests/02__tokenBridge/100__registerChain.ts index 112650c3a7..c0a54fb533 100644 --- a/solana/tests/02__tokenBridge/100__registerChain.ts +++ b/solana/tests/02__tokenBridge/100__registerChain.ts @@ -13,16 +13,16 @@ import { import * as anchor from "@coral-xyz/anchor"; import { expect } from "chai"; import { + GOVERNANCE_EMITTER_ADDRESS, GUARDIAN_KEYS, OPTIMISM_TOKEN_BRIDGE_ADDRESS, + TOKEN_BRIDGE_GOVERNANCE_MODULE, expectDeepEqual, expectIxErr, expectIxOk, processVaa, - TOKEN_BRIDGE_GOVERNANCE_MODULE, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../helpers/coreBridge"; import * as tokenBridge from "../helpers/tokenBridge"; // Mock governance emitter and guardian. @@ -129,9 +129,6 @@ describe("Token Bridge -- Instruction: Register Chain", () => { ); expectDeepEqual(registeredData, legacyRegisteredData); - const encodedVaaInfo = await connection.getAccountInfo(encodedVaa); - expect(encodedVaaInfo).is.null; - // Save for later. localVariables.set("signedVaa", signedVaa); }); diff --git a/solana/tests/02__tokenBridge/102__secureRegisteredEmitter.ts b/solana/tests/02__tokenBridge/102__secureRegisteredEmitter.ts index ef8af58cf6..1efc5ee1d2 100644 --- a/solana/tests/02__tokenBridge/102__secureRegisteredEmitter.ts +++ b/solana/tests/02__tokenBridge/102__secureRegisteredEmitter.ts @@ -36,14 +36,10 @@ describe("Token Bridge -- Instruction: Secure Registered Emitter", () => { foreignEmitter ); - const ix = await tokenBridge.secureRegisteredEmitterIx( - program, - { - payer: payer.publicKey, - legacyRegisteredEmitter: legacyRegistered, - }, - { init: {} } - ); + const ix = await tokenBridge.secureRegisteredEmitterIx(program, { + payer: payer.publicKey, + legacyRegisteredEmitter: legacyRegistered, + }); await expectIxOk(connection, [ix], [payer]); const registered = tokenBridge.RegisteredEmitter.address(program.programId, CHAIN_ID_ETH); @@ -66,15 +62,11 @@ describe("Token Bridge -- Instruction: Secure Registered Emitter", () => { it("Cannot Invoke `secure_registered_emitter` with Same Registered Emitter", async () => { const legacyRegistered = localVariables.get("legacyRegistered") as anchor.web3.PublicKey; - const ix = await tokenBridge.secureRegisteredEmitterIx( - program, - { - payer: payer.publicKey, - legacyRegisteredEmitter: legacyRegistered, - }, - { init: {} } - ); - await expectIxErr(connection, [ix], [payer], "EmitterAlreadyRegistered"); + const ix = await tokenBridge.secureRegisteredEmitterIx(program, { + payer: payer.publicKey, + legacyRegisteredEmitter: legacyRegistered, + }); + await expectIxErr(connection, [ix], [payer], "already in use"); }); }); }); diff --git a/solana/tests/03__mockCpi/100__coreBridge.ts b/solana/tests/03__mockCpi/100__coreBridge.ts index 186cfdd6ca..050bd41950 100644 --- a/solana/tests/03__mockCpi/100__coreBridge.ts +++ b/solana/tests/03__mockCpi/100__coreBridge.ts @@ -71,7 +71,6 @@ describe("Mock CPI -- Core Bridge", () => { const txDetails = await expectIxOkDetails(connection, [ix], [payer]); const messageData = await coreBridge.PostedMessageV1.fromAccountAddress(connection, message); - console.log("messageData", messageData); expectDeepEqual(messageData, { consistencyLevel: 32, emitterAuthority: anchor.web3.PublicKey.default, diff --git a/solana/tests/03__mockCpi/200__tokenBridge.ts b/solana/tests/03__mockCpi/200__tokenBridge.ts index 731d46d403..de894155d7 100644 --- a/solana/tests/03__mockCpi/200__tokenBridge.ts +++ b/solana/tests/03__mockCpi/200__tokenBridge.ts @@ -22,7 +22,7 @@ import { createAssociatedTokenAccountOffCurve, expectDeepEqual, expectIxOk, - invokeVerifySignaturesAndPostVaa, + processVaa, } from "../helpers"; import * as coreBridge from "../helpers/coreBridge"; import * as mockCpi from "../helpers/mockCpi"; @@ -330,10 +330,11 @@ describe("Mock CPI -- Token Bridge", () => { [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14] ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -347,7 +348,6 @@ describe("Mock CPI -- Token Bridge", () => { const { coreBridgeProgram, payerToken, - postedVaa, claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, custodyToken: tokenBridgeCustodyToken, @@ -373,7 +373,7 @@ describe("Mock CPI -- Token Bridge", () => { recipientToken, recipient, payerToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeCustodyToken, @@ -408,10 +408,11 @@ describe("Mock CPI -- Token Bridge", () => { payload ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -424,7 +425,6 @@ describe("Mock CPI -- Token Bridge", () => { const legacyRegisteredEmitterDerive = true; const { coreBridgeProgram, - postedVaa, claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, custodyToken: tokenBridgeCustodyToken, @@ -452,7 +452,7 @@ describe("Mock CPI -- Token Bridge", () => { tokenBridgeProgramRedeemerAuthority: programRedeemerAuthority, tokenBridgeCustomRedeemerAuthority: null, dstToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeCustodyToken, @@ -498,10 +498,11 @@ describe("Mock CPI -- Token Bridge", () => { payload ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -512,7 +513,6 @@ describe("Mock CPI -- Token Bridge", () => { const legacyRegisteredEmitterDerive = true; const { coreBridgeProgram, - postedVaa, claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, custodyToken: tokenBridgeCustodyToken, @@ -535,7 +535,7 @@ describe("Mock CPI -- Token Bridge", () => { tokenBridgeProgramRedeemerAuthority: null, tokenBridgeCustomRedeemerAuthority: customRedeemerAuthority, dstToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeCustodyToken, @@ -577,10 +577,11 @@ describe("Mock CPI -- Token Bridge", () => { [0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14] ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -604,7 +605,6 @@ describe("Mock CPI -- Token Bridge", () => { const { coreBridgeProgram, payerToken, - postedVaa, claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, wrappedAsset: tokenBridgeWrappedAsset, @@ -629,7 +629,7 @@ describe("Mock CPI -- Token Bridge", () => { recipientToken, recipient, payerToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeWrappedMint: wrappedMint, @@ -682,10 +682,11 @@ describe("Mock CPI -- Token Bridge", () => { payload ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -697,7 +698,7 @@ describe("Mock CPI -- Token Bridge", () => { const legacyRegisteredEmitterDerive = true; const { coreBridgeProgram, - postedVaa, + claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, wrappedAsset: tokenBridgeWrappedAsset, @@ -724,7 +725,7 @@ describe("Mock CPI -- Token Bridge", () => { tokenBridgeProgramRedeemerAuthority: programRedeemerAuthority, tokenBridgeCustomRedeemerAuthority: null, dstToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeWrappedMint: wrappedMint, @@ -778,10 +779,11 @@ describe("Mock CPI -- Token Bridge", () => { payload ); - await invokeVerifySignaturesAndPostVaa( + const encodedVaa = await processVaa( mockCpi.getCoreBridgeProgram(program), payer, - signedVaa + signedVaa, + GUARDIAN_SET_INDEX ); const parsed = parseVaa(signedVaa); @@ -791,7 +793,6 @@ describe("Mock CPI -- Token Bridge", () => { const legacyRegisteredEmitterDerive = true; const { coreBridgeProgram, - postedVaa, claim: tokenBridgeClaim, registeredEmitter: tokenBridgeRegisteredEmitter, wrappedAsset: tokenBridgeWrappedAsset, @@ -813,7 +814,7 @@ describe("Mock CPI -- Token Bridge", () => { tokenBridgeProgramRedeemerAuthority: null, tokenBridgeCustomRedeemerAuthority: customRedeemerAuthority, dstToken, - postedVaa, + vaa: encodedVaa, tokenBridgeClaim, tokenBridgeRegisteredEmitter, tokenBridgeWrappedMint: wrappedMint, diff --git a/solana/tests/accounts/fork.wrapped_asset_2.json b/solana/tests/accounts/fork.wrapped_asset_2.json new file mode 100644 index 0000000000..937b257989 --- /dev/null +++ b/solana/tests/accounts/fork.wrapped_asset_2.json @@ -0,0 +1,13 @@ +{ + "pubkey": "CQn6Ts4su6V7upffbEthe2xsVnSxMdTC42yZWKRveXkU", + "account": { + "lamports": 1134480, + "data": [ + "AgAAAAAAAAAAAAAAAAAAAAutwN663A3rrcDeutwN663A3hI=", + "base64" + ], + "owner": "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/accounts/fork.wrapped_mint_2.json b/solana/tests/accounts/fork.wrapped_mint_2.json new file mode 100644 index 0000000000..1721d826a3 --- /dev/null +++ b/solana/tests/accounts/fork.wrapped_mint_2.json @@ -0,0 +1,13 @@ +{ + "pubkey": "CAjoti21mmQZxtdTdmrfUL7oCzA1Yqa3u6NdWvFkmqej", + "account": { + "lamports": 1461600, + "data": [ + "AQAAAJdz8e+O65iVdMpxBFsTrA0MEUaiThB5GX5TF1UZTdCWAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/accounts/fork.wrapped_mint_2_metadata.json b/solana/tests/accounts/fork.wrapped_mint_2_metadata.json new file mode 100644 index 0000000000..2123c7b11a --- /dev/null +++ b/solana/tests/accounts/fork.wrapped_mint_2_metadata.json @@ -0,0 +1,13 @@ +{ + "pubkey": "BR9RYXcjBzyKYVfze3K2SbFFS5DG78UUfUf1Peh16uPz", + "account": { + "lamports": 15616720, + "data": [ + "BJdz8e+O65iVdMpxBFsTrA0MEUaiThB5GX5TF1UZTdCWpe8v6jfpbmvU7bkDGbcuAh3Sj2/vjOf88Q5w7HEW9FwgAAAARGVhZCBiZWVmLiBNb28uIChXb3JtaG9sZSkAAAAAAAAKAAAAREVBREJFRUYAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8BAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ==", + "base64" + ], + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/accounts/wrapped_asset_2.json b/solana/tests/accounts/wrapped_asset_2.json new file mode 100644 index 0000000000..31b6671aab --- /dev/null +++ b/solana/tests/accounts/wrapped_asset_2.json @@ -0,0 +1,13 @@ +{ + "pubkey": "CfBrfpS7cCNU5m5FtHvV7mPoURa71kT34Mf9XP8dpjmb", + "account": { + "lamports": 1134480, + "data": [ + "AgAAAAAAAAAAAAAAAAAAAAutwN663A3rrcDeutwN663A3hI=", + "base64" + ], + "owner": "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/accounts/wrapped_mint_2.json b/solana/tests/accounts/wrapped_mint_2.json new file mode 100644 index 0000000000..c85a349827 --- /dev/null +++ b/solana/tests/accounts/wrapped_mint_2.json @@ -0,0 +1,13 @@ +{ + "pubkey": "ATdV7cKoJnw3s6kaoY2jrQAnoc3wg3PmGpHFdUuHYK67", + "account": { + "lamports": 1461600, + "data": [ + "AQAAAG2mxeNIQmq+oYG6Q+ikf8dkz3xpCTRyA5kQi+2rP4XlAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "base64" + ], + "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/accounts/wrapped_mint_2_metadata.json b/solana/tests/accounts/wrapped_mint_2_metadata.json new file mode 100644 index 0000000000..e269db173f --- /dev/null +++ b/solana/tests/accounts/wrapped_mint_2_metadata.json @@ -0,0 +1,13 @@ +{ + "pubkey": "6XqcNZL4yn8X8saab9fMuJRxZFEtNyp96DCeDTkqxduY", + "account": { + "lamports": 15616720, + "data": [ + "BG2mxeNIQmq+oYG6Q+ikf8dkz3xpCTRyA5kQi+2rP4XljIuJan6XgLKM02S4R3AtRabSXd98nOFQLCEcKgauUsggAAAARGVhZCBiZWVmLiBNb28uAAAAAAAAAAAAAAAAAAAAAAAKAAAAREVBREJFRUYAAMgAAAB7CiAgIndvcm1ob2xlQ2hhaW5JZCI6IDIsCiAgImNhbm9uaWNhbEFkZHJlc3MiOiAiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMGJhZGMwZGViYWRjMGRlYmFkYzBkZWJhZGMwZGViYWRjMGRlIiwKICAibmF0aXZlRGVjaW1hbHMiOiAxOAp9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAfMBAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ==", + "base64" + ], + "owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + "executable": false, + "rentEpoch": 0 + } +} \ No newline at end of file diff --git a/solana/tests/helpers/consts.ts b/solana/tests/helpers/consts.ts index ca54ce02c8..a2da172b66 100644 --- a/solana/tests/helpers/consts.ts +++ b/solana/tests/helpers/consts.ts @@ -54,6 +54,10 @@ export const ETHEREUM_TOKEN_ADDRESS_MAX_TWO = tryNativeToUint8Array( "0x0000000000000000000000000000000000000002", "ethereum" ); +export const ETHEREUM_TOKEN_ALREADY_CREATED = tryNativeToUint8Array( + "0x00000badc0debadc0debadc0debadc0debadc0de", + "ethereum" +); export const OPTIMISM_TOKEN_BRIDGE_ADDRESS = "0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b"; @@ -89,3 +93,5 @@ export const COMMON_EMITTER = Keypair.fromSecretKey( export const TOKEN_BRIDGE_GOVERNANCE_MODULE = "000000000000000000000000000000000000000000546f6b656e427269646765"; + +export const GOVERNANCE_EMITTER_ADDRESS = new PublicKey("11111111111111111111111111111115"); diff --git a/solana/tests/helpers/coreBridge/consts.ts b/solana/tests/helpers/coreBridge/consts.ts index ab2edc61b6..8e5ba1c81b 100644 --- a/solana/tests/helpers/coreBridge/consts.ts +++ b/solana/tests/helpers/coreBridge/consts.ts @@ -1,8 +1,4 @@ -import { PublicKey } from "@solana/web3.js"; - export type ProgramId = | "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" // mainnet | "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5" // testnet | "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; // localnet - -export const GOVERNANCE_EMITTER_ADDRESS = new PublicKey("11111111111111111111111111111115"); diff --git a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/closeEncodedVaa.ts b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/closeEncodedVaa.ts new file mode 100644 index 0000000000..da83f3f98b --- /dev/null +++ b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/closeEncodedVaa.ts @@ -0,0 +1,14 @@ +import { PublicKey } from "@solana/web3.js"; +import { CoreBridgeProgram } from "../.."; + +export type CloseEncodedVaaContext = { + writeAuthority: PublicKey; + encodedVaa: PublicKey; +}; + +export async function closeEncodedVaaIx( + program: CoreBridgeProgram, + accounts: CloseEncodedVaaContext +) { + return program.methods.closeEncodedVaa().accounts(accounts).instruction(); +} diff --git a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/index.ts b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/index.ts index da69b81b11..60973df06e 100644 --- a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/index.ts +++ b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/index.ts @@ -1,3 +1,5 @@ +export * from "./closeEncodedVaa"; export * from "./initEncodedVaa"; export * from "./postVaaV1"; export * from "./processEncodedVaa"; +export * from "./writeEncodedVaa"; diff --git a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/postVaaV1.ts b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/postVaaV1.ts index 9a72dc2e19..e202a3bcc5 100644 --- a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/postVaaV1.ts +++ b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/postVaaV1.ts @@ -5,21 +5,17 @@ import { ethers } from "ethers"; export type PostVaaV1Context = { writeAuthority: PublicKey; - vaa: PublicKey; + encodedVaa: PublicKey; postedVaa?: PublicKey; }; -export type PostVaaV1Directive = { tryOnce: {} }; - -export async function postVaaV1Ix( - program: CoreBridgeProgram, - accounts: PostVaaV1Context, - directive: PostVaaV1Directive -) { - let { writeAuthority, vaa, postedVaa } = accounts; +export async function postVaaV1Ix(program: CoreBridgeProgram, accounts: PostVaaV1Context) { + let { writeAuthority, encodedVaa, postedVaa } = accounts; if (postedVaa === undefined) { - const vaaBuf = await program.account.encodedVaa.fetch(vaa).then((vaaData) => vaaData.buf); + const vaaBuf = await program.account.encodedVaa + .fetch(encodedVaa) + .then((vaaData) => vaaData.buf); const numSignatures = vaaBuf.readUInt8(5); const message = vaaBuf.subarray(6 + 66 * numSignatures); @@ -30,7 +26,7 @@ export async function postVaaV1Ix( } return program.methods - .postVaaV1(directive) - .accounts({ writeAuthority, vaa, postedVaa }) + .postVaaV1() + .accounts({ writeAuthority, encodedVaa, postedVaa }) .instruction(); } diff --git a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/processEncodedVaa.ts b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/processEncodedVaa.ts index 552cbbb50f..debe027529 100644 --- a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/processEncodedVaa.ts +++ b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/processEncodedVaa.ts @@ -1,31 +1,15 @@ import { PublicKey } from "@solana/web3.js"; import { CoreBridgeProgram } from "../.."; -export type ProcessEncodedVaaContext = { +export type VerifyEncodedVaaV1 = { writeAuthority: PublicKey; encodedVaa: PublicKey; - guardianSet: PublicKey | null; + guardianSet: PublicKey; }; -export type ProcessEncodedVaaDirective = - | { - closeVaaAccount: {}; - } - | { - write: { - index: number; - data: Buffer; - }; - } - | { verifySignaturesV1: {} }; - -export async function processEncodedVaaIx( +export async function verifyEncodedVaaV1Ix( program: CoreBridgeProgram, - accounts: ProcessEncodedVaaContext, - directive: ProcessEncodedVaaDirective + accounts: VerifyEncodedVaaV1 ) { - return program.methods - .processEncodedVaa(directive) - .accounts(accounts) - .instruction(); + return program.methods.verifyEncodedVaaV1().accounts(accounts).instruction(); } diff --git a/solana/tests/helpers/coreBridge/instructions/parseAndVerify/writeEncodedVaa.ts b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/writeEncodedVaa.ts new file mode 100644 index 0000000000..b3c51ed891 --- /dev/null +++ b/solana/tests/helpers/coreBridge/instructions/parseAndVerify/writeEncodedVaa.ts @@ -0,0 +1,20 @@ +import { PublicKey } from "@solana/web3.js"; +import { CoreBridgeProgram } from "../.."; + +export type WriteEncodedVaaContext = { + writeAuthority: PublicKey; + encodedVaa: PublicKey; +}; + +export type WriteEncodedVaaArgs = { + index: number; + data: Buffer; +}; + +export async function writeEncodedVaaIx( + program: CoreBridgeProgram, + accounts: WriteEncodedVaaContext, + args: WriteEncodedVaaArgs +) { + return program.methods.writeEncodedVaa(args).accounts(accounts).instruction(); +} diff --git a/solana/tests/helpers/coreBridge/instructions/postMessage/closeMessageV1.ts b/solana/tests/helpers/coreBridge/instructions/postMessage/closeMessageV1.ts new file mode 100644 index 0000000000..62a6c74b71 --- /dev/null +++ b/solana/tests/helpers/coreBridge/instructions/postMessage/closeMessageV1.ts @@ -0,0 +1,15 @@ +import { PublicKey } from "@solana/web3.js"; +import { CoreBridgeProgram } from "../.."; + +export type ProcessMessageV1Context = { + emitterAuthority: PublicKey; + draftMessage: PublicKey; + closeAccountDestination: PublicKey; +}; + +export async function closeMessageV1Ix( + program: CoreBridgeProgram, + accounts: ProcessMessageV1Context +) { + return program.methods.closeMessageV1().accounts(accounts).instruction(); +} diff --git a/solana/tests/helpers/coreBridge/instructions/postMessage/finalizeMessageV1.ts b/solana/tests/helpers/coreBridge/instructions/postMessage/finalizeMessageV1.ts new file mode 100644 index 0000000000..ac8df346d5 --- /dev/null +++ b/solana/tests/helpers/coreBridge/instructions/postMessage/finalizeMessageV1.ts @@ -0,0 +1,14 @@ +import { PublicKey } from "@solana/web3.js"; +import { CoreBridgeProgram } from "../.."; + +export type FinalizeMessageV1Context = { + emitterAuthority: PublicKey; + draftMessage: PublicKey; +}; + +export async function finalizeMessageV1Ix( + program: CoreBridgeProgram, + accounts: FinalizeMessageV1Context +) { + return program.methods.finalizeMessageV1().accounts(accounts).instruction(); +} diff --git a/solana/tests/helpers/coreBridge/instructions/postMessage/index.ts b/solana/tests/helpers/coreBridge/instructions/postMessage/index.ts index ac1632b31b..f2e3b75359 100644 --- a/solana/tests/helpers/coreBridge/instructions/postMessage/index.ts +++ b/solana/tests/helpers/coreBridge/instructions/postMessage/index.ts @@ -1,2 +1,4 @@ +export * from "./closeMessageV1"; +export * from "./finalizeMessageV1"; export * from "./initMessageV1"; -export * from "./processMessageV1"; +export * from "./writeMessageV1"; diff --git a/solana/tests/helpers/coreBridge/instructions/postMessage/processMessageV1.ts b/solana/tests/helpers/coreBridge/instructions/postMessage/processMessageV1.ts deleted file mode 100644 index 7f9f2d0a5f..0000000000 --- a/solana/tests/helpers/coreBridge/instructions/postMessage/processMessageV1.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { PublicKey } from "@solana/web3.js"; -import { CoreBridgeProgram } from "../.."; - -export type ProcessMessageV1Context = { - emitterAuthority: PublicKey; - draftMessage: PublicKey; - closeAccountDestination: PublicKey | null; -}; - -export type ProcessMessageV1Directive = - | { - closeMessageAccount: {}; - } - | { - write: { - index: number; - data: Buffer; - }; - } - | { - finalize: {}; - }; - -export async function processMessageV1Ix( - program: CoreBridgeProgram, - accounts: ProcessMessageV1Context, - directive: ProcessMessageV1Directive -) { - return program.methods.processMessageV1(directive).accounts(accounts).instruction(); -} diff --git a/solana/tests/helpers/coreBridge/instructions/postMessage/writeMessageV1.ts b/solana/tests/helpers/coreBridge/instructions/postMessage/writeMessageV1.ts new file mode 100644 index 0000000000..0a8ac25409 --- /dev/null +++ b/solana/tests/helpers/coreBridge/instructions/postMessage/writeMessageV1.ts @@ -0,0 +1,20 @@ +import { PublicKey } from "@solana/web3.js"; +import { CoreBridgeProgram } from "../.."; + +export type WriteMessageV1Context = { + emitterAuthority: PublicKey; + draftMessage: PublicKey; +}; + +export type WriteMessageV1Args = { + index: number; + data: Buffer; +}; + +export async function writeMessageV1Ix( + program: CoreBridgeProgram, + accounts: WriteMessageV1Context, + args: WriteMessageV1Args +) { + return program.methods.writeMessageV1(args).accounts(accounts).instruction(); +} diff --git a/solana/tests/helpers/coreBridge/state/EncodedVaa.ts b/solana/tests/helpers/coreBridge/state/EncodedVaa.ts index 1fea0e6130..ddeecd3c5a 100644 --- a/solana/tests/helpers/coreBridge/state/EncodedVaa.ts +++ b/solana/tests/helpers/coreBridge/state/EncodedVaa.ts @@ -7,21 +7,16 @@ export enum ProcessingStatus { Verified = 2, } -export enum VaaVersion { - Unset = 0, - V1 = 1, -} - export class EncodedVaa { status: ProcessingStatus; writeAuthority: PublicKey; - version: VaaVersion; + version: number; buf: Buffer; private constructor( status: ProcessingStatus, writeAuthority: PublicKey, - version: VaaVersion, + version: number, buf: Buffer ) { this.status = status; @@ -52,16 +47,6 @@ export class EncodedVaa { } })(); - const version = (() => { - if (vaaVersion.unset !== undefined) { - return VaaVersion.Unset; - } else if (vaaVersion.v1 !== undefined) { - return VaaVersion.V1; - } else { - throw new Error("Invalid processing status"); - } - })(); - - return new EncodedVaa(status, writeAuthority, version, buf); + return new EncodedVaa(status, writeAuthority, vaaVersion, buf); } } diff --git a/solana/tests/helpers/coreBridge/testing.ts b/solana/tests/helpers/coreBridge/testing.ts index dd77628b1b..1e72e9854b 100644 --- a/solana/tests/helpers/coreBridge/testing.ts +++ b/solana/tests/helpers/coreBridge/testing.ts @@ -22,25 +22,37 @@ export async function expectEqualMessageAccounts( program: CoreBridgeProgram, messageSigner: Keypair, forkedMessageSigner: Keypair, - unreliable: boolean + unreliable: boolean, + sameEmitter: boolean = true ) { const connection = program.provider.connection; - if (unreliable) { - const [messageData, forkedMessageData] = await Promise.all([ - coreBridge.PostedMessageV1Unreliable.fromAccountAddress(connection, messageSigner.publicKey), - coreBridge.PostedMessageV1Unreliable.fromAccountAddress( - connection, - forkedMessageSigner.publicKey - ), - ]); + const [messageData, forkedMessageData] = await (async () => { + if (unreliable) { + return Promise.all([ + coreBridge.PostedMessageV1Unreliable.fromAccountAddress( + connection, + messageSigner.publicKey + ), + coreBridge.PostedMessageV1Unreliable.fromAccountAddress( + connection, + forkedMessageSigner.publicKey + ), + ]); + } else { + return Promise.all([ + coreBridge.PostedMessageV1.fromAccountAddress(connection, messageSigner.publicKey), + coreBridge.PostedMessageV1.fromAccountAddress(connection, forkedMessageSigner.publicKey), + ]); + } + })(); + + if (sameEmitter) { expectDeepEqual(messageData, forkedMessageData); } else { - const [messageData, forkedMessageData] = await Promise.all([ - coreBridge.PostedMessageV1.fromAccountAddress(connection, messageSigner.publicKey), - coreBridge.PostedMessageV1.fromAccountAddress(connection, forkedMessageSigner.publicKey), - ]); - expectDeepEqual(messageData, forkedMessageData); + const { emitter: _a, ...other } = messageData; + const { emitter: _b, ...forkedOther } = forkedMessageData; + expectDeepEqual(other, forkedOther); } } diff --git a/solana/tests/helpers/tokenBridge/instructions/governance/secureRegisteredEmitter.ts b/solana/tests/helpers/tokenBridge/instructions/governance/secureRegisteredEmitter.ts index 25dacd7825..79a655d92d 100644 --- a/solana/tests/helpers/tokenBridge/instructions/governance/secureRegisteredEmitter.ts +++ b/solana/tests/helpers/tokenBridge/instructions/governance/secureRegisteredEmitter.ts @@ -8,18 +8,9 @@ export type SecureRegisteredEmitterContext = { legacyRegisteredEmitter: PublicKey; }; -export type SecureRegisteredEmitterDirective = - | { - init: {}; - } - | { - closeLegacy: {}; - }; - export async function secureRegisteredEmitterIx( program: TokenBridgeProgram, - accounts: SecureRegisteredEmitterContext, - directive: SecureRegisteredEmitterDirective + accounts: SecureRegisteredEmitterContext ) { const programId = program.programId; @@ -34,7 +25,7 @@ export async function secureRegisteredEmitterIx( } return program.methods - .secureRegisteredEmitter(directive) + .secureRegisteredEmitter() .accounts({ payer, registeredEmitter, legacyRegisteredEmitter }) .instruction(); } diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/native.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/native.ts index d0ffdcb643..7a6162027c 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/native.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/native.ts @@ -15,7 +15,7 @@ import { Config, RegisteredEmitter, custodyAuthorityPda, custodyTokenPda } from export type LegacyCompleteTransferNativeContext = { payer: PublicKey; config?: PublicKey; // TODO: demonstrate this isn't needed in tests - postedVaa?: PublicKey; + vaa?: PublicKey; claim?: PublicKey; registeredEmitter?: PublicKey; recipientToken: PublicKey; @@ -43,7 +43,7 @@ export function legacyCompleteTransferNativeAccounts( let { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -63,8 +63,8 @@ export function legacyCompleteTransferNativeAccounts( config = Config.address(programId); } - if (postedVaa === undefined) { - postedVaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); + if (vaa === undefined) { + vaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); } if (claim === undefined) { @@ -105,7 +105,7 @@ export function legacyCompleteTransferNativeAccounts( return { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -135,7 +135,7 @@ export function legacyCompleteTransferNativeIx( const { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -156,22 +156,22 @@ export function legacyCompleteTransferNativeIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, { - pubkey: postedVaa, + pubkey: vaa!, isWritable: false, isSigner: false, }, { - pubkey: claim, + pubkey: claim!, isWritable: true, isSigner: false, }, { - pubkey: registeredEmitter, + pubkey: registeredEmitter!, isWritable: false, isSigner: false, }, @@ -181,12 +181,12 @@ export function legacyCompleteTransferNativeIx( isSigner: false, }, { - pubkey: payerToken, + pubkey: payerToken!, isWritable: true, isSigner: false, }, { - pubkey: custodyToken, + pubkey: custodyToken!, isWritable: true, isSigner: false, }, @@ -196,12 +196,12 @@ export function legacyCompleteTransferNativeIx( isSigner: false, }, { - pubkey: custodyAuthority, + pubkey: custodyAuthority!, isWritable: false, isSigner: false, }, { - pubkey: recipient, + pubkey: recipient!, isWritable: false, isSigner: false, }, @@ -211,12 +211,12 @@ export function legacyCompleteTransferNativeIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/wrapped.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/wrapped.ts index e3abc8b400..bb63458c26 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/wrapped.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransfer/wrapped.ts @@ -21,7 +21,7 @@ import { export type LegacyCompleteTransferWrappedContext = { payer: PublicKey; config?: PublicKey; // TODO: demonstrate this isn't needed in tests - postedVaa?: PublicKey; + vaa?: PublicKey; claim?: PublicKey; registeredEmitter?: PublicKey; recipientToken: PublicKey; @@ -58,7 +58,7 @@ export function legacyCompleteTransferWrappedAccounts( let { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -78,8 +78,8 @@ export function legacyCompleteTransferWrappedAccounts( config = Config.address(programId); } - if (postedVaa === undefined) { - postedVaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); + if (vaa === undefined) { + vaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); } if (claim === undefined) { @@ -128,7 +128,7 @@ export function legacyCompleteTransferWrappedAccounts( return { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -160,7 +160,7 @@ export function legacyCompleteTransferWrappedIx( const { payer, config, - postedVaa, + vaa, claim, registeredEmitter, recipientToken, @@ -187,7 +187,7 @@ export function legacyCompleteTransferWrappedIx( isSigner: false, }, { - pubkey: postedVaa, + pubkey: vaa, isWritable: false, isSigner: false, }, @@ -237,12 +237,12 @@ export function legacyCompleteTransferWrappedIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/native.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/native.ts index 9ff59d5491..f31c1a13a9 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/native.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/native.ts @@ -15,7 +15,7 @@ import { Config, RegisteredEmitter, custodyAuthorityPda, custodyTokenPda } from export type LegacyCompleteTransferWithPayloadNativeContext = { payer: PublicKey; config?: PublicKey; // TODO: demonstrate this isn't needed in tests - postedVaa?: PublicKey; + vaa?: PublicKey; claim?: PublicKey; registeredEmitter?: PublicKey; dstToken: PublicKey; @@ -39,7 +39,7 @@ export function legacyCompleteTransferWithPayloadNativeAccounts( let { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -59,8 +59,8 @@ export function legacyCompleteTransferWithPayloadNativeAccounts( config = Config.address(programId); } - if (postedVaa === undefined) { - postedVaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); + if (vaa === undefined) { + vaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); } if (claim === undefined) { @@ -99,7 +99,7 @@ export function legacyCompleteTransferWithPayloadNativeAccounts( return { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -121,7 +121,7 @@ export function legacyCompleteTransferWithPayloadNativeIx( const { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -145,22 +145,22 @@ export function legacyCompleteTransferWithPayloadNativeIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, { - pubkey: postedVaa, + pubkey: vaa!, isWritable: false, isSigner: false, }, { - pubkey: claim, + pubkey: claim!, isWritable: true, isSigner: false, }, { - pubkey: registeredEmitter, + pubkey: registeredEmitter!, isWritable: false, isSigner: false, }, @@ -170,7 +170,7 @@ export function legacyCompleteTransferWithPayloadNativeIx( isSigner: false, }, { - pubkey: redeemerAuthority, + pubkey: redeemerAuthority!, isWritable: false, isSigner: true, }, @@ -180,7 +180,7 @@ export function legacyCompleteTransferWithPayloadNativeIx( isSigner: false, }, { - pubkey: custodyToken, + pubkey: custodyToken!, isWritable: true, isSigner: false, }, @@ -190,12 +190,12 @@ export function legacyCompleteTransferWithPayloadNativeIx( isSigner: false, }, { - pubkey: custodyAuthority, + pubkey: custodyAuthority!, isWritable: false, isSigner: false, }, { - pubkey: rent, + pubkey: rent!, isWritable: false, isSigner: false, }, @@ -205,12 +205,12 @@ export function legacyCompleteTransferWithPayloadNativeIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/wrapped.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/wrapped.ts index 9a33ced118..2143c8d056 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/wrapped.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/completeTransferWithPayload/wrapped.ts @@ -21,7 +21,7 @@ import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; export type LegacyCompleteTransferWithPayloadWrappedContext = { payer: PublicKey; config?: PublicKey; // TODO: demonstrate this isn't needed in tests - postedVaa?: PublicKey; + vaa?: PublicKey; claim?: PublicKey; registeredEmitter?: PublicKey; dstToken: PublicKey; @@ -50,7 +50,7 @@ export function legacyCompleteTransferWithPayloadWrappedAccounts( let { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -70,8 +70,8 @@ export function legacyCompleteTransferWithPayloadWrappedAccounts( config = Config.address(programId); } - if (postedVaa === undefined) { - postedVaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); + if (vaa === undefined) { + vaa = PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); } if (claim === undefined) { @@ -118,7 +118,7 @@ export function legacyCompleteTransferWithPayloadWrappedAccounts( return { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -146,7 +146,7 @@ export function legacyCompleteTransferWithPayloadWrappedIx( const { payer, config, - postedVaa, + vaa, claim, registeredEmitter, dstToken, @@ -177,7 +177,7 @@ export function legacyCompleteTransferWithPayloadWrappedIx( isSigner: false, }, { - pubkey: postedVaa, + pubkey: vaa, isWritable: false, isSigner: false, }, @@ -232,12 +232,12 @@ export function legacyCompleteTransferWithPayloadWrappedIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/createOrUpdateWrapped.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/createOrUpdateWrapped.ts index c333583909..bfb23d9c52 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/createOrUpdateWrapped.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/createOrUpdateWrapped.ts @@ -25,7 +25,7 @@ export type LegacyCreateOrUpdateWrappedContext = { payer: PublicKey; config?: PublicKey; // TODO: demonstrate this isn't needed in tests registeredEmitter?: PublicKey; - postedVaa?: PublicKey; + vaa?: PublicKey; claim?: PublicKey; wrappedMint?: PublicKey; wrappedAsset?: PublicKey; @@ -51,7 +51,7 @@ export function legacyCreateOrUpdateWrappedIx( payer, config, registeredEmitter, - postedVaa, + vaa, claim, wrappedMint, wrappedAsset, @@ -78,8 +78,8 @@ export function legacyCreateOrUpdateWrappedIx( ); } - if (postedVaa === undefined) { - postedVaa = coreBridge.PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); + if (vaa === undefined) { + vaa = coreBridge.PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); } if (claim === undefined) { @@ -132,7 +132,7 @@ export function legacyCreateOrUpdateWrappedIx( isSigner: false, }, { - pubkey: postedVaa, + pubkey: vaa, isWritable: false, isSigner: false, }, @@ -167,22 +167,22 @@ export function legacyCreateOrUpdateWrappedIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: SystemProgram.programId, isWritable: false, isSigner: false, }, { - pubkey: SystemProgram.programId, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: mplTokenMetadataProgram, isWritable: false, isSigner: false, }, { - pubkey: mplTokenMetadataProgram, + pubkey: coreBridgeProgram, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/governance/upgradeContract.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/governance/upgradeContract.ts index be740bd2e1..ecebbae7c1 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/governance/upgradeContract.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/governance/upgradeContract.ts @@ -30,7 +30,6 @@ export type LegacyUpgradeContractContext = { rent?: PublicKey; clock?: PublicKey; bpfLoaderUpgradeableProgram?: PublicKey; - coreBridgeProgram?: PublicKey; }; export function legacyUpgradeContractIx( @@ -53,12 +52,9 @@ export function legacyUpgradeContractIx( rent, clock, bpfLoaderUpgradeableProgram, - coreBridgeProgram, } = accounts; - if (coreBridgeProgram === undefined) { - coreBridgeProgram = coreBridgeProgramId(program); - } + const coreBridgeProgram = coreBridgeProgramId(program); if (postedVaa === undefined) { postedVaa = coreBridge.PostedVaaV1.address(coreBridgeProgram, Array.from(hash)); @@ -166,11 +162,6 @@ export function legacyUpgradeContractIx( isWritable: false, isSigner: false, }, - { - pubkey: coreBridgeProgram, - isWritable: false, - isSigner: false, - }, ]; const data = Buffer.alloc(1, 8); diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/native.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/native.ts index ffe995c6d5..819192746b 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/native.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/native.ts @@ -153,7 +153,7 @@ export function legacyTransferTokensNativeIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, @@ -168,22 +168,22 @@ export function legacyTransferTokensNativeIx( isSigner: false, }, { - pubkey: custodyToken, + pubkey: custodyToken!, isWritable: true, isSigner: false, }, { - pubkey: transferAuthority, + pubkey: transferAuthority!, isWritable: false, isSigner: false, }, { - pubkey: custodyAuthority, + pubkey: custodyAuthority!, isWritable: false, isSigner: false, }, { - pubkey: coreBridgeConfig, + pubkey: coreBridgeConfig!, isWritable: true, isSigner: false, }, @@ -193,27 +193,27 @@ export function legacyTransferTokensNativeIx( isSigner: true, }, { - pubkey: coreEmitter, + pubkey: coreEmitter!, isWritable: false, isSigner: false, }, { - pubkey: coreEmitterSequence, + pubkey: coreEmitterSequence!, isWritable: true, isSigner: false, }, { - pubkey: coreFeeCollector, + pubkey: coreFeeCollector!, isWritable: true, isSigner: false, }, { - pubkey: clock, + pubkey: clock!, isWritable: false, isSigner: false, }, { - pubkey: rent, + pubkey: rent!, isWritable: false, isSigner: false, }, @@ -223,12 +223,12 @@ export function legacyTransferTokensNativeIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/wrapped.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/wrapped.ts index 8299cc3f6b..177828f6c1 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/wrapped.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokens/wrapped.ts @@ -151,17 +151,17 @@ export function legacyTransferTokensWrappedIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, { - pubkey: srcToken, + pubkey: srcToken!, isWritable: true, isSigner: false, }, { - pubkey: srcOwner, + pubkey: srcOwner!, isWritable: false, isSigner: false, }, @@ -171,17 +171,17 @@ export function legacyTransferTokensWrappedIx( isSigner: false, }, { - pubkey: wrappedAsset, + pubkey: wrappedAsset!, isWritable: false, isSigner: false, }, { - pubkey: transferAuthority, + pubkey: transferAuthority!, isWritable: false, isSigner: false, }, { - pubkey: coreBridgeConfig, + pubkey: coreBridgeConfig!, isWritable: true, isSigner: false, }, @@ -191,27 +191,27 @@ export function legacyTransferTokensWrappedIx( isSigner: true, }, { - pubkey: coreEmitter, + pubkey: coreEmitter!, isWritable: false, isSigner: false, }, { - pubkey: coreEmitterSequence, + pubkey: coreEmitterSequence!, isWritable: true, isSigner: false, }, { - pubkey: coreFeeCollector, + pubkey: coreFeeCollector!, isWritable: true, isSigner: false, }, { - pubkey: clock, + pubkey: clock!, isWritable: false, isSigner: false, }, { - pubkey: rent, + pubkey: rent!, isWritable: false, isSigner: false, }, @@ -221,12 +221,12 @@ export function legacyTransferTokensWrappedIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/native.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/native.ts index a2484bf022..0d44f45db2 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/native.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/native.ts @@ -157,7 +157,7 @@ export function legacyTransferTokensWithPayloadNativeIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, @@ -172,22 +172,22 @@ export function legacyTransferTokensWithPayloadNativeIx( isSigner: false, }, { - pubkey: custodyToken, + pubkey: custodyToken!, isWritable: true, isSigner: false, }, { - pubkey: transferAuthority, + pubkey: transferAuthority!, isWritable: false, isSigner: false, }, { - pubkey: custodyAuthority, + pubkey: custodyAuthority!, isWritable: false, isSigner: false, }, { - pubkey: coreBridgeConfig, + pubkey: coreBridgeConfig!, isWritable: true, isSigner: false, }, @@ -197,22 +197,22 @@ export function legacyTransferTokensWithPayloadNativeIx( isSigner: true, }, { - pubkey: coreEmitter, + pubkey: coreEmitter!, isWritable: false, isSigner: false, }, { - pubkey: coreEmitterSequence, + pubkey: coreEmitterSequence!, isWritable: true, isSigner: false, }, { - pubkey: coreFeeCollector, + pubkey: coreFeeCollector!, isWritable: true, isSigner: false, }, { - pubkey: clock, + pubkey: clock!, isWritable: false, isSigner: false, }, @@ -222,7 +222,7 @@ export function legacyTransferTokensWithPayloadNativeIx( isSigner: true, }, { - pubkey: rent, + pubkey: rent!, isWritable: false, isSigner: false, }, @@ -232,12 +232,12 @@ export function legacyTransferTokensWithPayloadNativeIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/wrapped.ts b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/wrapped.ts index 75aea1c6a8..592c45f294 100644 --- a/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/wrapped.ts +++ b/solana/tests/helpers/tokenBridge/legacy/instructions/transferTokensWithPayload/wrapped.ts @@ -159,17 +159,17 @@ export function legacyTransferTokensWithPayloadWrappedIx( isSigner: true, }, { - pubkey: config, + pubkey: config!, isWritable: false, isSigner: false, }, { - pubkey: srcToken, + pubkey: srcToken!, isWritable: true, isSigner: false, }, { - pubkey: srcOwner, + pubkey: srcOwner!, isWritable: false, isSigner: false, }, @@ -179,17 +179,17 @@ export function legacyTransferTokensWithPayloadWrappedIx( isSigner: false, }, { - pubkey: wrappedAsset, + pubkey: wrappedAsset!, isWritable: false, isSigner: false, }, { - pubkey: transferAuthority, + pubkey: transferAuthority!, isWritable: false, isSigner: false, }, { - pubkey: coreBridgeConfig, + pubkey: coreBridgeConfig!, isWritable: true, isSigner: false, }, @@ -199,32 +199,32 @@ export function legacyTransferTokensWithPayloadWrappedIx( isSigner: true, }, { - pubkey: coreEmitter, + pubkey: coreEmitter!, isWritable: false, isSigner: false, }, { - pubkey: coreEmitterSequence, + pubkey: coreEmitterSequence!, isWritable: true, isSigner: false, }, { - pubkey: coreFeeCollector, + pubkey: coreFeeCollector!, isWritable: true, isSigner: false, }, { - pubkey: clock, + pubkey: clock!, isWritable: false, isSigner: false, }, { - pubkey: senderAuthority, + pubkey: senderAuthority!, isWritable: false, isSigner: true, }, { - pubkey: rent, + pubkey: rent!, isWritable: false, isSigner: false, }, @@ -234,12 +234,12 @@ export function legacyTransferTokensWithPayloadWrappedIx( isSigner: false, }, { - pubkey: coreBridgeProgram, + pubkey: TOKEN_PROGRAM_ID, isWritable: false, isSigner: false, }, { - pubkey: TOKEN_PROGRAM_ID, + pubkey: coreBridgeProgram!, isWritable: false, isSigner: false, }, diff --git a/solana/tests/helpers/tokenBridge/legacy/state/WrappedAsset.ts b/solana/tests/helpers/tokenBridge/legacy/state/WrappedAsset.ts index 5af0f57029..e4b5ed49b3 100644 --- a/solana/tests/helpers/tokenBridge/legacy/state/WrappedAsset.ts +++ b/solana/tests/helpers/tokenBridge/legacy/state/WrappedAsset.ts @@ -1,3 +1,4 @@ +import { BN } from "@coral-xyz/anchor"; import { AccountInfo, Commitment, @@ -10,11 +11,18 @@ export class WrappedAsset { tokenChain: number; tokenAddress: number[]; nativeDecimals: number; + lastUpdatedSequence?: BN; - private constructor(tokenChain: number, tokenAddress: number[], nativeDecimals: number) { + private constructor( + tokenChain: number, + tokenAddress: number[], + nativeDecimals: number, + lastUpdatedSequence?: BN + ) { this.tokenChain = tokenChain; this.tokenAddress = tokenAddress; this.nativeDecimals = nativeDecimals; + this.lastUpdatedSequence = lastUpdatedSequence; } static address(programId: PublicKey, mint: PublicKey): PublicKey { @@ -46,12 +54,17 @@ export class WrappedAsset { } static deserialize(data: Buffer): WrappedAsset { - if (data.length != 35) { - throw new Error("data.length != 35"); - } const tokenChain = data.readUInt16LE(0); const tokenAddress = Array.from(data.subarray(2, 34)); const nativeDecimals = data.readUInt8(34); - return new WrappedAsset(tokenChain, tokenAddress, nativeDecimals); + + if (data.length === 43) { + const lastUpdatedSequence = new BN(data.subarray(35, 43), "le"); + return new WrappedAsset(tokenChain, tokenAddress, nativeDecimals, lastUpdatedSequence); + } else if (data.length == 35) { + return new WrappedAsset(tokenChain, tokenAddress, nativeDecimals); + } else { + throw new Error("data.length != 35 or != 37"); + } } } diff --git a/solana/tests/helpers/utils.ts b/solana/tests/helpers/utils.ts index 722db01a32..ac775399d4 100644 --- a/solana/tests/helpers/utils.ts +++ b/solana/tests/helpers/utils.ts @@ -33,8 +33,7 @@ import { Err, Ok } from "ts-results"; import { ethers } from "ethers"; import * as coreBridge from "./coreBridge"; import * as tokenBridge from "./tokenBridge"; -import { createSecp256k1Instruction } from "./"; -import { GOVERNANCE_EMITTER_ADDRESS } from "../../old-tests/helpers"; +import { createSecp256k1Instruction, GOVERNANCE_EMITTER_ADDRESS } from "./"; export type InvalidAccountConfig = { label: string; @@ -450,14 +449,13 @@ export async function processVaa( }); const endAfterInit = 840; - const firstProcessIx = await coreBridge.processEncodedVaaIx( + const firstProcessIx = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa: encodedVaa.publicKey, - guardianSet: null, }, - { write: { index: 0, data: signedVaa.subarray(0, endAfterInit) } } + { index: 0, data: signedVaa.subarray(0, endAfterInit) } ); if (vaaLen > endAfterInit) { @@ -471,27 +469,22 @@ export async function processVaa( for (let start = endAfterInit; start < vaaLen; start += chunkSize) { const end = Math.min(start + chunkSize, vaaLen); - const writeIx = await coreBridge.processEncodedVaaIx( + const writeIx = await coreBridge.writeEncodedVaaIx( program, { writeAuthority: payer.publicKey, encodedVaa: encodedVaa.publicKey, - guardianSet: null, }, - { write: { index: start, data: signedVaa.subarray(start, end) } } + { index: start, data: signedVaa.subarray(start, end) } ); if (verify && end === vaaLen) { const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 360_000 }); - const verifyIx = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa: encodedVaa.publicKey, - guardianSet: coreBridge.GuardianSet.address(program.programId, guardianSetIndex), - }, - { verifySignaturesV1: {} } - ); + const verifyIx = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa: encodedVaa.publicKey, + guardianSet: coreBridge.GuardianSet.address(program.programId, guardianSetIndex), + }); await expectIxOk(connection, [computeIx, writeIx, verifyIx], [payer]); } else { await expectIxOk(connection, [writeIx], [payer]); @@ -499,15 +492,11 @@ export async function processVaa( } } else if (verify) { const computeIx = ComputeBudgetProgram.setComputeUnitLimit({ units: 420_000 }); - const verifyIx = await coreBridge.processEncodedVaaIx( - program, - { - writeAuthority: payer.publicKey, - encodedVaa: encodedVaa.publicKey, - guardianSet: coreBridge.GuardianSet.address(program.programId, guardianSetIndex), - }, - { verifySignaturesV1: {} } - ); + const verifyIx = await coreBridge.verifyEncodedVaaV1Ix(program, { + writeAuthority: payer.publicKey, + encodedVaa: encodedVaa.publicKey, + guardianSet: coreBridge.GuardianSet.address(program.programId, guardianSetIndex), + }); await expectIxOk( program.provider.connection,