diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..39b3104d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,8 @@ +1. We are committed to providing a friendly, safe, and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. +2. Please avoid using overtly sexual aliases or other nicknames that might detract from a friendly, safe, and welcoming environment for all. +3. Please be kind and courteous. There's no need to be mean or rude. +4. Respect people's differences of opinion and the fact that every design or implementation choice involves a trade-off and numerous costs. There is seldom a correct answer. +5. Please limit unstructured critique. If you have solid ideas you want to experiment with, make a fork and see how it works. +6. We will exclude you from interaction if you insult, demean, or harass anyone. That is not welcome behavior. We interpret the term "harassment" as including the definition in the Citizen Code of Conduct; if you lack clarity about what might be included in that concept, please read their definition. In particular, we don't tolerate behavior that excludes people in socially marginalized groups. +7. Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please get in touch with the administrators. Whether you're a regular contributor or a newcomer, we care about making this community safe and having your back. +8. Any spamming, trolling, flaming, baiting, or other attention-stealing behavior is unwelcome. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..49e1331f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to Solana Axelar integration + +First off, thanks for taking the time to contribute! ๐ŸŽ‰๐Ÿ‘ + +The following is a set of guidelines for contributing to this project. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Code of Conduct + +The [Code of Conduct](CODE_OF_CONDUCT.md) governs this project and everyone participating in it. By participating, you are expected to uphold this code. + +## How Can I Contribute? + +### Reporting Bugs + +Before creating bug reports, please check if the issue has already been reported. If it has and the issue is still open, comment on the existing issue instead of opening a new one. + +### Pull Requests + +Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: + +1. Fork the repo and create your branch from `main`. +2. Add tests if you've added code that should be tested. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..bfc7494c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Eiger + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 8cfe8fd5..63d3f98a 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,70 @@ # Solana-Axelar Interoperability -This repository contains the under-development integration work between Solana and Axelar, enabling seamless cross-chain communication. The project includes General Message Passing (GMP) contracts and off-chain services for event monitoring and transaction broadcasting. +This repository contains the integration work between Solana and Axelar, enabling seamless cross-chain communication. The project includes General Message Passing (GMP) contracts and other Axelar core components. ## Table of Contents - [Repository contents](#repository-contents) - - [EVM Smart Contracts](#evm-smart-contracts) - [Solana contracts](#solana-contracts) - - [Offchain microservices](#offchain-microservices) + - [Utility crates](#utility-crates) + - [EVM Smart contracts](#evm-smart-contracts) + - [Related repositories](#related-repositories) - [Getting Started](#getting-started) - [Prerequisites](#prerequisites) - [Installation](#installation) - - [Usage](#usage) -- [Contributing](#contributing) -- [License](#license) - ## Repository contents -### EVM Smart Contracts -- **placeholder Contract name**: [Description Needed] -- **placeholder Contract name**: [Description Needed] - -### Solana contracts -- **Event Listener**: [Description Needed] +![image](https://github.com/user-attachments/assets/88008f1c-4096-4248-87b2-128b65cb8e41) -### Offchain microservices -- **Relayer**: [Description Needed] +The Solana-Axelar integration contains on-chain and off-chain components. +### Solana contracts +- [**Gateway**](solana/programs/axelar-solana-gateway/README.md): The core contract responsible for authenticating GMP messages. +- [**Gas Service**](solana/programs/axelar-solana-gas-service/README.md): Used for gas payments for the relayer. +- [**Interchain Token Service**](solana/programs/axelar-solana-its/README.md): Bridge tokens between chains. +- [**Multicall**](solana/programs/axelar-solana-multicall): Execute multiple actions from a single GMP message. +- [**Governance**](solana/programs/axelar-solana-governance/README.md): The governing entity over on-chain programs, responsible for program upgrades. +- [**Memo**](solana/programs/axelar-solana-memo-program): An example program that sends and receives GMP messages. -## Getting Started -### Prerequisites +#### Utility crates +- [**Axelar Executable**](solana/crates/axelar-executable/README.md): A set of libraries & interfaces that the destination program (3rd party integration) must implement. +- [**Axelar Solana Encoding**](solana/crates/axelar-solana-encoding/README.md): Encoding used by the Multisig Prover to encode the data in a way that the relayer & the Solana Gateway can interpret. +- [**Gateway Event Stack**](solana/crates/gateway-event-stack): The Relayer uses this crate to parse Gas Service & Gateway events. -- [List of prerequisites, e.g., Rust, Solana CLI, Axelar SDK, etc.] +### EVM Smart Contracts +- [**Axelar Memo**](evm-contracts/src/AxelarMemo.sol): A counterpart of the `axelar-solana-memo` program that acts as an example program used to send GMP messages back and forth Solana. +- [**Axelar Solana Multi Call**](evm-contracts/src/AxelarSolanaMultiCall.sol): An example contract used to showcase how to compose Multicall payloads for Solana. +- [**Solana Gateway Payload**](evm-contracts/src/ExampleEncoder.sol): A Solditiy library that can create Solana-specific GMP payloads. -### Installation -```bash -# Clone the repo (& init submodules) -git clone --recurse-submodules +## Related Repositories -# if the repo has already been cloned you need to fetch the submodules -git submodule update --init --recursive -``` +- [**Solana Relayer**](https://github.com/eigerco/axelar-solana-relayer): The off-chain entity that will route your messages to and from Solana. +- [**Relayer Core**](https://github.com/eigerco/axelar-relayer-core): All Axelar-related relayer infrastructure. Used as a core building block for the Solana Relayer. The Axelar-Starknet and Axlelar-Aleo relayers also use it. +- [**Multisig Prover**](https://github.com/eigerco/axelar-amplifier/tree/add-multisig-prover-sol-logic/contracts/multisig-prover): The entity on the Axelar chain that is responsible for encoding the data for the Relayer and the Solana Gateway +- [**Utility Scripts**](https://github.com/eigerco/solana-axelar-scripts): Deployment scripts; GMP testing scripts and other utilities. -### Usage -- [Instructions on how to run the project, deploy contracts, etc.] +## Getting Started -## Contributing +### Prerequisites -- [Guidelines for contributing to the repo, including pull request processes, coding standards, etc.] +- [Solana CLI (for running tests during development)](https://solana.com/docs/intro/installation) +- [Foundry (for running e2e tests, GMP examples between Solana and an EVM chain)](https://book.getfoundry.sh/getting-started/installation) -## License +### Installation -- [Details about the licensing of the project] +```bash +git clone git@github.com:eigerco/solana-axelar.git +cd solana +cargo xtask test +``` ## About [Eiger](https://www.eiger.co) -We are engineers. We contribute to various ecosystems by building low level implementations and core components. We work on several Axelar and Solana projects and believe that connecting these two is a very important goal to achieve cross-chain execution. +We are engineers. We contribute to various ecosystems by building low-level implementations and core components. We work on several Axelar and Solana projects and connecting these two is a fundamental goal to achieve cross-chain execution. Contact us at hello@eiger.co Follow us on [X/Twitter](https://x.com/eiger_co) diff --git a/solana/Cargo.lock b/solana/Cargo.lock index 87216ced..4811f776 100644 --- a/solana/Cargo.lock +++ b/solana/Cargo.lock @@ -851,6 +851,7 @@ dependencies = [ "ethers", "governance-gmp", "program-utils", + "role-management", "serde", "solana-logger", "solana-program", @@ -876,6 +877,7 @@ dependencies = [ "borsh 1.5.3", "evm-contracts-test-suite", "interchain-token-transfer-gmp", + "itertools 0.12.1", "mpl-token-metadata", "program-utils", "role-management", diff --git a/solana/crates/axelar-executable/README.md b/solana/crates/axelar-executable/README.md new file mode 100644 index 00000000..79ec836a --- /dev/null +++ b/solana/crates/axelar-executable/README.md @@ -0,0 +1,98 @@ +# Axelar Executable + +If we look at the event data that the EVM Gateway produces when making a [`gateway.callContract()`](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/contracts/interfaces/IAxelarGateway.sol#L19-L25) call when communicating with an external chain: +- `destinationContractAddress` is the identifier of _where_ the Message should land on the destination chain. +- `payload` is the payload data used for the contract call. The raw data that the Relayer must send to the destination contract +- `payloadHash` is the `keccak256` hash of the sent payload data, used by the recipient to ensure that the data is not tampered with. + +Relayer for the edge chain **must** know how to call a contract on the given edge chain so that the contract can properly process the Axelar GMP message. The same concept applies to Solana, that every contract that wants to be compatible with the Axelar protocol **must** implement the `axelar-executable` interface. Implementing the `axelar-executable` interface allows the Relayer to send transactions to it. + +![high level interactoin](https://github.com/user-attachments/assets/c676c8c6-8867-4560-a0cc-fa476b2b21a7) + +1. Relayer will compose a transaction that can interact with the destination program on Solana +2. `axelar-executable` exposes a function that allows the program to parse the Message into a format that it can work with +3. `axelar-executable` exposes a utility function that validates that all the relayer-provided accounts are valid and not malicious. It then performs a CPI call to the Solana Gateway to set the message status to `Executed`. + + +## Solana specific rundown + +> [!NOTE] +> For better clarity, read the following Solana docs: +> - [`Solana Account Model`](https://solana.com/docs/core/accounts) +> - [`Solana Transactions and Instructions`](https://solana.com/docs/core/transactions) +> - [`Solana CPI`](https://solana.com/docs/core/cpi) +> - [`Solana PDAs`](https://solana.com/docs/core/pda) + +**Accounts** + +A notable difference between Solana and EVM chains is that every interaction with some Solana programs must define an array of `accounts[]`. Accounts can be looked at as on-chain storage memory slots from Solidity. A contract can only read & write to the accounts provided when the instruction is created on the relayer/user level. The `accounts[]` must also include all the accounts that any internal CPI calls may require. Every storage slot the instruction may touch must be visible to the entity that crafts the transaction before submitting the TX for on-chain execution. + +**PDAs** + +Programs can have their accounts, called Program Derived Addresses (PDA for short), which sets the program ID as the owner. A contract can use PDAs to store data up to 10kb, or be a signer to make actoins on the behalf of a program. This can be imagined as having contract storage that only the on-chain program can modify. The PDAs must be deterministically derived. + +**CPI** + +Solana programs can call other programs (CPI - cross-program invocation), and a PDA can sign the calls on behalf of a program (this is because the program ID cannot be a signer itself; only its PDAs can be signers). Also, the CPI calls require a list of accounts to be proxied from the top-level call for the CPI call to operate correctly. + +Key points: +- If the proper accounts are not provided for a contract interaction, then **the call will fail**. +- The array of accounts **must** be known by the Relayer before calling the destination contract. +- The on-chain logic must validate that the provided accounts have been properly derived and are not malicious (e.g. checking if they were derived correctly, checking the expected owners, etc.) +- A Solana contract (represented by a `program_id`) cannot be a signer for a CPI call, but a PDA owned by the given `program_id` can be a signer. This is important for when the program makes a `gateway.validate_call()` CPI call + +## Providing the `accounts[]` in the payload + +Solana identifies an instance of the on-chain program by the contract address AND the provided accounts. This means that by having a different array of accounts passed to a program, we may communicate to a completely different instance of the same program. + +For example, the [SPL token program](https://spl.solana.com/token#creating-a-new-token-type) is a singular program everyone uses to create their on-chain tokens. By having a different token mint account, you are effectively talking to a different token โ€” all while the `program_id` has not even changed. + +This means that the base interface for GMP messages defined by the Axelar protocol is not expressive enough for communicating with Solana programs - because thereโ€™s no place to put the account arrays. Therefore a workaround is needed, where we need to provide the information about the accounts within the scope of the existing `ContractCall` data structure. + +Solanas `axelar-executable` now expects that the `payload` emitted on the source chain also includes all the `account[]` data for the Relayer to create a proper transaction. If the `account[]` is absent on the GMP call, the Relayer cannot know what accounts to provide when calling the destination contract. + +![Axelar Message Payload](https://github.com/user-attachments/assets/29a96677-83f5-4727-befa-3f815e31ad39) + + +The source chain that wants to interact with Solana must encode the messages in a specific format that the Solana destination contract understands, and the Relayer can understand. `Accounts[]` requirement also lets the Relayer deterministically derive the accounts when crafting the transaction. + +Currently, the `[0]th` byte of the payload indicates the encoding that specifies how the rest of the data is encoded: + +| value | meaning | +|--|--| +| 0b00000000 | The rest of the data is Borsh encoded | +| 0b00000001 | The rest of the data is ABI encoded | + +The way how accounts and the payload are encoded is encoding-specific. The `axelar-executable` and the Relayer maintainers can add new encoding support over time. As new chains get added to Axelar, they may not play nicely with ABI or Borsh (or just be expensive to compute). Making the encoding flexible gives us room to support new encodings in the future. + +## Examples + +| Item | Explanation | +|--|--| +| [`SolanaGatewayPayload.sol`](https://github.com/eigerco/solana-axelar/blob/main/evm-contracts/src/SolanaGatewayPayload.sol) | Can be used to encode the accounts together with the actual payload in Solidity so that the Solana relayer can properly interpret them | +| [`AxelarMemo.sol`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/evm-contracts/src/AxelarMemo.sol#L46) | An example contract showcasing how the Solidity contract would send a `string` message to Solana, while prefixing some arbitrary accounts | +| [`AxelarSolanaMemo::processor`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-memo-program/src/processor.rs#L33-L36) | Solana program example that implements `axelar-executable`; receives a string message and prints all the accounts it has received | +| [end to end unittest](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-memo-program/tests/evm-e2e/from_evm_to_solana.rs#L202) | A unit test you can run locally and see for yourself how it works together | + +## Differences from EVM + +| Action | EVM Axelar Executable | Solana Axelar Executable | +|--|--|--| +| Interface that the contract must implement | EVM contracts must inherit the [AxelarExecutable.sol](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/contracts/executable/AxelarExecutable.sol) base contract for the Relayer to be able to call it. | On Solana, there is no concept for inheriting contracts. There is only a single entry point for a Solana program, and the branching logic of how the raw bytes are interpreted is up to the contract logic. On Solana, a contract developer **must** try to parse the incoming bytes using [`parse_axelar_message()`](https://github.com/eigerco/solana-axelar-internal/blob/aee979d2c83d875a9255d73b4eb31eb0f38e544d/solana/crates/axelar-executable/src/lib.rs#L282) before attempting any other action. | +| Relayer calling `destination_contract.execute()` | The Relayer will encode the call with the full payload and call the destination contract; the `AxelarExecutable.sol` interface allows to process the incoming payload | The Relayer will split the raw payload from the `accounts[]` because the tx layout requires them to be split. It will encode the payload in a way that allows the destination contract to parse it using this library | +| Calling `validate_message()` | The `AxelarExecutable.sol` contract takes care of the internal cross-contract call to the Gateway to validate that the message has been approved | The Solana contract developer *must* immediately call [`validate_message()`](https://github.com/eigerco/solana-axelar-internal/blob/aee979d2c83d875a9255d73b4eb31eb0f38e544d/solana/crates/axelar-executable/src/lib.rs#L47) form the `axelar-executable` library. This internally will make a CPI call to the Gateway using the messages command id to create a short-lived PDA that will be the signer of the call. | +| Providing the raw payload | As tx arguments | Raw payload is stored on a PDA owned by the Gateway; it must be read from there | +| Providing the raw `accounts[]` from the original payload | _No such concept_ | Relayer will parse the payload, extract the provided accounts and append them in the order that they were provided in the original message | +| Providing accounts for the raw payload, gateway, etc | _No such concept_ | Relayer will prefix hardcoded accounts for the raw payload PDA | +| Validating payload hash | `AxelarExecutable.sol` will take care of hashing the raw payload from tx args and comparing it with the provided payload hash | `axelar_executable::validate_message` will validate all the arguments passed to the instruction (including accounts) match the ones defined on `MessagePayload PDA`, and ensure that the data hashes match. | + +You can see the anatomy of an instruction that the Solana Relayer will send to the destination program when sending the raw payload to it: + +![Anatomy of an ix](https://github.com/user-attachments/assets/0312abb4-fe7f-45c7-a8ae-1318489da9d2) + + +## Exceptions of the `accounts[]` rule: ITS & Governance + +[Interchain Token Service](https://github.com/axelarnetwork/interchain-token-service/blob/main/DESIGN.md#interchain-tokens) and [Governance contract](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/test/utils.js#L24-L44) have a legacy ABI interface that must be respected. This means that we cannot enforce arbitrary new encoding for the existing protocols; we only need them to be able to interact with Solana. As a result, the Relayer has special handling when interacting with ITS & Governance contracts; it will decode the `abi` encoded messages, introspect into the message contents and attempt to deterministically derive all the desired accounts for the action the message wants to make. This approach only works when the message layout is known beforehand (meaning that the Relayer can decode it) AND the Relayer has hardcoded custom logic to derive the accounts. This means that this special handling is not possible for the generic case. + +Also, ITS & Governance use a different entry point on [`axelar_executable::validate_with_gmp_metadata`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/crates/axelar-executable/src/lib.rs#L128C8-L128C34). The only difference is that the `accounts[]` validation is no longer done by `axelar_executable`. Instead, it becomes the responsibility of the destination contract itself to validate the provided `accounts[]`. diff --git a/solana/crates/axelar-solana-encoding/README.md b/solana/crates/axelar-solana-encoding/README.md new file mode 100644 index 00000000..6bd16bf8 --- /dev/null +++ b/solana/crates/axelar-solana-encoding/README.md @@ -0,0 +1,158 @@ +# Axelar Solana Encoding crate + +This crate defines utilities that are used on the following components: +- Encodes data on the [**Multisig Prover**](https://github.com/eigerco/axelar-amplifier/blob/acd6d68da408ff9ea8859debd3b04427b08f5be3/contracts/multisig-prover/src/encoding/mod.rs#L21) in a Merkelised way +- [**Relayer**](https://github.com/eigerco/axelar-solana-relayer) uses the encoded data to send many small transactions to the Axelar Solana Gateway to approve messages or rotate signer sets +- [**Axelar Solana Gateway**](../../programs/axelar-solana-gateway/README.md) uses the encoding crate to create hashes that are consistent between all implementations +- All components listed above use the data types defined in this crate. For a high-level overview, see [the root README.md](../../../README.md) + +## Using `borsh` + +While `abi` encoding can be used inside Solana programs, the ecosystem primarily has settled on using `borsh`. Borsh encoding is simple to use, relatively cheap (in compute unit consumption), and has JS libraries. It's the default used by the Anchor framework [[source]](https://solana.com/developers/courses/native-onchain-development/serialize-instruction-data-frontend#serialization). Also, Solana is not the only blockchain that uses borsh [[link]](https://docs.near.org/build/smart-contracts/anatomy/serialization), meaning it was a natural choice, and as the limitations above highlight - encoding and decoding the raw data is not the limitation. + +## Merkelising the data + +> [!NOTE] +> For a better understanding of the following chapter - [Wikipedia Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree). +> +> Our `axelar-solana-encoding` library protects against [second preimage attacks](https://en.wikipedia.org/wiki/Merkle_tree#Second_preimage_attack). + +The `axelar-solana-encoding` crate uses Merkle Trees to Merkelise the data and builds commitment schemes. This is necessary because Solana TX and compute limitations prevent doing _everything_ that the EVM gateway can do in a single TX. The defining property of `axelar-solana-encoding` allows the Relayer to send many small transactions without complex on-chain state tracking. Merkle Roots are the commitment values that tie all the small transactions together. + +![Merkle Tree visualisation](https://github.com/user-attachments/assets/6cce2c07-2299-497d-bfdb-e889d3ac67dc) + +The fundamental idea of the Merkle Tree: +- You can prove that an item is part of the set without requiring the whole set present (e.g. prove **that a message is part of the message batch** or a **verifier is part of a verifier set**) +- Each item of the set is represented as a Leaf Node. Each leaf node contains all the information about the set, such as size, domain separator, leaf node position, etc. +- Given a leaf node, proof (an array of hashes), and the Merkle root, you can prove that an item is part of a set. + +The unique property of this approach is that: +- we reduce the amount of data we need to expose for an action. For example, for 1000 items in a batch, the Merkle proof would be 10 hashes. +- we can verify each signature as a separate transaction by verifying that a verifier is part of the verifier set without passing the whole verifier set. +- We can verify that a message is similar to a message batch. + +Let's take a look at how we construct leaf nodes from verifier sets and message batches: + +![Merkelising the Data](https://github.com/user-attachments/assets/6bbb91c6-7ba1-4c35-a770-cead1a97dc1a) + +> [!NOTE] +> **Payload digest**: this is the data that the verifiers sign. It is a hash that consists of all the messages, verifiers, and other metadata. + +| Action | Verifier Set | Message batch | +|-|-|-| +|Base data structure layout|This is the base data representation without any extra metadata, representing a single verifier set|A vector of messages, aka a batch of messages. All other integrations (like EVM) operate directly on this data type| +|Constructing leaf node|A leaf node is constructed by flattening the data, extracting metadata like set size and verifier position, and injecting Axelar-specific information like the domain separator. We **don't** inject the "sigingin verifier set". |A leaf node is constructed by flattening the data, extracting metadata like set size and message position, and injecting Axelar-specific information like the domain separator. We also inject the "signing verifier set" so that every leaf node is tied directly to the verifier set that is signing it.| +|Constructing leaves|A simple iterator over the leaves|A simple iterator over the leaves| +|Merkle tree root|This is the logical equivalent of "signer set hash" from the EVM abi encoding|This is the logical equivalent of payload hash from the EVM abi encoding| +|Payload digest|We inject a "signing verifier set" (also a Merkle root) so that the payload digest knows the verifier set that will sign it. This allows us to have two logically tied data values: the unique hash for the verifier set and the hash that the verifiers are going to sign.|We use the Merkle root from ๐Ÿ‘†| + +### Execute Data + +> [!NOTE] +> **Execute Data**: This is the data that the Multisig Prover returns after getting all the signatures. It aggregates the signatures and all the data used to create a **payload digest**. The goal of the data is to allow the gateway to check that the verifiers have signed a payload digest and that the provided messages can be re-hashed to create the payload digest. + +![Execute Data layout](https://github.com/user-attachments/assets/7a6b784b-32a5-42cb-adee-26cdd430fe84) + +After the data has been Merkelised, the Multisig Prover neatly packs it together for the Relayer to consume. +It encodes: +- the verifier set that signed the data +- all the signatures and proofs of every signer in the set +- the payload digest (either of the verifier set or the message batch) + +As a result, this approach allows us to do the following: + +| Action | Description | Semantical difference from EVM | +|-|-|-| +| Verify that a verifier set is valid | The Merkle root (a hash), just as in the EVM version, acts as a unique identifier of the verifier set. It can be done via simple on-chain hash comparison. | None | +| Verify signature | Every signature in a signing verifier set can be validated as an individual transaction, tracking the progress on-chain | None | +| Approving messages | After all signatures have been approved for a given payload digest, we can check if a given message is part of an approved message batch, and if it is, then mark its status as "approved" in an on-chain state. | None | +| Rotating singers | After all signatures have been approved for a given payload digest, we can provide the new verifier set hash, together with the verifier set hash that signed the message, and reconstruct the "verifier set payload digest" on-chain, to check if it matches the one that has been signed over. The end goal of this indirection is to prevent malicious actors from providing "Message batch payload digest" as the hash of the new verifier set. | We don't reconstruct the new verifier set hash on-chain; we only operate on hashes | + +### Hashing data + +The hashing of data needs to be consistent across all users of this crate: +- Multisig Prover running in wasm on Axelar chain because it constructs the digests that the verifiers sign over; +- Relayer running on a server because it needs to use the hashes to compute PDAs when interacting with the Axelar Solana Gateway; +- Axelar Solana Gateway constructing data on-chain and validating that the hashes match + +Because of Solana's computing limitations, [a syscall is the best way to hash data](https://docs.rs/solana-program/2.1.5/src/solana_program/keccak.rs.html#118-141). Solana provides syscalls for multiple hash functions, but we settled on using the keccak256 hash function. +This means that our `axelar-solana-encoding` code has a branching mechanism that allows it to be generic over the hasher: +- on Solana, we leverage the syscall for a minimal compute unit footprint +- on Axelars CosmWASM runtime (Multisig Prover), we leverage the Rust-native keccak256 implementation +- on the Relayer, we leverage the Rust-native keccak256 implementation + +![Hashing the data](https://github.com/user-attachments/assets/fe3f4006-6131-40af-a775-df6291bb3f8f) + +For encoding the data, we use [udigest crate](https://docs.rs/udigest/0.2.2/udigest/encoding/index.html), which allows us to transform a set of data into a vector of bytes. [Read this article about hashing the data and creating digests](https://www.dfns.co/article/unambiguous-hashing). + +### Current limits of the Merkelised implementation +Now let's take a look at [the full requirements that the Axelar Solana Gateway must follow and see how it affects the `axelar-solana-encoding` scheme we use](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/contracts/gateway/INTEGRATION.md?plain=1#L261): + +| Limit | Minimum | Recommended | Axelar Solana Gateway | +|-|-|-|-| +| Cross-chain Message Size | 16 KB | 64 KB | < 10 KB | +| Signer Set Size | 40 signers | 100 signers | < 256 | +| Signature Verification | 27 signatures | 67 signatures | < 256 | +| Message Approval Batching | 1 | Configurable | Practically unlimited | +| Storage Limit for Messages | Practically unlimited (2^64) | Practically unlimited (2^64) | Practically unlimited | + +The message size is tackled purely on the Gateway and is not part of the `axelar-solana-encoding` scheme. The Merkeliesd data allows us to: +- have a signer set up for 256 participants +- verify a signature for every single signer +- allows us to have a practically unlimited amount of messages in a batch (limited by how many hashes we can do in a single tx) + +The _Storage Limit for Messages_ requirement is a given using Solana PDAs, no extra effort required from the Gateway or the encoding crate. + +--- + +## Understanding the EVM encoding for comparison sake + +To better understand how this approach differs from the EVM Gateways ABI encoding, let's analyze it. The EVM encoding works the following way: + +![evm abi encoding](https://github.com/user-attachments/assets/3e3c32a8-13fe-482a-9d9c-a9f4853a63cc) + +Summary from the **payload digest**: +- all messages in a batch get encoded and hashed in one go + - This means that to reconstruct the payload hash, the smart contract requires all of the messages to be directly available as function arguments +- all signers in a verifier set get hashed in one go + - Same as for messages: to reconstruct the signer hash, the smart contract requires all of the signers to be directly available as function arguments +- the verifiers that will sign the payload digest also get hashed, and their hash is part of it. This is a security measure. +- The verifiers sign over the payload digest + +How Gateway operates on the **execute data**: +- this is the piece of data that the Multisig Prover returns to the relayer +- relayer passes ExecuteData structure directly to the EVM Gateway smart contract + - The Gateway will reconstruct the payload digest and use the created hash to verify signatures. To rebuild the payload digest: + - on-chain logic will reconstruct the hash for all messages / new verifiers + - on-chain logic will reconstruct the hash of all verifiers to ensure that the verifier set has been approved +- Verify every signature in the batch against the created payload digest. If the quorum is met: + - for every message in the batch, mark it as "approved" by updating the Gateway contract state + +### Payload size implications + +Some napkin math to understand the implications of such a payload [let's take a look at the **minimal** requirements](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/contracts/gateway/INTEGRATION.md?plain=1#L261): + +| Data piece | Minimum | Size | Total | +|-|-|-|-| +| Signer Set Size | 40 signers | `address` size is 20 bytes | 20 * 40 = 800 bytes | +| Signature Verification | 27 signatures | `secp256k1 signature` is 65 bytes | 65 * 27 = 1755 bytes | +| Message Approval Batching | 1 | 20 bytes source chain name; ~50 bytes message id; ~30 bytes source address; 20 bytes contract address; 32 bytes payload hash | 20 + ~50 + ~30 + 20 + 32= ~152 bytes | +| | | | ~2707 bytes | + +This total size (2707 bytes) is just the _raw data_ for minimal requirements, excluding extra information added by the abi encoding itself, like padding the bytes between data types, len prefixes for arrays, etc. + +### Why such an approach will never work for Solana + +Solana has quite a few different limitations, both in how many actions can be done in a single transaction (the ceiling for max compute units) and how large the transaction can be. +2 most notable things to keep in mind about Solana's limitations: +- [Transaction size is capped at 1232 bytes](https://solana.com/docs/core/transactions#key-points). This tx info contains the raw data to send, as well as a list of all of the accounts to be used by the transaction (if you are reading this as a Solidity dev, imagine that you need to provide a list of `[]address` of every storage slot that your on-chain contract will try to read or mutate, including all the storage slots that an internal contract call may touch). This information itself also eats up precious bytes. There's also extra metadata, like the signatures, block hash, and header data, that eat away at the tx sizeโ€”the more sophisticated the contract, the more accounts it needs to access. +- Computationally, every operation on Solana has a cost, measured in compute units. The heavier the math operation ([e.g. division](https://solana.com/docs/programs/limitations#signed-division)), the more compute units it will take. Many operations like hashing and signature verification can leverage "syscalls" [(which also have a fixed cost)](https://github.com/anza-xyz/agave/blob/b7bbe36918f23d98e2e73502e3c4cba78d395ba9/program-runtime/src/compute_budget.rs#L133-L178) where the runtime calls a static function on the host machine, leveraging pre-compiled code instead of emulating heavy computations inside the virtual machine. + +The most significant limitation of 1232 bytes per tx means that it is impossible to pack the minimum required data (2707 bytes) into a single transaction. In the initial stages of the Axelar-Solana Gateway, we implemented a logic that would store the ExecuteData on-chain in a PDA (aka storage slot for drawing parallels with Solidity). This allowed us to get rid of the size limitations. However, we quickly discovered that computing the desired data in a single TX is impossible, and we require a multi-step computation model. One approach besides Merkelisng the flow would be introducing on-chain state tracking of ExecuteData processing. However, an internal discussion led to the conclusion that it brings no extra security measures, makes the process more expensive in terms of gas fees and introduces a lot of additional complexity. [(For details, see this public report on our attempt)](https://docs.google.com/document/d/1I3PQQ7H6oZNiayteJcrb6T1o2UHrRBcsAkSa7mOBCCY/edit?usp=sharing), we quickly found out that we cannot hash & verify the amount of data we require when "approving messages" on the gateway (reconstructing the payload digest and verifying signatures). Although this example used `bcs` encoding instead of `abi`, which was developed & maintained by Axelar, the internal encoding structure is the same as in the `abi` example described above. Our conclusion was: + - The maximum number of signers we can have is 5. It will be less, but never more than 5, depending on the other variables. + - The maximum number of messages in one batch is 3. Depending on the other variables, it will be less, but never more than 3. + - The maximum number of accounts is 20. Depending on the message size, it will be less, but never more than 20. + - The maximum message size is 635 bytes. Depending on the number of accounts, it will be smaller but never larger than 635 bytes. + - The bottleneck for the number of signers and number of messages per batch is the gateway. In contrast, the bottleneck for the message size and number of accounts is the destination contract. + +This meant that we just could not put enough data inside our transactions **and** we could not do enough computations in a **single** transaction. Hence why we had to redesign the whole approach of how we encode the data on the Multisig Prover side. We required splitting all of the work between many small transactions while still ensuring that the state of the computation is being tracked on-chain. diff --git a/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs b/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs index 06c81275..762d7bac 100644 --- a/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs +++ b/solana/crates/axelar-solana-gateway-test-fixtures/src/base.rs @@ -611,6 +611,8 @@ impl TestFixture { pub trait FindLog { /// Find the desired log fn find_log(&self, expected: &str) -> Option<&str>; + /// Find at least one of the expected logs + fn find_at_least_one_log(&self, expected: &[&str]) -> Option<&str>; } impl FindLog for BanksTransactionResultWithMetadata { @@ -622,7 +624,12 @@ impl FindLog for BanksTransactionResultWithMetadata { .map(std::string::String::as_str) }) } + + fn find_at_least_one_log(&self, expected: &[&str]) -> Option<&str> { + expected.iter().find_map(|x| self.find_log(x)) + } } + /// Add an upgradeable loader account to the context #[allow(clippy::impl_trait_in_params)] // Todo - remove this pub async fn add_upgradeable_loader_account( diff --git a/solana/crates/gateway-event-stack/src/lib.rs b/solana/crates/gateway-event-stack/src/lib.rs index a54b5379..0f8eaade 100644 --- a/solana/crates/gateway-event-stack/src/lib.rs +++ b/solana/crates/gateway-event-stack/src/lib.rs @@ -127,8 +127,8 @@ where let mut logs = log .as_ref() .trim() - .trim_start_matches("Program data:") - .split_whitespace() + .trim_start_matches("Program data: ") + .split(' ') .filter_map(decode_base64); let disc = logs .next() @@ -196,8 +196,8 @@ where let mut logs = log .as_ref() .trim() - .trim_start_matches("Program data:") - .split_whitespace() + .trim_start_matches("Program data: ") + .split(' ') .filter_map(decode_base64); let disc = logs .next() @@ -228,7 +228,9 @@ where let event = SplGasRefundedEvent::new(logs)?; GasServiceEvent::SplGasRefunded(event) } - _ => return Err(EventParseError::Other("unsupported discrimintant")), + _ => { + return Err(EventParseError::Other("unsupported discrimintant")); + } }; Ok(gas_service_event) @@ -271,6 +273,7 @@ fn handle_success_log(program_stack: &mut Vec>) { mod tests { use core::str::FromStr; + use axelar_solana_gas_service::processor::NativeGasPaidForContractCallEvent; use axelar_solana_gateway::processor::CallContractEvent; use pretty_assertions::assert_eq; use solana_sdk::pubkey::Pubkey; @@ -412,4 +415,49 @@ mod tests { assert_eq!(result, expected); } + + #[test] + fn test_gas_service_fixture() { + let logs = [ + "Program gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G invoke [1]", + "Program 11111111111111111111111111111111 invoke [2]", + "Program 11111111111111111111111111111111 success", + "Program data: bmF0aXZlIGdhcyBwYWlkIGZvciBjb250cmFjdCBjYWxs uHuSGR4VBBCRNPjze8Y91JXLTJnrh8qv2IxFZAjnrfI= ZXZt MHhkZWFkYmVlZg== /Qd2xw7aQmd/4PP+LMP3Kwouwb8mAfoKYiWkSoTQv5E= AAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= iBMAAAAAAAA=", + "Program gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G consumed 7199 of 400000 compute units", + "Program gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G success", + "Program mem7LhKWbKydCPk1TwNzeCvVSpoVx2mqxNuvjGgWAbG invoke [1]", + "Program log: Instruction: Native", + "Program log: Instruction: SendToGateway", + "Program gtwLjHAsfKAR6GWB4hzTUAA1w4SDdFMKamtGA5ttMEe invoke [2]", + "Program log: Instruction: Call Contract", + "Program data: Y2FsbCBjb250cmFjdF9fXw== 7JQPdUfAeRg1X1Nr6GECnQ3fp0Mj2A6smBFZZwEbwhI= /Qd2xw7aQmd/4PP+LMP3Kwouwb8mAfoKYiWkSoTQv5E= ZXZt MHhkZWFkYmVlZg== bXNnIG1lbW8gYW5kIGdhcw==", + "Program gtwLjHAsfKAR6GWB4hzTUAA1w4SDdFMKamtGA5ttMEe consumed 4799 of 386578 compute units", + "Program gtwLjHAsfKAR6GWB4hzTUAA1w4SDdFMKamtGA5ttMEe success", + "Program mem7LhKWbKydCPk1TwNzeCvVSpoVx2mqxNuvjGgWAbG consumed 11145 of 392801 compute units", + "Program mem7LhKWbKydCPk1TwNzeCvVSpoVx2mqxNuvjGgWAbG success", + ]; + let match_context = MatchContext::new("gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G"); + let result = build_program_event_stack(&match_context, &logs, parse_gas_service_log); + + let event = NativeGasPaidForContractCallEvent { + config_pda: "DR9Ja5ojPLPDWmWFRmpc2SEUvK94dKX4uM6AofgwAAJm" + .parse() + .unwrap(), + destination_chain: "evm".to_owned(), + destination_address: "0xdeadbeef".to_owned(), + payload_hash: [ + 253, 7, 118, 199, 14, 218, 66, 103, 127, 224, 243, 254, 44, 195, 247, 43, 10, 46, + 193, 191, 38, 1, 250, 10, 98, 37, 164, 74, 132, 208, 191, 145, + ], + refund_address: "11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3".parse().unwrap(), + params: vec![], + gas_fee_amount: 5000, + }; + let expected = vec![ProgramInvocationState::Succeeded(vec![( + 3, + GasServiceEvent::NativeGasPaidForContractCall(event), + )])]; + + assert_eq!(result, expected); + } } diff --git a/solana/programs/axelar-solana-gas-service/README.md b/solana/programs/axelar-solana-gas-service/README.md new file mode 100644 index 00000000..101dc880 --- /dev/null +++ b/solana/programs/axelar-solana-gas-service/README.md @@ -0,0 +1 @@ +# Gas Service diff --git a/solana/programs/axelar-solana-gas-service/src/lib.rs b/solana/programs/axelar-solana-gas-service/src/lib.rs index ee3cd1f9..c11ca73f 100644 --- a/solana/programs/axelar-solana-gas-service/src/lib.rs +++ b/solana/programs/axelar-solana-gas-service/src/lib.rs @@ -12,7 +12,7 @@ use solana_program::msg; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; -solana_program::declare_id!("gasHQkvaC4jTD2MQpAuEN3RdNwde2Ym5E5QNDoh6m6G"); +solana_program::declare_id!("gasFkyvr4LjK3WwnMGbao3Wzr67F88TmhKmi4ZCXF9K"); /// Seed prefixes for PDAs created by this program pub mod seed_prefixes { diff --git a/solana/programs/axelar-solana-gateway/README.md b/solana/programs/axelar-solana-gateway/README.md new file mode 100644 index 00000000..7902a124 --- /dev/null +++ b/solana/programs/axelar-solana-gateway/README.md @@ -0,0 +1,160 @@ +# Axelar Solana Gateway + +> [!NOTE] +> Mandatory reading prerequisites: +> - [`Solidity Gateway reference implementation`](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/432449d7b330ec6edf5a8e0746644a253486ca87/contracts/gateway/INTEGRATION.md) developed by Axelar. +> +> Important Solana details are described in the docs: +> - [`Solana Account Model`](https://solana.com/docs/core/accounts) +> - [`Solana Transactions and Instructions`](https://solana.com/docs/core/transactions) +> - [`Solana CPI`](https://solana.com/docs/core/cpi) +> - [`Solana PDAs`](https://solana.com/docs/core/pda) +> +> ๐Ÿ‘† a shorter-summary version is available [on Axelar Executable docs](../../crates/axelar-executable/README.md#solana-specific-rundown). + +When integrating with it, you are not expected to be exposed to the Axelar Solana Gateway's inner workings and security mechanisms. +- To receive GMP messages from other chains, read [Axelar Executable docs](../../crates/axelar-executable/README.md). +- To send messages to other chains, read [Sending messages from Solana](#sending-messages-from-solana). + +## Sending messages from Solana + +Here, you can see the entire flow of how a message gets proxied through the network when sending a message from Solana to any other chain: + +![Solana to other chains](https://github.com/user-attachments/assets/61d9934e-221a-4858-be62-a70c5a12d21d) + +A CPI must be made to the Axelar Solana Gateway for a destination contract to communicate with it. +- On Solana, there is no `msg.sender` concept as in Solidity. +- On Solana `program_id`'s **cannot** be signers. +- On Solana, only PDAs can sign on behalf of a program. The only way for programs to send messages is to create PDAs that use [`invoke_signed()`](https://docs.rs/solana-cpi/latest/solana_cpi/fn.invoke_signed.html) and sign over the CPI call. +- The interface of `axelar_solana_gateway::GatewayInstruction::CallContract` instruction defines that the first account in the `accounts[]` must be the `program_id` that is sending the GMP payload. +The second account is a `signing PDA`, meaning the source program must generate a PDA with specific parameters and sign the CPI call for `gateway.call_contract`. This Signature acts as an authorization token that allows the Gateway to interpret that the provided `program_id` is indeed the one that made the call and thus will use the `program_id` as the sender. + + +| PDA name | descriptoin | users | notes | owner | +| - | - | - | - | - | +| [CallContract](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/lib.rs#L312-L317) | This acts only as a signing PDA, never initialized; Permits the destination program to call `CallContract` on the Gateway | Destination program will craft this when making the CPI call to the Gateway | Emulates `msg.sender` from Solidity | Destination program | + +[Full-fledged example](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-memo-program/src/processor.rs#L123-L157): Memo program that leverages a PDA for signing the `Call Contract` CPI call. + +[Full-fledged example](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-memo-program/src/processor.rs#L164-L198): Memo program that leverages a PDA for signing the `Call Contract Offchain Data` CPI call. + +| Gateway Instruction | Use Case | Caveats | +| - | - | - | +| [Call Contract](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/instructions.rs#L52-L67) | When you can create the data fully on-chain. Or When the data is small enough to fit into tx arguments | Even if you can generate all the data on-chain, the Solana tx log is limited to 10kb. And if your program logs more than that, there won't be any error on the transaction level. The log will be truncated, and the message will be malformed. **Please be careful when making this API call.** | +| [Call Contract Offchain Data](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/instructions.rs#L69-L85) | When the payload data cannot be generated on-chain or it does not fit into tx size limitations. This instruction only requires the payload hash. The full payload is expected to be provided to the Relayer directly | Whether the payload gets provided before or after sending this instruction is fully up to the Relayer and not part of the Gateway spec. | + +### Axelar network steps + +After the Relayer sends the message to Amplifier API, Axelar network and `ampd` perform all the validations. + +![image](https://github.com/user-attachments/assets/e7a137e7-6545-4161-be7e-91ec9d6223a5) + +- Relevant `ampd` code is located [here, axelar-amplifier/solana/ampd](https://github.com/eigerco/axelar-amplifier/tree/solana/ampd) +- `ampd` will query the Solana RPC network for a given tx hash (in Solanas case, it's the tx signature, which is 64 bytes) + - retrieve the logs, parse the logs using [`gateway-event-stack` crate](https://github.com/eigerco/solana-axelar/tree/next/solana/crates/gateway-event-stack), and then try to find an event at the given index. If the event exists and the contents match, then `ampd` will produce signatures for the rest of the Axelar network to consume. + +## Receiving messages on Solana + +Receiving messages on Solana is more complex than sending messages. There are a couple of PDAs involved in the process. + +![image](https://github.com/user-attachments/assets/43e0ac3b-04e9-4d76-9075-8b325aec278b) + +| PDA name | descriptoin | users | notes | owner | +| - | - | - | - | - | +| [Gateway Config](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/state/config.rs) | Tracks all the information about the Gateway, the verifier set epoch, verifier set hashes, verifier rotation delays, etc. | This PDA is present in all the public interfaces on the Gateway. Relayer and every contract is expected to interact with it | | Gateway | +| [Verifier Set Tracker](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/state/verifier_set_tracker.rs) | Tracks information about an individual verifier set | Relayer, when rotating verifier sets; Relayer, when approving messages; | Solana does not have built-in infinite size hash maps as storage variables, using PDA for each verifier set entry allows us to ensure that duplicate verifier sets never get created | Gateway | +| [Signtautre Verification Session](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/state/signature_verification_pda.rs) | Tracks that all the signatures for a given payload batch get verified | Relayer uses this in the multi-tx message approval process, where each Signature from a verifier is sent individually to the Gateway for verification | | Gateway | +| [Incoming Message](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/state/incoming_message.rs) | Tracks the state of an individual GMP message (executed/approved + metadata). | Relayer - After all the signatures have been approved, each GMP message must be initialized individually as well, and the Relayer takes care of that. The destination program will receive this PDA in its `execute` flow when receiving the payload | | Gateway | +| [Message Payload](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/state/message_payload.rs) | Contains the raw payload of a message. Limited of up to 10kb. Directly linked to an `IncomingMessage` PDA. | Relayer will upload the raw payload to a PDA and, after message execution (or failure of execution), will close the PDA, regaining all the funds. The destination program will receive this PDA in its `execute` flow. | Solana tx size limitation prevents sending large payloads directly on the chain. Thus, the payload is stored directly on-chain | Gateway; the Relayer that created this PDA can also close it | +| [Validate Call](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-gateway/src/lib.rs#L286-L291) | This acts only as a signing PDA, never initialized; Permits the destination program to set `IncomingMessage` status to `executed`; | Destination program will craft this when making the CPI call to the Gateway | Emulates `msg.sender` from Solidity | Destination program | + +### Signature verification + +**Prerequisite:** initialized `Gateway Root Config PDA` with a valid verifier set; active `Multisig Prover`; active `Relayer`; + +![Execute Data](https://github.com/user-attachments/assets/d039ad91-b7aa-40d2-9c33-b53d3926ad22) + + +Due to Solana limitations, we cannot verify the desired amount of signatures in a single on-chain transaction to fulfil the minimal requirements imposed by the Axelar protocol. For detailed reading, please look at the [axelar-solana-encoding/README.md](../crates/axelar-solana-encoding/README.md#execute-data). + +The approach taken here is that: +1. Relayer receives fully Merkelised data [`ExecuteData`](../crates/axelar-solana-encoding/README.md#current-limits-of-the-merkelised-implementation) from the Multisig Prover, which fulfils the following properties: + 1. we can prove that each `message` is part of the `payload digest` with the corresponding Merkle Proof + 2. we can prove that each `verifier` is part of the `verifier set` that signed the `payload digest` with the corresponding Merkle Proof + 3. each `verifier` has a corresponding Signature attached to it + +| action | tx count | description | +| - | - | - | +| Relayer calls `Initialize Payload Verification Session` on the Gateway [[link to the processor]](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/initialize_payload_verification_session.rs) | 1 | This creates a new PDA that will keep track of the verified signatures. The `payload digest` is used as the core seed parameter for the PDA. This is safe because a `payload digest` will only be duplicated if the `verifier set` remains the same (this is often the case) AND all of the messages are the same. Even if all the messages remain the same, `Axelar Solana Gateway` has idempotency on a per-message level, meaning duplicate execution is impossible. | +| The Relayer sends a tx [`VerifySignature` (link to the processor)](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/verify_signature.rs). | For each `verifier` + Signature in the `ExecuteData` that signed the payload digest | The core logic is that we:
  1. ensure that the `verifier` is part of the `verifier set` that signed the data using Merkle Proof.
  2. check if the `signature` is valid for a given `payload digest` and if it matches the given `verifier` (by performing ECDSA recovery).
  3. update the `signature verification PDA` to track the current weight of the verifier that was verified and the index of its Signature
  4. repeat this tx for every `signature` until the `quorum` has been reached
| + +**Artefact:** We have reached the quorum, tracked on `Signature Verification Session PDA`. + +### Message approval + +**Prerequisite:** `Signature Verification Session PDA` that has reached its quorum. + +As in the signature verification step, we cannot approve dozens of Messages in a single transaction due to Solana limitations. + +| action | tx count | description | +| - | - | - | +| Relayer calls [`Approve Message` (link to the processor)](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/approve_message.rs). | For each GMP message in the `ExecuteData` |
  1. Validating that a `message` is part of a `payload digest` using Merkle Proof.
  2. Validating that the `payload digest` corresponds to `Signature Verification PDA`, and it has reached its quorum.
  3. Validating that the `message` has not already been initialized
  4. Initializes a new PDA (called `Incoming Message PDA`) responsible for tracking a message's `approved`/`executed` state. The core seed of this PDA is `command_id`. You can read more about `command_id` in the [EVM docs #replay prevention section](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/gateway/INTEGRATION.md#replay-prevention); our implementation is the same.
  5. This action emits a log for the Relayer to capture.
  6. Repeat this tx for every `message` in a batch.
| + +**Artefact:** We have initialized a new `Incoming Message PDA` for each message with its state set as `approved`. There have been no changes to PDA contents for messages approved in previous batches. + +### Message Execution + +**Prerequisite:** `Incoming Message PDA` for a message. + +![Caliing the destination program](https://github.com/user-attachments/assets/f7c1eaf9-cae7-4a74-8cea-19b17caaad0a) + +[Full-fledged example](https://github.com/eigerco/solana-axelar/blob/bf3351013ccf5061aaa1195411e2430c67250ec8/solana/programs/axelar-solana-memo-program/src/processor.rs#L87-L103): Memo program that leverages receives a GMP message and implements `axelar-executable` + +After the Relayer reports the event to Amplifier API about a message being approved, the Relayer will receive the raw payload to call the destination program. Because of Solana limitations, the Relayer cannot send large enough payloads in the transaction arguments to satisfy the minimum requirements of Axelar protocol. Therefore, the Relayer does chunk uploading of the raw data to a PDA for the end program to consume. + + +| action | tx count | description | +| - | - | - | +| Relayer calls [`Initialize Message Payload` (link to processor)](https://github.com/eigerco/solana-axelar/blob/c73300dec01547634a80d85b9984348015eb9fb2/solana/programs/axelar-solana-gateway/src/processor/initialize_message_payload.rs). | 1 | The seed of the PDA is directly tied to the Relayer and the `Incoming Message PDA` (`command_id`). This means that if multiple concurrent relayers exist, they will not override each others' payload data. | +| Relayer chunks the raw payload and uploads it in batches using [`Write Message Payload`](https://github.com/eigerco/solana-axelar/blob/main/solana/programs/axelar-solana-gateway/src/processor/write_message_payload.rs). | new tx for each chunk of the payload; max size of a chunk ~800 bytes | Such an approach allows us to **upload up to 10kb of raw message data. That is the upper bound of the Solana integration**. | +| Relayer calls [`Commit Message Payload`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-gateway/src/processor/commit_message_payload.rs) | 1 | Computes the hash of the raw payload. This also ensures that after the hash has been calculated & committed, the payload can no longer be mutated in place by the Relayer. | + + As a result, we now have the following PDAs: + - `Incoming Message PDA`: contains the execution status of a message (will be `approved` state after message approval). Relationship - 1 PDA for each unique message on the Axelar network. + - `Message Payload PDA`: contains the raw payload of a message. There can be many `Message Payload PDA`s, one for each operation relayer. Each `Message Payload PDA` points to a specific `Incoming Message PDA`. + +Next, the Relayer must communicate with the destination program. For a third-party developer to build an integration with the `Axelar Solana Gateway` and receive GMP messages, the only expectation is for the contract to implement [`axelar-executable`](../../crates/axelar-executable/README.md) interface. This allows the Relayer PDA to have a known interface to compose and send transactions after they've been approved on the Gateway. Exception of the rule is [`Interchain Token Service`](../axelar-solana-its/README.md) & [`Governance`](../axelar-solana-governance/README.md) programs, which do not implement `axelar-executable`. + +| action | tx count | description | +| - | - | - | +| Relayer calls the `destination program`| 1 | Composes a tx using `axelar-executable` | +| `Destination program` (via `axelar-executale`) Calls [`Validate Message`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-gateway/src/processor/validate_message.rs). | Internal CPI of ๐Ÿ‘† |
  1. The `destination program` needs to craft a `signing pda` to ensure that the given `program id` is the message's desired recipient (akin to `msg.sender` on Solidity).
  2. `Incoming Message PDA` status gets set to `executed`
  3. event gets emitted
+| The Relayer can close `Message Payload PDA` using [`Close Message Payload`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-gateway/src/processor/close_message_payload.rs) call. | 1 | This will return ~99% of the funds spent uploading the raw data on-chain. | + +**Artifact:** Message has been successfully executed; `Incoming Message PDA` marked as `executed`; `Message Payload PDA` has been closed, and funds refunded to the Relayer. + +### Verifier rotation + +**Prerequisite:** `Signature Verification Session PDA` that has reached its quorum. + +| action | tx count | description | +| - | - | - | +| The Relayer calls [`Rotate Signers`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-gateway/src/processor/rotate_signers.rs). | 1 |
  1. The processor will validate the following logic:
    • If the tx **was not** submitted by `operator`, then check if signer rotation is not happening too frequently (the `rotation delay` parameter is configured on the `Gateway Config PDA`)
    • If the tx **was** submitted by the `operator`, then skip the rotation delay check
  2. Check: Only rotate the verifiers if the `verifier set` that signed the action is the **latest** `verifier set`
  3. Check: ensure that the new verifier set is not a duplicate of an old one
  4. Initialize a new `Verifier Tracker PDA` that will track the epoch and the hash of the newly created `verifier set`
  5. Update the `Gateway Config PDA` to update the latest verifier set epoch
  6. This will emit an event for the relayer to capture and report back to `ampd`
| + +## Operator role + +This role can rotate the `verifier set` without enforcing the `minimum rotation delay`. + +The role can be updated using [`Transfer Operatorship`](https://github.com/eigerco/solana-axelar/blob/033bd17df32920eb6b57a0e6b8d3f82298b0c5ff/solana/programs/axelar-solana-gateway/src/processor/transfer_operatorship.rs#L33). The ix is accessible to: +- **The old operator** can transfer operatorship to a new user +- The **`bpf_loader_upgadeable::upgrade_authority`** can also transfer operatorship. This is equivalent to the upgrade authority on the Solidity implementation. + +## Differences from the EVM implementation + +| Action | EVM reference impl | Solana implementation | Reasoning | +| - | - | - | - | +| [Authentication](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/gateway/INTEGRATION.md#authentication) | Every verifier and all the messages get hashed together in a single hash, then signatures get verified against that hash. All done in a single tx. | Every action is done in a separate tx. Signatures get verified against a hash first. Then, we use Merkle Proofs to prove that a message is part of the hash. | Solana cannot do that many actions in a single transaction (e.g. hashing multiple messages and creating a big hash out of that); we need to split up the approval process into many small transactions. This is described in detail on [axelar-solana-encoding](../crates/axelar-solana-encoding/README.md#current-limits-of-the-merkelised-implementation) crate | +| Receiving the message on the destination contract | Payload is passed as tx args. | Payload is chunked and uploaded to on-chain storage in many small transactions | Otherwise, the average payload size we could provide would be ~600-800 bytes; Solana tx size is limited to 1232 bytes, and a lot of that is consumed by metadata | +| [Message size](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/gateway/INTEGRATION.md#limits) | 16kb is min; more than 1mb on EVM | 10kb is max with options to increase this in the future | The maximum amount of PDA storage (on-chain contract owned account) is 10kb when initialized up-front | +| Updating verifier set | Requires the whole verifier set to be present, then it is re-hashed and then re-validated on chain | Only the verifier set hash is provided in tx parameters; we don't re-hash individual entries from the verifier set upon verifier set rotation. We take the verifier set hash from the Multisig Prover as granted and only validate that the latest verifier set signed it. We expect the hash always to be valid. | We cannot hash that many entries (67 verifiers being the minimum requirement) in a single transaction. The only thing we can do is _"prove that a verifier belongs to the verifier set"_ (like we do during signature verification). Still, even that would not change the underlying verifier set hash we set; thus, the operation would be pointless. | +| [Upgradability](https://github.com/axelarnetwork/axelar-gmp-sdk-solidity/blob/main/contracts/gateway/INTEGRATION.md#upgradability) | Gateway is deployed via a proxy contract | Gateway is deployed using `bpf_loader_upgradeable` program | This is the standard on Solana | diff --git a/solana/programs/axelar-solana-gateway/src/instructions.rs b/solana/programs/axelar-solana-gateway/src/instructions.rs index 4e143725..0ed40ce5 100644 --- a/solana/programs/axelar-solana-gateway/src/instructions.rs +++ b/solana/programs/axelar-solana-gateway/src/instructions.rs @@ -52,8 +52,9 @@ pub enum GatewayInstruction { /// Represents the `CallContract` Axelar event. /// /// Accounts expected by this instruction: - /// 0. [SIGNER] Sender (origin) of the message) - /// 1. [] Gateway Root Config PDA account + /// 0. [] Sender (origin) of the message, program id + /// 1. [SIGNER] PDA created by the `sender`, works as authorization token for a given program id + /// 2. [] Gateway Root Config PDA account CallContract { /// The name of the target blockchain. destination_chain: String, @@ -61,14 +62,17 @@ pub enum GatewayInstruction { destination_contract_address: String, /// Contract call data. payload: Vec, + /// The pda bump for the signing PDA + signing_pda_bump: u8, }, /// Represents the `CallContract` Axelar event. The contract call data is expected to be /// handled off-chain by uploading the data using the relayer API. /// /// Accounts expected by this instruction: - /// 0. [SIGNER] Sender (origin) of the message) - /// 1. [] Gateway Root Config PDA account + /// 0. [] Sender (origin) of the message, program id + /// 1. [SIGNER] PDA created by the `sender`, works as authorization token for a given program id + /// 2. [] Gateway Root Config PDA account CallContractOffchainData { /// The name of the target blockchain. destination_chain: String, @@ -76,6 +80,8 @@ pub enum GatewayInstruction { destination_contract_address: String, /// Hash of the contract call data, to be uploaded off-chain through the relayer API. payload_hash: [u8; 32], + /// The pda bump for the signing PDA + signing_pda_bump: u8, }, /// Initializes the Gateway configuration PDA account. @@ -312,10 +318,13 @@ pub fn rotate_signers( /// # Errors /// /// Returns a [`ProgramError::BorshIoError`] if the instruction serialization fails. +#[allow(clippy::too_many_arguments)] pub fn call_contract( gateway_program_id: Pubkey, gateway_root_pda: Pubkey, - sender: Pubkey, + sender_program_id: Pubkey, + sender_call_contract_pda: Pubkey, + sender_call_contract_bump: u8, destination_chain: String, destination_contract_address: String, payload: Vec, @@ -324,10 +333,12 @@ pub fn call_contract( destination_chain, destination_contract_address, payload, + signing_pda_bump: sender_call_contract_bump, })?; let accounts = vec![ - AccountMeta::new_readonly(sender, true), + AccountMeta::new_readonly(sender_program_id, false), + AccountMeta::new_readonly(sender_call_contract_pda, true), AccountMeta::new_readonly(gateway_root_pda, false), ]; @@ -343,10 +354,13 @@ pub fn call_contract( /// # Errors /// /// Returns a [`ProgramError::BorshIoError`] if the instruction serialization fails. +#[allow(clippy::too_many_arguments)] pub fn call_contract_offchain_data( gateway_program_id: Pubkey, gateway_root_pda: Pubkey, - sender: Pubkey, + sender_program_id: Pubkey, + sender_call_contract_pda: Pubkey, + sender_call_contract_bump: u8, destination_chain: String, destination_contract_address: String, payload_hash: [u8; 32], @@ -355,10 +369,12 @@ pub fn call_contract_offchain_data( destination_chain, destination_contract_address, payload_hash, + signing_pda_bump: sender_call_contract_bump, })?; let accounts = vec![ - AccountMeta::new_readonly(sender, true), + AccountMeta::new_readonly(sender_program_id, false), + AccountMeta::new_readonly(sender_call_contract_pda, true), AccountMeta::new_readonly(gateway_root_pda, false), ]; diff --git a/solana/programs/axelar-solana-gateway/src/lib.rs b/solana/programs/axelar-solana-gateway/src/lib.rs index 471af3bd..9be11d52 100644 --- a/solana/programs/axelar-solana-gateway/src/lib.rs +++ b/solana/programs/axelar-solana-gateway/src/lib.rs @@ -26,6 +26,8 @@ pub mod seed_prefixes { pub const VERIFIER_SET_TRACKER_SEED: &[u8] = b"ver-set-tracker"; /// The seed prefix for deriving signature verification PDAs pub const SIGNATURE_VERIFICATION_SEED: &[u8] = b"gtw-sig-verif"; + /// The seed prefix for deriving call contract signature verification PDAs + pub const CALL_CONTRACT_SIGNING_SEED: &[u8] = b"gtw-call-contract"; /// The seed prefix for deriving incoming message PDAs pub const INCOMING_MESSAGE_SEED: &[u8] = b"incoming message"; /// The seed prefix for deriving message payload PDAs @@ -304,6 +306,36 @@ pub fn create_validate_message_signing_pda( Pubkey::create_program_address(&[command_id, &[signing_pda_bump]], destination_address) } +/// Create a new Signing PDA that is used for `CallContract` call by the source contract to authorize its call +#[inline] +#[must_use] +pub fn get_call_contract_signing_pda(source_program_id: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[seed_prefixes::CALL_CONTRACT_SIGNING_SEED], + &source_program_id, + ) +} + +/// Create a new Signing PDA that is used for authorizing the source program to call `CallContract` +/// +/// # Errors +/// +/// Returns a [`PubkeyError`] if the derived address lies on the ed25519 curve and is therefore not +/// a valid program derived address when using the destination address as the program ID. +#[inline] +pub fn create_call_contract_signing_pda( + source_program_id: Pubkey, + signing_pda_bump: u8, +) -> Result { + Pubkey::create_program_address( + &[ + seed_prefixes::CALL_CONTRACT_SIGNING_SEED, + &[signing_pda_bump], + ], + &source_program_id, + ) +} + /// Finds the `MessagePayload` PDA. /// /// This function is expensive and should not be used on-chain. Prefer diff --git a/solana/programs/axelar-solana-gateway/src/processor.rs b/solana/programs/axelar-solana-gateway/src/processor.rs index 35487f75..9a0a3131 100644 --- a/solana/programs/axelar-solana-gateway/src/processor.rs +++ b/solana/programs/axelar-solana-gateway/src/processor.rs @@ -81,6 +81,7 @@ impl Processor { destination_chain, destination_contract_address, payload, + signing_pda_bump, } => { msg!("Instruction: Call Contract"); Self::process_call_contract( @@ -89,12 +90,14 @@ impl Processor { &destination_chain, &destination_contract_address, &payload, + signing_pda_bump, ) } GatewayInstruction::CallContractOffchainData { destination_chain, destination_contract_address, payload_hash, + signing_pda_bump, } => { msg!("Instruction: Call Contract Offchain Data"); Self::process_call_contract_offchain_data( @@ -103,6 +106,7 @@ impl Processor { &destination_chain, &destination_contract_address, payload_hash, + signing_pda_bump, ) } GatewayInstruction::InitializeConfig(init_config) => { diff --git a/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs b/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs index b8473d28..98833e87 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/call_contract.rs @@ -4,12 +4,13 @@ use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use solana_program::{bpf_loader, bpf_loader_upgradeable}; use super::event_utils::{read_array, read_string, EventParseError}; use super::Processor; use crate::error::GatewayError; use crate::state::GatewayConfig; -use crate::{assert_valid_gateway_root_pda, event_prefixes}; +use crate::{assert_valid_gateway_root_pda, create_call_contract_signing_pda, event_prefixes}; impl Processor { /// This function initializes a cross-chain message by emitting an event containing the call details. @@ -17,6 +18,9 @@ impl Processor { /// The message can then be picked up by off-chain components for /// cross-chain delivery. /// + /// It requires a valid signing PDA & signing PDA bump to be provided for verifying the + /// authenticity of the call. + /// /// # Errors /// /// Returns [`ProgramError`] if: @@ -42,9 +46,11 @@ impl Processor { destination_chain: &str, destination_contract_address: &str, payload: &[u8], + signing_pda_bump: u8, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); - let sender = next_account_info(accounts_iter)?; + let sender_program_id = next_account_info(accounts_iter)?; + let sender_signing_pda = next_account_info(accounts_iter)?; let gateway_root_pda = next_account_info(accounts_iter)?; // Check: Gateway Root PDA is initialized. @@ -58,15 +64,32 @@ impl Processor { let payload_hash = solana_program::keccak::hash(payload).to_bytes(); // Check: sender is signer - if !sender.is_signer { + if !sender_signing_pda.is_signer { solana_program::msg!("Error: Sender must be a signer"); return Err(ProgramError::MissingRequiredSignature); } + // Check: sender program id is an actual program + if !sender_program_id.executable + && (sender_program_id.owner == &bpf_loader::ID + || sender_program_id.owner == &bpf_loader_upgradeable::ID) + { + solana_program::msg!("Error: sender must be a program"); + return Err(ProgramError::IncorrectProgramId); + } + + // check that caller ir valid signing PDA + let expected_signing_pda = + create_call_contract_signing_pda(*sender_program_id.key, signing_pda_bump)?; + if &expected_signing_pda != sender_signing_pda.key { + solana_program::msg!("Invalid signing PDA"); + return Err(GatewayError::InvalidSigningPDA.into()); + } + // Emit an event sol_log_data(&[ event_prefixes::CALL_CONTRACT, - &sender.key.to_bytes(), + &sender_program_id.key.to_bytes(), &payload_hash, destination_chain.as_bytes(), destination_contract_address.as_bytes(), diff --git a/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs b/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs index 9f5202ed..82c9c343 100644 --- a/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs +++ b/solana/programs/axelar-solana-gateway/src/processor/call_contract_offchain_data.rs @@ -4,12 +4,13 @@ use solana_program::entrypoint::ProgramResult; use solana_program::log::sol_log_data; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use solana_program::{bpf_loader, bpf_loader_upgradeable}; use super::event_utils::{read_array, read_string, EventParseError}; use super::Processor; use crate::error::GatewayError; use crate::state::GatewayConfig; -use crate::{assert_valid_gateway_root_pda, event_prefixes}; +use crate::{assert_valid_gateway_root_pda, create_call_contract_signing_pda, event_prefixes}; impl Processor { /// Processes a cross-chain contract call using off-chain data and emits the appropriate event. @@ -17,6 +18,9 @@ impl Processor { /// This function is similar to [`Processor::process_call_contract`] but accepts a pre-computed /// payload hash instead of the raw payload bytes. /// + /// It requires a valid signing PDA & signing PDA bump to be provided for verifying the + /// authenticity of the call. + /// /// # Errors /// /// Returns [`ProgramError`] if: @@ -41,9 +45,11 @@ impl Processor { destination_chain: &str, destination_contract_address: &str, payload_hash: [u8; 32], + signing_pda_bump: u8, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); - let sender = next_account_info(accounts_iter)?; + let sender_program_id = next_account_info(accounts_iter)?; + let sender_signing_pda = next_account_info(accounts_iter)?; let gateway_root_pda = next_account_info(accounts_iter)?; // Check: Gateway Root PDA is initialized. @@ -54,15 +60,31 @@ impl Processor { assert_valid_gateway_root_pda(gateway_config.bump, gateway_root_pda.key)?; // Check: sender is signer - if !sender.is_signer { + if !sender_signing_pda.is_signer { solana_program::msg!("Error: Sender must be a signer"); return Err(ProgramError::MissingRequiredSignature); } + // Check: sender program id is an actual program + if !sender_program_id.executable + && (sender_program_id.owner == &bpf_loader::ID + || sender_program_id.owner == &bpf_loader_upgradeable::ID) + { + solana_program::msg!("Error: sender must be a program"); + return Err(ProgramError::IncorrectProgramId); + } + + // check that caller ir valid signing PDA + let expected_signing_pda = + create_call_contract_signing_pda(*sender_program_id.key, signing_pda_bump)?; + if &expected_signing_pda != sender_signing_pda.key { + solana_program::msg!("Invalid signing PDA"); + return Err(GatewayError::InvalidSigningPDA.into()); + } // Emit an event sol_log_data(&[ event_prefixes::CALL_CONTRACT_OFFCHAIN_DATA, - &sender.key.to_bytes(), + &sender_program_id.key.to_bytes(), &payload_hash, destination_chain.as_bytes(), destination_contract_address.as_bytes(), diff --git a/solana/programs/axelar-solana-gateway/tests/integration/validate_message.rs b/solana/programs/axelar-solana-gateway/tests/integration/validate_message.rs index 6ec4308a..d5a2b129 100644 --- a/solana/programs/axelar-solana-gateway/tests/integration/validate_message.rs +++ b/solana/programs/axelar-solana-gateway/tests/integration/validate_message.rs @@ -165,15 +165,16 @@ async fn fail_if_invalid_signing_pda_seeds() { .unwrap(); let err = metadata.send_tx(&[ix]).await.unwrap_err(); - // assert - let err_variant_one = err.find_log("Invalid signing PDA").is_some(); - let err_variant_two = err - .find_log("Provided seeds do not result in a valid address") - .is_some(); // this depends on the bump that gets derived -- sometimes they match, sometimes // they don't depending on the random parameters of test run - let either_error = err_variant_one || err_variant_two; - assert!(either_error); + + // assert + assert!(err + .find_at_least_one_log(&[ + "Invalid signing PDA", + "Provided seeds do not result in a valid address" + ]) + .is_some()); } async fn set_existing_incoming_message_state( diff --git a/solana/programs/axelar-solana-governance/Cargo.toml b/solana/programs/axelar-solana-governance/Cargo.toml index d66f84d4..add44e28 100644 --- a/solana/programs/axelar-solana-governance/Cargo.toml +++ b/solana/programs/axelar-solana-governance/Cargo.toml @@ -23,6 +23,7 @@ axelar-solana-encoding.workspace = true alloy-sol-types.workspace = true base64.workspace = true axelar-solana-gateway = { workspace = true, features = ["no-entrypoint"] } +role-management.workspace = true [dev-dependencies] solana-logger.workspace = true diff --git a/solana/programs/axelar-solana-governance/README.md b/solana/programs/axelar-solana-governance/README.md index b85a10d2..86d9ea00 100644 --- a/solana/programs/axelar-solana-governance/README.md +++ b/solana/programs/axelar-solana-governance/README.md @@ -1,3 +1,17 @@ + +## High level overview + +[![governance hl](https://github.com/user-attachments/assets/1d5be514-67d9-4dd6-b635-375300f01ae5)](https://excalidraw.com/#json=pVoAXLtjUps5y9nU8wYu2,cz_P-xoEobAN9qbfe0-MwQ) + +The governance module allows decisions taken on the Axelar network to be propagated and executed on the different integrated chains, giving a chance (by timelock) to each chain maintainer to prepare for it's execution. So the governance module acts as a "approved proposal's forwarder" which is connected to the Axelar governance infrastructure via [GMP](https://www.axelar.network/blog/general-message-passing-and-how-can-it-change-web3) messages. + +All the voting happens on the axelar side. Once an approved proposal +is forwarded to the governance module, it might be executed in the Solana blockchain either by a normal solana actor when the time lock ends, or by an operator actor role anytime, if and only if the operator was approved to do so by the Axelar network via another GMP message. Operators might be multisig schemes. + +Apart from the scheduled time lock proposal command, there are other [GMP commands](./tests/module/gmp/) which helps managing the proposal lifecycle, like cancelling or putting the proposal under the control of the operator. The governance module will only accept GMP commands coming from the Axelar governance and verified by the gateway. + +[Native instructions](./tests/module/) works as a second but separated part of the flow, used for local network (Solana in this case) operations. + ## Governance module design The governance module program is a port from the original Solidity implementation. We encourage reading @@ -13,8 +27,31 @@ In Solana there's no existence of the inheritance concept, so you can find here All the data types coming from the Axelar network needs to be respected in all public interfaces/storage, as they are used for hashing operations that could be lead to further checkins. +## The GMP message payload + +The GMP messages are coming from the Axelar network and we should respect their form and encoding (ABI encoding). In order to help on that, the crate [governance-gmp](./../../helpers/governance-gmp/) was created. + +The payload ([call_data]((./../../helpers/governance-gmp/)) field) of the governance command structure is meant to be the borsh serialized version of the [ExecuteProposalData](./src/state/proposal.rs) type. + +Building GMP messages is made easy for callers thanks to the ix builder. See [how to interact with this program](#how-to-interact-with-this-program) section for more information. + + +## PDA and data structures + +There are some PDA's involved in the governance module: + +* The `config PDA`, in which the program stores its configuration and which pubkey should be set as `upgrade_authority` when executing program updates through proposals. [See this test example](./tests/module/gateway_upgrade.rs) for a complete example. + +* The `proposal PDA`. It is created with the hash of the elements of the proposal following original [EVM implementation](#governance-module-design) . Check [proposal.rs](./src/state/proposal.rs) to see hashing functions. This pda stores proposal related data, like the timelock eta and [canonical bump seeds](https://solana.com/developers/courses/program-security/bump-seed-canonicalization). + +* The `managed proposal PDA`. This is just a "marker PDA" which tells the system whether a proposal can be directly executed by a Operator, without the need of accomplishing the proposal timelock. This PDA derivation + [takes as a seed](./src/state/operator.rs) the proposal hash. + +**A note on GMP payload PDA's**: In solana we have some limits (1232 bytes) regarding how much can be sent on a transaction. In order to workaround this, the GMP payload must be first stored on a dedicated account by the caller (on the governance module case, the Axelar governance) and such account to be sent along the GMP instruction. +We can check how we reference the message metadata this in the [GovernanceInstruction::ProcessGMP struct](./src/instructions.rs). For uploading the payload to the account, we can take as an example the test helpers [approve_ix_at_gateway()](./tests/module/helpers.rs). + ## How to interact with this program The best way to interact with this program is to use the [IxBuilder](./src/instructions.rs) provided by this program lib. It will help developers to quickly build the needed instructions without dealing with all the internal representations and program accounts order. -Check the `IxBuilder` tests on [it's module](./src/instructions.rs) for a better view of the example use cases. +Check the `IxBuilder` tests on [it's module](./src/instructions.rs) for a better view of the example [use cases](./tests/module/). diff --git a/solana/programs/axelar-solana-governance/src/instructions.rs b/solana/programs/axelar-solana-governance/src/instructions.rs index 4ce7ca8d..8064975c 100644 --- a/solana/programs/axelar-solana-governance/src/instructions.rs +++ b/solana/programs/axelar-solana-governance/src/instructions.rs @@ -336,8 +336,10 @@ pub mod builder { config_pda: &Pubkey, config: GovernanceConfig, ) -> IxBuilder { + let program_data_pda = bpf_loader_upgradeable::get_program_data_address(&crate::ID); let accounts = vec![ AccountMeta::new(*payer, true), + AccountMeta::new_readonly(program_data_pda, false), AccountMeta::new(*config_pda, false), AccountMeta::new_readonly(system_program::ID, false), ]; @@ -1073,13 +1075,8 @@ pub mod builder { fn simplest_use_case() { let payer = Pubkey::new_unique(); let config_pda = Pubkey::new_unique(); - let config = GovernanceConfig::new( - 1, - [0_u8; 32], - [0_u8; 32], - 1, - Pubkey::new_unique().to_bytes(), - ); + let config = + GovernanceConfig::new([0_u8; 32], [0_u8; 32], 1, Pubkey::new_unique().to_bytes()); let _ix = IxBuilder::new() .initialize_config(&payer, &config_pda, config) diff --git a/solana/programs/axelar-solana-governance/src/lib.rs b/solana/programs/axelar-solana-governance/src/lib.rs index cddef77d..013ee834 100644 --- a/solana/programs/axelar-solana-governance/src/lib.rs +++ b/solana/programs/axelar-solana-governance/src/lib.rs @@ -11,7 +11,7 @@ pub mod processor; pub mod sol_types; pub mod state; -solana_program::declare_id!("govDofoQLgN7GLAFA7QzQdyFfHuK4ssqjzWL1ESghT5"); +solana_program::declare_id!("govuuGWCowvknaLm2jkViP54eHCoLLzRqstne5Dgwvj"); /// Checks that the supplied program ID is the correct one /// diff --git a/solana/programs/axelar-solana-governance/src/processor/init_config.rs b/solana/programs/axelar-solana-governance/src/processor/init_config.rs index cd35db5b..97ba4ca3 100644 --- a/solana/programs/axelar-solana-governance/src/processor/init_config.rs +++ b/solana/programs/axelar-solana-governance/src/processor/init_config.rs @@ -2,6 +2,7 @@ //! data. use program_utils::ValidPDA; +use role_management::processor::ensure_upgrade_authority; use solana_program::account_info::{next_account_info, AccountInfo}; use solana_program::msg; use solana_program::program_error::ProgramError; @@ -20,13 +21,16 @@ use crate::state::GovernanceConfig; pub(crate) fn process( program_id: &Pubkey, accounts: &[AccountInfo<'_>], - governance_config: GovernanceConfig, + mut governance_config: GovernanceConfig, ) -> Result<(), ProgramError> { let accounts_iter = &mut accounts.iter(); let payer = next_account_info(accounts_iter)?; + let program_data = next_account_info(accounts_iter)?; let root_pda = next_account_info(accounts_iter)?; let system_account = next_account_info(accounts_iter)?; + ensure_upgrade_authority(program_id, payer, program_data)?; + // Check: System Program Account if !system_program::check_id(system_account.key) { return Err(ProgramError::IncorrectProgramId); @@ -39,6 +43,8 @@ pub(crate) fn process( return Err(ProgramError::InvalidArgument); } + governance_config.bump = bump; + // Check: PDA Account is not initialized root_pda.check_uninitialized_pda()?; diff --git a/solana/programs/axelar-solana-governance/src/state/mod.rs b/solana/programs/axelar-solana-governance/src/state/mod.rs index eabed72a..6aa35b13 100644 --- a/solana/programs/axelar-solana-governance/src/state/mod.rs +++ b/solana/programs/axelar-solana-governance/src/state/mod.rs @@ -44,14 +44,13 @@ impl GovernanceConfig { /// Creates a new governance program config. #[must_use] pub const fn new( - bump: u8, chain_hash: Hash, address_hash: Hash, minimum_proposal_eta_delay: u32, operator: Address, ) -> Self { Self { - bump, + bump: 0, // This will be set by the program chain_hash, address_hash, minimum_proposal_eta_delay, diff --git a/solana/programs/axelar-solana-governance/tests/module/execute_operator_proposal.rs b/solana/programs/axelar-solana-governance/tests/module/execute_operator_proposal.rs index c27c9d66..7fb40c1a 100644 --- a/solana/programs/axelar-solana-governance/tests/module/execute_operator_proposal.rs +++ b/solana/programs/axelar-solana-governance/tests/module/execute_operator_proposal.rs @@ -1,17 +1,18 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_gateway_test_fixtures::base::TestFixture; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use borsh::to_vec; -use solana_program_test::tokio; +use solana_program_test::{tokio, ProgramTest}; use solana_sdk::instruction::AccountMeta; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::{Keypair, Signer}; use crate::fixtures::operator_keypair; use crate::helpers::{ - approve_ix_at_gateway, assert_msg_present_in_logs, events, gmp_memo_metadata, - gmp_sample_metadata, init_contract_with_operator, ix_builder_with_memo_proposal_data, - ix_builder_with_sample_proposal_data, program_test, setup_programs, + approve_ix_at_gateway, assert_msg_present_in_logs, deploy_governance_program, events, + gmp_memo_metadata, gmp_sample_metadata, init_contract_with_operator, + ix_builder_with_memo_proposal_data, ix_builder_with_sample_proposal_data, setup_programs, }; /// This is a full flow test that tests the execution of a proposal that reaches @@ -27,12 +28,15 @@ async fn test_full_flow_operator_proposal_execution() { let (mut sol_integration, config_pda, counter_pda) = Box::pin(setup_programs()).await; + let (memo_signing_pda, _) = + axelar_solana_gateway::get_call_contract_signing_pda(axelar_solana_memo_program::ID); // Using the memo program as target proposal program. let memo_program_accounts = &[ + AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(counter_pda, false), + AccountMeta::new_readonly(memo_signing_pda, false), AccountMeta::new_readonly(sol_integration.gateway_root_pda, false), AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(sol_integration.fixture.payer.pubkey(), true), ]; @@ -169,7 +173,9 @@ async fn test_only_operator_can_execute_ix() { // Get the operator key pair; let operator = Keypair::new(); // Incorrect operator keypair - let mut fixture = TestFixture::new(program_test()).await; + let mut fixture = TestFixture::new(ProgramTest::default()).await; + + deploy_governance_program(&mut fixture).await; // Setup gov module (initialize contract) let (config_pda, _) = @@ -240,10 +246,15 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { ) .await; assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } #[tokio::test] diff --git a/solana/programs/axelar-solana-governance/tests/module/execute_proposal.rs b/solana/programs/axelar-solana-governance/tests/module/execute_proposal.rs index 784895b3..ca56d256 100644 --- a/solana/programs/axelar-solana-governance/tests/module/execute_proposal.rs +++ b/solana/programs/axelar-solana-governance/tests/module/execute_proposal.rs @@ -1,3 +1,4 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use borsh::to_vec; @@ -57,12 +58,15 @@ async fn test_time_lock_is_enforced() { async fn test_proposal_can_be_executed_and_reached_memo_program() { let (mut sol_integration, config_pda, counter_pda) = Box::pin(setup_programs()).await; + let (memo_signing_pda, _) = + axelar_solana_gateway::get_call_contract_signing_pda(axelar_solana_memo_program::ID); // Using the memo program as target proposal program. let memo_program_accounts = &[ + AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(counter_pda, false), + AccountMeta::new_readonly(memo_signing_pda, false), AccountMeta::new_readonly(sol_integration.gateway_root_pda, false), AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(sol_integration.fixture.payer.pubkey(), true), ]; @@ -143,10 +147,15 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { let res = sol_integration.fixture.send_tx(&[ix]).await; // The runtime detects the wrong PDA and returns an error. assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } #[tokio::test] @@ -166,12 +175,15 @@ async fn test_proposal_can_be_executed_and_reached_memo_program_transferring_fun // Using the memo program as target proposal program. let memo_program_funds_receiver_account = AccountMeta::new(counter_pda, false); + let (memo_signing_pda, _) = + axelar_solana_gateway::get_call_contract_signing_pda(axelar_solana_memo_program::ID); let memo_program_accounts = &[ + AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), memo_program_funds_receiver_account.clone(), + AccountMeta::new_readonly(memo_signing_pda, false), AccountMeta::new_readonly(sol_integration.gateway_root_pda, false), AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(sol_integration.fixture.payer.pubkey(), true), ]; @@ -222,12 +234,15 @@ async fn test_proposal_is_deleted_after_execution() { // Memo program solana accounts. gathered from // `axelar_solana_memo_program_old::instruction::call_gateway_with_memo` + let (memo_signing_pda, _) = + axelar_solana_gateway::get_call_contract_signing_pda(axelar_solana_memo_program::ID); // Using the memo program as target proposal program. let memo_program_accounts = &[ + AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(counter_pda, false), + AccountMeta::new_readonly(memo_signing_pda, false), AccountMeta::new_readonly(sol_integration.gateway_root_pda, false), AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(sol_integration.fixture.payer.pubkey(), true), ]; @@ -273,12 +288,15 @@ async fn test_proposal_is_deleted_after_execution() { async fn test_same_proposal_can_be_created_after_execution() { let (mut sol_integration, config_pda, counter_pda) = Box::pin(setup_programs()).await; + let (memo_signing_pda, _) = + axelar_solana_gateway::get_call_contract_signing_pda(axelar_solana_memo_program::ID); // Using the memo program as target proposal program. let memo_program_accounts = &[ + AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(counter_pda, false), + AccountMeta::new_readonly(memo_signing_pda, false), AccountMeta::new_readonly(sol_integration.gateway_root_pda, false), AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new_readonly(axelar_solana_memo_program::id(), false), AccountMeta::new_readonly(sol_integration.fixture.payer.pubkey(), true), ]; diff --git a/solana/programs/axelar-solana-governance/tests/module/gmp/approve_operator.rs b/solana/programs/axelar-solana-governance/tests/module/gmp/approve_operator.rs index b99293eb..0a184b03 100644 --- a/solana/programs/axelar-solana-governance/tests/module/gmp/approve_operator.rs +++ b/solana/programs/axelar-solana-governance/tests/module/gmp/approve_operator.rs @@ -1,3 +1,4 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use borsh::to_vec; @@ -153,10 +154,15 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { approve_ix_at_gateway(&mut sol_integration, &mut gmp_call_data).await; let res = sol_integration.fixture.send_tx(&[gmp_call_data.ix]).await; assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } #[tokio::test] diff --git a/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_operator.rs b/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_operator.rs index b9865927..0ca14051 100644 --- a/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_operator.rs +++ b/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_operator.rs @@ -1,3 +1,4 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use borsh::to_vec; @@ -135,10 +136,14 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { let res = sol_integration.fixture.send_tx(&[gmp_call_data.ix]).await; assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } #[tokio::test] diff --git a/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_time_lock_proposal.rs b/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_time_lock_proposal.rs index 7196457b..86fc4915 100644 --- a/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_time_lock_proposal.rs +++ b/solana/programs/axelar-solana-governance/tests/module/gmp/cancel_time_lock_proposal.rs @@ -1,3 +1,4 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use borsh::to_vec; @@ -113,8 +114,13 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { approve_ix_at_gateway(&mut sol_integration, &mut gmp_call_data).await; let res = sol_integration.fixture.send_tx(&[gmp_call_data.ix]).await; assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } diff --git a/solana/programs/axelar-solana-governance/tests/module/gmp/schedule_time_lock_proposal.rs b/solana/programs/axelar-solana-governance/tests/module/gmp/schedule_time_lock_proposal.rs index aa997be1..d4527154 100644 --- a/solana/programs/axelar-solana-governance/tests/module/gmp/schedule_time_lock_proposal.rs +++ b/solana/programs/axelar-solana-governance/tests/module/gmp/schedule_time_lock_proposal.rs @@ -1,3 +1,4 @@ +use axelar_solana_gateway_test_fixtures::base::FindLog; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::{IxBuilder, ProposalRelated}; use axelar_solana_governance::state::operator; @@ -7,7 +8,7 @@ use solana_program_test::tokio; use solana_sdk::signature::Signer; use crate::fixtures::MINIMUM_PROPOSAL_DELAY; -use crate::gmp::{assert_msg_present_in_logs, gmp_sample_metadata}; +use crate::gmp::gmp_sample_metadata; use crate::helpers::{ approve_ix_at_gateway, events, ix_builder_with_sample_proposal_data, setup_programs, }; @@ -119,8 +120,13 @@ async fn test_program_checks_proposal_pda_is_correctly_derived() { let res = sol_integration.fixture.send_tx(&[gmp_call_data.ix]).await; assert!(res.is_err()); - assert_msg_present_in_logs( - res.err().unwrap(), - "Derived proposal PDA does not match provided one", - ); + + let meta = res.err().unwrap(); + + assert!(meta + .find_at_least_one_log(&[ + "Derived proposal PDA does not match provided one", + "Provided seeds do not result in a valid address", + ]) + .is_some()); } diff --git a/solana/programs/axelar-solana-governance/tests/module/helpers.rs b/solana/programs/axelar-solana-governance/tests/module/helpers.rs index 246b374e..c29e3225 100644 --- a/solana/programs/axelar-solana-governance/tests/module/helpers.rs +++ b/solana/programs/axelar-solana-governance/tests/module/helpers.rs @@ -2,7 +2,7 @@ use std::time::SystemTime; use axelar_solana_encoding::types::messages::{CrossChainId, Message}; use axelar_solana_gateway::state::incoming_message::command_id; -use axelar_solana_gateway_test_fixtures::base::TestFixture; +use axelar_solana_gateway_test_fixtures::base::{workspace_root_dir, TestFixture}; use axelar_solana_gateway_test_fixtures::{ SolanaAxelarIntegration, SolanaAxelarIntegrationMetadata, }; @@ -13,7 +13,7 @@ use axelar_solana_governance::instructions::builder::{ use axelar_solana_governance::state::GovernanceConfig; use axelar_solana_memo_program::instruction::AxelarMemoInstruction; use borsh::to_vec; -use solana_program_test::{processor, BanksTransactionResultWithMetadata, ProgramTest}; +use solana_program_test::{tokio, BanksTransactionResultWithMetadata, ProgramTest}; use solana_sdk::bpf_loader_upgradeable; use solana_sdk::instruction::AccountMeta; use solana_sdk::program_error::ProgramError; @@ -26,16 +26,17 @@ use crate::fixtures::{ }; pub(crate) async fn setup_programs() -> (SolanaAxelarIntegrationMetadata, Pubkey, Pubkey) { - let mut fixture = TestFixture::new(program_test()).await; + let mut fixture = TestFixture::new(ProgramTest::default()).await; + let upgrade_authority = Keypair::new(); + + deploy_governance_program(&mut fixture).await; - // Setup gov module (initialize contract) + // Init gov module (initialize contract) let (gov_config_pda, _) = init_contract_with_operator(&mut fixture, operator_keypair().pubkey().to_bytes()) .await .unwrap(); - let upgrade_authority = Keypair::new(); - // Setup gateway let mut sol_integration = SolanaAxelarIntegration::builder() .initial_signer_weights(vec![555, 222]) @@ -81,12 +82,27 @@ pub(crate) async fn setup_programs() -> (SolanaAxelarIntegrationMetadata, Pubkey (sol_integration, gov_config_pda, memo_counter_pda.0) } -pub(crate) fn program_test() -> ProgramTest { - ProgramTest::new( - "axelar_solana_governance", - axelar_solana_governance::id(), - processor!(axelar_solana_governance::processor::Processor::process_instruction), - ) +pub(crate) async fn deploy_governance_program_with_upgrade_authority( + fixture: &mut TestFixture, + upgrade_authority: &Pubkey, +) { + let program_bytecode = + tokio::fs::read(workspace_root_dir().join("target/deploy/axelar_solana_governance.so")) + .await + .unwrap(); + + fixture + .register_upgradeable_program( + &program_bytecode, + upgrade_authority, + &axelar_solana_governance::ID, + ) + .await; +} + +pub(crate) async fn deploy_governance_program(fixture: &mut TestFixture) { + let upgrade_authority = fixture.payer.pubkey(); + deploy_governance_program_with_upgrade_authority(fixture, &upgrade_authority).await; } pub(crate) async fn init_contract_with_operator( @@ -96,7 +112,6 @@ pub(crate) async fn init_contract_with_operator( let (config_pda, bump) = GovernanceConfig::pda(); let config = axelar_solana_governance::state::GovernanceConfig::new( - bump, SOURCE_CHAIN_NAME_KECCAK_HASH, SOURCE_CHAIN_ADDRESS_KECCAK_HASH, MINIMUM_PROPOSAL_DELAY, diff --git a/solana/programs/axelar-solana-governance/tests/module/initialize_config.rs b/solana/programs/axelar-solana-governance/tests/module/initialize_config.rs index 378ec82c..a3257884 100644 --- a/solana/programs/axelar-solana-governance/tests/module/initialize_config.rs +++ b/solana/programs/axelar-solana-governance/tests/module/initialize_config.rs @@ -1,21 +1,25 @@ use axelar_solana_gateway_test_fixtures::base::TestFixture; use axelar_solana_governance::instructions::builder::IxBuilder; use axelar_solana_governance::state::GovernanceConfig; -use solana_program_test::tokio; +use solana_program_test::{tokio, ProgramTest}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signer; use crate::fixtures::MINIMUM_PROPOSAL_DELAY; -use crate::helpers::{assert_msg_present_in_logs, program_test}; +use crate::helpers::{ + assert_msg_present_in_logs, deploy_governance_program, + deploy_governance_program_with_upgrade_authority, +}; #[tokio::test] async fn test_successfully_initialize_config() { // Setup - let mut fixture = TestFixture::new(program_test()).await; - let (config_pda, bump) = GovernanceConfig::pda(); + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program(&mut fixture).await; - let config = axelar_solana_governance::state::GovernanceConfig::new( - bump, + let (config_pda, _) = GovernanceConfig::pda(); + + let config = GovernanceConfig::new( [0_u8; 32], [0_u8; 32], MINIMUM_PROPOSAL_DELAY, @@ -31,20 +35,25 @@ async fn test_successfully_initialize_config() { // Assert assert!(res.is_ok()); let root_pda_data = fixture - .get_account_with_borsh::(&config_pda) + .get_account_with_borsh::(&config_pda) .await .unwrap(); - assert_eq!(&config, &root_pda_data); + assert_eq!(&config.address_hash, &root_pda_data.address_hash); + assert_eq!(&config.chain_hash, &root_pda_data.chain_hash); + assert_eq!( + &config.minimum_proposal_eta_delay, + &root_pda_data.minimum_proposal_eta_delay + ); + assert_eq!(&config.operator, &root_pda_data.operator); } #[tokio::test] async fn test_program_checks_config_pda_successfully_derived() { // Setup - let mut fixture = TestFixture::new(program_test()).await; - let (_, bump) = GovernanceConfig::pda(); + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program(&mut fixture).await; - let config = axelar_solana_governance::state::GovernanceConfig::new( - bump, + let config = GovernanceConfig::new( [0_u8; 32], [0_u8; 32], MINIMUM_PROPOSAL_DELAY, @@ -68,3 +77,63 @@ async fn test_program_checks_config_pda_successfully_derived() { "Derived PDA does not match provided PDA", ); } + +#[tokio::test] +async fn test_program_overrides_config_bump() { + // Setup + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program(&mut fixture).await; + + let (config_pda, _) = GovernanceConfig::pda(); + + let config = GovernanceConfig::new( + [0_u8; 32], + [0_u8; 32], + MINIMUM_PROPOSAL_DELAY, + Pubkey::new_unique().to_bytes(), + ); + + let ix = IxBuilder::new() + .initialize_config(&fixture.payer.pubkey(), &config_pda, config.clone()) + .build(); // Wrong PDA + + let res = fixture.send_tx(&[ix]).await; + assert!(res.is_ok()); + + let config = fixture + .get_account_with_borsh::(&config_pda) + .await + .unwrap(); + + // Assert + assert!(config.bump != 0); +} + +#[tokio::test] +async fn test_only_deployer_can_initialize_program() { + // Setup + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program_with_upgrade_authority(&mut fixture, &Pubkey::new_unique()).await; // Wrong deployer + + let (config_pda, _) = GovernanceConfig::pda(); + + let config = GovernanceConfig::new( + [0_u8; 32], + [0_u8; 32], + MINIMUM_PROPOSAL_DELAY, + Pubkey::new_unique().to_bytes(), + ); + + let ix = IxBuilder::new() + .initialize_config(&fixture.payer.pubkey(), &config_pda, config.clone()) + .build(); + + let res = fixture.send_tx(&[ix]).await; + + // Assert + assert!(res.is_err()); + assert_msg_present_in_logs( + res.err().unwrap(), + "Given authority is not the program upgrade authority", + ); +} diff --git a/solana/programs/axelar-solana-governance/tests/module/transfer_operatorship.rs b/solana/programs/axelar-solana-governance/tests/module/transfer_operatorship.rs index ff42fa1f..fb18e78e 100644 --- a/solana/programs/axelar-solana-governance/tests/module/transfer_operatorship.rs +++ b/solana/programs/axelar-solana-governance/tests/module/transfer_operatorship.rs @@ -2,15 +2,16 @@ use axelar_solana_gateway_test_fixtures::base::TestFixture; use axelar_solana_governance::events::GovernanceEvent; use axelar_solana_governance::instructions::builder::IxBuilder; use axelar_solana_governance::state::GovernanceConfig; -use solana_program_test::tokio; +use solana_program_test::{tokio, ProgramTest}; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; use crate::fixtures::operator_keypair; use crate::helpers::{ - approve_ix_at_gateway, assert_msg_present_in_logs, default_proposal_eta, events, - gmp_sample_metadata, init_contract_with_operator, program_test, setup_programs, + approve_ix_at_gateway, assert_msg_present_in_logs, default_proposal_eta, + deploy_governance_program, events, gmp_sample_metadata, init_contract_with_operator, + setup_programs, }; #[tokio::test] @@ -18,7 +19,8 @@ async fn test_operator_transfer_can_happen_being_operator_signer() { // Get the operator key pair; let operator = operator_keypair(); - let mut fixture = TestFixture::new(program_test()).await; + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program(&mut fixture).await; // Setup gov module (initialize contract) let (config_pda, _) = @@ -76,7 +78,8 @@ async fn test_error_is_emitted_when_no_required_signers() { // Not current operator let operator = Keypair::new(); - let mut fixture = TestFixture::new(program_test()).await; + let mut fixture = TestFixture::new(ProgramTest::default()).await; + deploy_governance_program(&mut fixture).await; // Setup gov module (initialize contract) let (config_pda, _) = diff --git a/solana/programs/axelar-solana-its/Cargo.toml b/solana/programs/axelar-solana-its/Cargo.toml index a6cc658b..37b382a3 100644 --- a/solana/programs/axelar-solana-its/Cargo.toml +++ b/solana/programs/axelar-solana-its/Cargo.toml @@ -19,11 +19,12 @@ alloy-sol-types.workspace = true axelar-executable.workspace = true axelar-message-primitives.workspace = true axelar-solana-encoding.workspace = true -axelar-solana-gateway = { workspace = true, features = ["no-entrypoint"] } axelar-solana-gas-service = { workspace = true, features = ["no-entrypoint"] } +axelar-solana-gateway = { workspace = true, features = ["no-entrypoint"] } bitflags.workspace = true borsh.workspace = true interchain-token-transfer-gmp.workspace = true +itertools.workspace = true mpl-token-metadata.workspace = true program-utils.workspace = true role-management.workspace = true diff --git a/solana/programs/axelar-solana-its/README.md b/solana/programs/axelar-solana-its/README.md new file mode 100644 index 00000000..ecc79f2a --- /dev/null +++ b/solana/programs/axelar-solana-its/README.md @@ -0,0 +1,96 @@ +# Interchain Token Service (ITS) + +This is the Solana implementation of the Interchain Token Service. From the [EVM](https://github.com/axelarnetwork/interchain-token-service) reference implementation: + +> The interchain token service is meant to allow users/developers to easily create their own token bridge. All the underlying interchain communication is done by the service, and the deployer can either use an `InterchainToken` that the service provides, or their own implementations. +> There are quite a few different possible configurations for bridges, and any user of any deployed bridge needs to trust the deployer of said bridge, much like any user of a token needs to trust the operator of said token. We plan to eventually remove upgradability from the service to make this protocol more trustworthy. Please reference the [design description](https://github.com/axelarnetwork/interchain-token-service/blob/main/DESIGN.md) for more details on the design. Please look at the [docs](https://github.com/axelarnetwork/interchain-token-service/blob/main/docs/index.md) for more details on the contracts. + +--- + +## Comparison with the EVM Reference Implementation + +On EVM, the ERC-20 standard is used for tokens, while Solana uses the SPL standard. Deploying an ERC-20 token on EVM involves creating a new contract instance, whereas on Solana, a new mint account is created due to its account model. + +In EVM, an `InterchainToken` is an implementation of the ERC-20 standard, allowing multiple minters. This enables the ITS contract to be granted the _minter_ role. On Solana, a mint account representing an SPL token can have only a **single** mint authority. For `TokenManager` types such as `NativeInterchainToken`, `Mint/BurnFrom`, and `Mint/Burn`, which require minting and burning tokens during interchain transfers, the mint authority must be assigned to ITS (technically, to the `TokenManager` created for the token). + +Consequently, `InterchainToken`s deployed with these `TokenManager` types on Solana have ITS set as their mint authority, with an additional `minter` optionally defined during deployment. This minter can mint tokens but must use the proxy mint instruction in the ITS program instead of the standard `spl-token` instruction. ITS also provides instructions to transfer this internal minter role to another account. Note that the `mint_authority` on the SPL token cannot be changed once transferred to the `TokenManager`. + +--- + +## PDA Structure + +This is a list of [Program Derived Addresses (PDAs)](https://solana.com/docs/core/pda) used within ITS. The list follows a hierarchy in terms of dependency for derivation (i.e.: the second PDA in the list uses the first in its derivation, the third uses the second, so on and so forth), with exception of the User Roles PDA, which depends on the resource the role is being tracked, either ITS Root Config PDA or Token Manager PDA. + +| PDA | Description | Owner | Deriving function | State | +|-----|-------------|-------|-------------------|-------| +| Gateway Root Config | This is a singleton PDA that addresses an account that keeps the state of the Gateway | Gateway program | [get_gateway_root_config_pda](../axelar-solana-gateway/src/lib.rs#L77) | [GatewayConfig](../axelar-solana-gateway/src/state/config.rs#L21) | +| ITS Root Config | This is a singleton PDA that addresses an account that keeps the state of ITS. | ITS program | [find_its_root_pda](./src/lib.rs#L132) | [InterchainTokenService](./src/state/mod.rs) | +| Interchain Token | This is the address used for the mint accounts created by ITS (Native Interchain Tokens). | ITS program | [find_interchain_token_pda](./src/lib.rs#L274) | [Mint](https://docs.rs/spl-token-2022/latest/spl_token_2022/state/struct.Mint.html) | +| Token Manager | Addresses for Token Manager accounts. | ITS program | [find_token_manager_pda](./src/lib.rs#L197) | [TokenManager](./src/state/token_manager.rs) | +| Flow Slot | These are addresses for accounts that track the flow of an interchain token. | ITS program | [find_flow_slot_pda](./src/lib.rs#L311) | [FlowSlot](./src/state/flow_limit.rs) | +| User Roles | These are addresses for accounts that track user roles (Minter, FlowLimiter, Operator) on resources (ITS Root Config, TokenManager). | ITS program | [find_user_roles_pda](../../helpers/role-management/src/lib.rs#L68) | [UserRoles](../../helpers/role-management/src/state.rs#L43) | + +--- + +## Interaction with Different Token Standards + +When bridging a token, the main consideration is how it is represented on the source and destination chains. ERC-20 tokens typically use 18 decimals, whereas SPL tokens usually have 9 decimals. While higher decimals are possible, they can hinder usability on Solana, where token amounts are represented using unsigned 64-bit integers. + +Additionally, some token standards impose limitations on token metadata, such as the name, symbol, and other properties. See [Metadata Limitations](#metadata-limitations) for details on Solana's restrictions. + +--- + +## Custom Token Linking + +Custom token linking on Axelar involves deploying `TokenManager`s for existing tokens. The [axelar-examples](https://github.com/axelarnetwork/axelar-examples) repository includes an [example](https://github.com/axelarnetwork/axelar-examples/blob/main/examples/evm/its-custom-token/index.js) for linking custom tokens between EVM chains. The same process applies when linking tokens between EVM and Solana. + +As noted earlier, mint accounts on Solana can have only one mint authority. Therefore, when deploying a `Mint/Burn` or `Mint/BurnFrom` `TokenManager` on Solana, the mint authority role must be transferred to the `TokenManager` PDA. + +- **Local Deployment**: This transfer is handled automatically, requiring the _payer_ to be the current mint authority. +- **Remote Deployment**: If the `TokenManager` is deployed via a message from another chain, the mint authority must be manually transferred using the [`SetAuthority`](https://docs.rs/spl-token-2022/latest/spl_token_2022/instruction/enum.TokenInstruction.html#variant.SetAuthority) instruction from the `spl-token(-2022)` program. Failure to do so will prevent the token bridge from functioning, as the `TokenManager` cannot mint tokens for interchain transfers. + +The [from_solana_to_evm.rs](https://github.com/eigerco/solana-axelar/blob/main/solana/programs/axelar-solana-its/tests/module/from_solana_to_evm.rs) test module includes several examples of token linking with different `TokenManager` types, which can serve as a guide for using `axelar-solana-its` instructions. + +--- + +## Token Metadata + +Unlike ERC-20 tokens, SPL tokens do not natively include metadata such as name, symbol, or URI. The `spl-token-2022` program introduces extensions, including `TokenMetadata` and `MetadataPointer`, to add this information to mint accounts. + +However, the Solana ecosystem has historically addressed this gap using the Metaplex `mpl-token-metadata` program, which remains the most common method for managing token metadata. Consequently, `InterchainToken`s deployed on Solana follow the Metaplex metadata specification. During deployment, the metadata is created via the `mpl-token-metadata` program. + +### Metadata Limitations + +Some basic limitation exists when creating the token metadata on Solana: + +- Maximum length for the `name`: 32 +- Maximum length for the `symbol`: 10 +- There is also a maximum length of 200 for the `uri`, but `uri` is currently not used by the ITS protocol. + +When deploying an `InterchainToken`, if any of these limits are not respected, the transaction will fail. + +--- + +## Calling an External Contract with a Token Transfer + +The ITS implements functionality to allow token transfers to carry instruction data, enabling tokens to be transferred to a contract which is then executed in the same transaction. The destination program has to implement the required interfaces. The [axelar-solana-memo-program](https://github.com/eigerco/solana-axelar-internal/blob/main/solana/programs/axelar-solana-memo-program/src/processor.rs#L38) implements it, you can see it in action in the [ITS tests](https://github.com/eigerco/solana-axelar-internal/blob/main/solana/programs/axelar-solana-its/tests/module/from_evm_to_solana.rs#L144). + +The diagram below shows how the message flows from the EVM ITS contract to a Solana program while also showing how the message is structured (click to open on Excalidraw an be able to zoom-in). + +[![EVM->Solana](https://github.com/user-attachments/assets/bf0bf75e-3acc-404e-8425-0d7779a2893b)](https://excalidraw.com/#json=0lVeKwoyvgoGjZqq37iVP,dHVboAUXzZ-tsfo2KUizpQ) + +When calling a contract on the Solana chain using this flow, the Solana instruction should be serialized using Borsh (as expected by the solana program). Due to the Solana account model, the accounts required by the instruction should also be provided. The [AbiSolanaGateway](../../../evm-contracts/src/SolanaGatewayPayload.sol#L20) solidity library can be used directly or as a guide for creating the executable payload to send from EVM to Solana. This payload should then be used to populate the `data` field of the `InterchainTransfer` message. + +When calling a contract on EVM from Solana, encoding the call data with ABI encoding and populating the `data` field of the `InterchainTransfer` message is enough. + +## Relationship with other Axelar Solana Programs + +### Gateway + +ITS leverages the Axelar GMP protocol and therefore follows the same approval and validation process through the Gateway (see [image above](#calling-an-external-contract-with-a-token-transfer)). However, unlike regular cross-chain contract calls, ITS messages must adhere to the ITS protocol. Specifically, the payload of each GMP message must match one of the message types defined in the [ITS design document](https://github.com/axelarnetwork/interchain-token-service/blob/main/DESIGN.md) and must be ABI-encoded. + +Because of these requirements, we cannot use [SolanaGatewayPayload](../../../evm-contracts/src/SolanaGatewayPayload.sol) for ITS messages. This raises an important question: **How do we provide the necessary accounts to Solana ITS?** To solve this, we created a helper crate, [its-instruction-builder](../../helpers/its-instruction-builder/src/lib.rs). It exposes a function that parses the original ITS message and queries the Solana blockchain via RPC, gathering all required accounts and creating the corresponding instructions for the Solana ITS. + +### Gas Service + +ITS uses the Axelar GMP protocol, and thus gas is paid as any other message on the network. For more info on the Gas Service, please check its [README](../axelar-solana-gas-service/README.md). diff --git a/solana/programs/axelar-solana-its/src/instructions/mod.rs b/solana/programs/axelar-solana-its/src/instructions/mod.rs index 1ab10a08..3c623c95 100644 --- a/solana/programs/axelar-solana-its/src/instructions/mod.rs +++ b/solana/programs/axelar-solana-its/src/instructions/mod.rs @@ -27,6 +27,31 @@ pub mod minter; pub mod operator; pub mod token_manager; +/// Convenience module with the indices of the accounts passed in the +/// [`ItsGmpPayload`] instruction (offset by the prefixed GMP accounts). +pub mod its_account_indices { + /// The index of the gateway root PDA account. + pub const GATEWAY_ROOT_PDA_INDEX: usize = 0; + /// The index of the system program account. + pub const SYSTEM_PROGRAM_INDEX: usize = 1; + /// The index of the ITS root PDA account. + pub const ITS_ROOT_PDA_INDEX: usize = 2; + /// The index of the token manager PDA account. + pub const TOKEN_MANAGER_PDA_INDEX: usize = 3; + /// The index of the token mint account. + pub const TOKEN_MINT_INDEX: usize = 4; + /// The index of the token manager ATA account. + pub const TOKEN_MANAGER_ATA_INDEX: usize = 5; + /// The index of the token program account. + pub const TOKEN_PROGRAM_INDEX: usize = 6; + /// The index of the associated token program account. + pub const SPL_ASSOCIATED_TOKEN_ACCOUNT_INDEX: usize = 7; + /// The index of the its user roles account. + pub const ITS_USER_ROLES_PDA_INDEX: usize = 8; + /// The rent account index + pub const RENT_ACCOUNT_INDEX: usize = 9; +} + bitflags! { /// Bitmask for the optional accounts passed in some of the instructions. #[derive(Debug, PartialEq, Eq)] @@ -289,6 +314,10 @@ pub struct DeployInterchainTokenInputs { /// The gas value to be paid for the deploy transaction pub(crate) gas_value: u64, + + /// Signing PDA bump + #[builder(default, setter(strip_option))] + pub(crate) signing_pda_bump: Option, } /// Parameters for `[InterchainTokenServiceInstruction::DeployTokenManager]`. @@ -327,6 +356,10 @@ pub struct DeployTokenManagerInputs { /// `spl_token_2022::id()`. #[builder(default, setter(strip_option))] pub(crate) token_program: Option, + + /// Signing PDA bump + #[builder(default, setter(strip_option))] + pub(crate) signing_pda_bump: Option, } /// Parameters for `[InterchainTokenServiceInstruction::InterchainTransfer]`. @@ -392,6 +425,10 @@ pub struct InterchainTransferInputs { /// or `spl_token_2022::id()`. Assumes `spl_token_2022::id()` if not set. #[builder(default = spl_token_2022::id())] pub(crate) token_program: Pubkey, + + /// Signing PDA bump + #[builder(default, setter(strip_option))] + pub(crate) signing_pda_bump: Option, } /// Inputs for the `[InterchainTokenServiceInstruction::CallContractWithInterchainToken]` @@ -459,6 +496,8 @@ pub fn initialize( operator: Pubkey, ) -> Result { let (its_root_pda, _) = crate::find_its_root_pda(&gateway_root_pda); + let (program_data_address, _) = + Pubkey::find_program_address(&[crate::id().as_ref()], &bpf_loader_upgradeable::id()); let (user_roles_pda, _) = role_management::find_user_roles_pda(&crate::id(), &its_root_pda, &operator); @@ -466,6 +505,7 @@ pub fn initialize( let accounts = vec![ AccountMeta::new(payer, true), + AccountMeta::new_readonly(program_data_address, false), AccountMeta::new_readonly(gateway_root_pda, false), AccountMeta::new(its_root_pda, false), AccountMeta::new_readonly(system_program::ID, false), @@ -514,7 +554,7 @@ pub fn set_pause_status(payer: Pubkey, paused: bool) -> Result Result { let (gateway_root_pda, _) = axelar_solana_gateway::get_gateway_root_config_pda(); let payer = Pubkey::new_from_array( @@ -534,6 +574,9 @@ pub fn deploy_interchain_token( accounts.append(&mut its_accounts); } else { let (its_root_pda, _) = crate::find_its_root_pda(&gateway_root_pda); + let (call_contract_signing_pda, signing_pda_bump) = + axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); + params.signing_pda_bump = Some(signing_pda_bump); accounts.push(AccountMeta::new_readonly(gateway_root_pda, false)); accounts.push(AccountMeta::new_readonly( @@ -544,6 +587,8 @@ pub fn deploy_interchain_token( accounts.push(AccountMeta::new_readonly(params.gas_service, false)); accounts.push(AccountMeta::new_readonly(system_program::id(), false)); accounts.push(AccountMeta::new_readonly(its_root_pda, false)); + accounts.push(AccountMeta::new_readonly(call_contract_signing_pda, false)); + accounts.push(AccountMeta::new_readonly(crate::ID, false)); }; let data = to_vec(&InterchainTokenServiceInstruction::DeployInterchainToken { params })?; @@ -561,7 +606,9 @@ pub fn deploy_interchain_token( /// # Errors /// /// If serialization fails. -pub fn deploy_token_manager(params: DeployTokenManagerInputs) -> Result { +pub fn deploy_token_manager( + mut params: DeployTokenManagerInputs, +) -> Result { let (gateway_root_pda, _) = axelar_solana_gateway::get_gateway_root_config_pda(); let payer = Pubkey::new_from_array( params @@ -590,6 +637,9 @@ pub fn deploy_token_manager(params: DeployTokenManagerInputs) -> Result Result Result Result { - let accounts = interchain_transfer_accounts(¶ms)?; +pub fn interchain_transfer( + mut params: InterchainTransferInputs, +) -> Result { + let (accounts, signing_pda_bump) = interchain_transfer_accounts(¶ms)?; + params.signing_pda_bump = Some(signing_pda_bump); let data = to_vec(&InterchainTokenServiceInstruction::InterchainTransfer { params })?; Ok(Instruction { @@ -641,10 +696,11 @@ pub fn interchain_transfer(params: InterchainTransferInputs) -> Result Result { - let accounts = interchain_transfer_accounts(¶ms)?; + let (accounts, signing_pda_bump) = interchain_transfer_accounts(¶ms)?; + params.signing_pda_bump = Some(signing_pda_bump); let data = to_vec(&InterchainTokenServiceInstruction::CallContractWithInterchainToken { params })?; @@ -664,7 +720,7 @@ pub fn call_contract_with_interchain_token( pub fn call_contract_with_interchain_token_offchain_data( mut params: CallContractWithInterchainTokenInputs, ) -> Result<(Instruction, Vec), ProgramError> { - let accounts = interchain_transfer_accounts(¶ms)?; + let (accounts, signing_pda_bump) = interchain_transfer_accounts(¶ms)?; let Some(destination_chain) = params.destination_chain.as_ref() else { return Err(ProgramError::InvalidArgument); @@ -681,6 +737,7 @@ pub fn call_contract_with_interchain_token_offchain_data( let offchain_data = hub_payload.encode(); params.payload_hash = Some(solana_program::keccak::hashv(&[&offchain_data]).0); + params.signing_pda_bump = Some(signing_pda_bump); let data = to_vec( &InterchainTokenServiceInstruction::CallContractWithInterchainTokenOffchainData { params }, @@ -698,7 +755,7 @@ pub fn call_contract_with_interchain_token_offchain_data( fn interchain_transfer_accounts( inputs: &InterchainTransferInputs, -) -> Result, ProgramError> { +) -> Result<(Vec, u8), ProgramError> { let (gateway_root_pda, _) = axelar_solana_gateway::get_gateway_root_config_pda(); let (its_root_pda, _) = crate::find_its_root_pda(&gateway_root_pda); let (interchain_token_pda, _) = @@ -751,23 +808,30 @@ fn interchain_transfer_accounts( let token_manager_ata = get_associated_token_address_with_program_id(&token_manager_pda, &mint, &token_program); + let (call_contract_signing_pda, signing_pda_bump) = + axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); - Ok(vec![ - AccountMeta::new_readonly(payer, true), - AccountMeta::new_readonly(authority, signer), - AccountMeta::new_readonly(gateway_root_pda, false), - AccountMeta::new_readonly(axelar_solana_gateway::id(), false), - AccountMeta::new(inputs.gas_config_pda, false), - AccountMeta::new_readonly(inputs.gas_service, false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(its_root_pda, false), - AccountMeta::new(source_account, false), - AccountMeta::new(mint, false), - AccountMeta::new_readonly(token_manager_pda, false), - AccountMeta::new(token_manager_ata, false), - AccountMeta::new_readonly(token_program, false), - AccountMeta::new(flow_slot_pda, false), - ]) + Ok(( + vec![ + AccountMeta::new_readonly(payer, true), + AccountMeta::new_readonly(authority, signer), + AccountMeta::new_readonly(gateway_root_pda, false), + AccountMeta::new_readonly(axelar_solana_gateway::id(), false), + AccountMeta::new(inputs.gas_config_pda, false), + AccountMeta::new_readonly(inputs.gas_service, false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(its_root_pda, false), + AccountMeta::new_readonly(call_contract_signing_pda, false), + AccountMeta::new_readonly(crate::ID, false), + AccountMeta::new(source_account, false), + AccountMeta::new(mint, false), + AccountMeta::new_readonly(token_manager_pda, false), + AccountMeta::new(token_manager_ata, false), + AccountMeta::new_readonly(token_program, false), + AccountMeta::new(flow_slot_pda, false), + ], + signing_pda_bump, + )) } /// Creates an [`InterchainTokenServiceInstruction::SetFlowLimit`]. @@ -1151,6 +1215,7 @@ fn derive_common_its_accounts( pub(crate) trait OutboundInstructionInputs { fn destination_chain(&mut self) -> Option; fn gas_value(&self) -> u64; + fn signing_pda_bump(&self) -> Option; } impl OutboundInstructionInputs for DeployInterchainTokenInputs { @@ -1161,6 +1226,10 @@ impl OutboundInstructionInputs for DeployInterchainTokenInputs { fn gas_value(&self) -> u64 { self.gas_value } + + fn signing_pda_bump(&self) -> Option { + self.signing_pda_bump + } } impl OutboundInstructionInputs for DeployTokenManagerInputs { @@ -1171,6 +1240,9 @@ impl OutboundInstructionInputs for DeployTokenManagerInputs { fn gas_value(&self) -> u64 { self.gas_value } + fn signing_pda_bump(&self) -> Option { + self.signing_pda_bump + } } #[allow(dead_code)] diff --git a/solana/programs/axelar-solana-its/src/processor/interchain_transfer.rs b/solana/programs/axelar-solana-its/src/processor/interchain_transfer.rs index c5ae7965..52490455 100644 --- a/solana/programs/axelar-solana-its/src/processor/interchain_transfer.rs +++ b/solana/programs/axelar-solana-its/src/processor/interchain_transfer.rs @@ -219,6 +219,10 @@ pub(crate) fn process_outbound_transfer<'a>( .destination_chain .take() .ok_or(ProgramError::InvalidInstructionData)?; + let signing_pda_bump = inputs + .signing_pda_bump + .take() + .ok_or(ProgramError::InvalidInstructionData)?; let gas_value = inputs.gas_value; let payload = inputs .try_into() @@ -232,6 +236,7 @@ pub(crate) fn process_outbound_transfer<'a>( &payload, destination_chain, gas_value, + signing_pda_bump, maybe_payload_hash, ) } @@ -660,7 +665,12 @@ impl<'a> FromAccountInfoSlice<'a> for TakeTokenAccounts<'a> { _gas_service_config_pda: next_account_info(accounts_iter)?, _gas_service: next_account_info(accounts_iter)?, system_account: next_account_info(accounts_iter)?, - its_root_pda: next_account_info(accounts_iter)?, + its_root_pda: { + let keep = next_account_info(accounts_iter)?; + next_account_info(accounts_iter)?; + next_account_info(accounts_iter)?; + keep + }, source_account: next_account_info(accounts_iter)?, token_mint: next_account_info(accounts_iter)?, token_manager_pda: next_account_info(accounts_iter)?, diff --git a/solana/programs/axelar-solana-its/src/processor/mod.rs b/solana/programs/axelar-solana-its/src/processor/mod.rs index 95cd4651..138e1876 100644 --- a/solana/programs/axelar-solana-its/src/processor/mod.rs +++ b/solana/programs/axelar-solana-its/src/processor/mod.rs @@ -1,5 +1,4 @@ //! Program state processor - use axelar_executable::{validate_with_gmp_metadata, PROGRAM_ACCOUNTS_START_INDEX}; use axelar_solana_encoding::types::messages::Message; use axelar_solana_gateway::error::GatewayError; @@ -7,26 +6,31 @@ use axelar_solana_gateway::state::message_payload::ImmutMessagePayload; use axelar_solana_gateway::state::GatewayConfig; use borsh::BorshDeserialize; use interchain_token_transfer_gmp::{GMPPayload, SendToHub}; +use itertools::{self, Itertools}; use program_utils::{BorshPda, BytemuckedPda, ValidPDA}; use role_management::processor::{ ensure_signer_roles, ensure_upgrade_authority, RoleManagementAccounts, }; use role_management::state::UserRoles; use solana_program::account_info::{next_account_info, AccountInfo}; +use solana_program::clock::Clock; use solana_program::entrypoint::ProgramResult; use solana_program::program::invoke; use solana_program::program::invoke_signed; use solana_program::program_error::ProgramError; use solana_program::pubkey::Pubkey; +use solana_program::sysvar::Sysvar; use solana_program::{msg, system_program}; use self::interchain_transfer::process_inbound_transfer; use self::token_manager::SetFlowLimitAccounts; use crate::instructions::{ - self, InterchainTokenServiceInstruction, OptionalAccountsFlags, OutboundInstructionInputs, + self, its_account_indices, InterchainTokenServiceInstruction, OptionalAccountsFlags, + OutboundInstructionInputs, }; +use crate::state::token_manager::TokenManager; use crate::state::InterchainTokenService; -use crate::{assert_valid_its_root_pda, check_program_account, seed_prefixes, Roles}; +use crate::{assert_valid_its_root_pda, check_program_account, Roles}; pub mod interchain_token; pub mod interchain_transfer; @@ -124,6 +128,7 @@ pub fn process_instruction<'a>( fn process_initialize(program_id: &Pubkey, accounts: &[AccountInfo<'_>]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let payer = next_account_info(account_info_iter)?; + let program_data_account = next_account_info(account_info_iter)?; let gateway_root_pda_account = next_account_info(account_info_iter)?; let its_root_pda_account = next_account_info(account_info_iter)?; let system_account = next_account_info(account_info_iter)?; @@ -135,6 +140,9 @@ fn process_initialize(program_id: &Pubkey, accounts: &[AccountInfo<'_>]) -> Prog return Err(ProgramError::IncorrectProgramId); } + // Check: Upgrade Authority + ensure_upgrade_authority(program_id, payer, program_data_account)?; + // Check: PDA Account is not initialized its_root_pda_account.check_uninitialized_pda()?; @@ -234,6 +242,8 @@ fn process_inbound_its_gmp_payload<'a>( let payload = GMPPayload::decode(&inner.payload).map_err(|_err| ProgramError::InvalidInstructionData)?; + validate_its_accounts(instruction_accounts, &payload)?; + match payload { GMPPayload::InterchainTransfer(transfer) => process_inbound_transfer( message, @@ -270,20 +280,23 @@ where .ok_or(ProgramError::InvalidInstructionData)?; let gas_value = payload.gas_value(); - let destination_chain = payload.destination_chain(); + let bump_and_chain = payload + .signing_pda_bump() + .and_then(|bump| payload.destination_chain().map(|chain| (bump, chain))); let payload: GMPPayload = payload .try_into() .map_err(|_err| ProgramError::InvalidInstructionData)?; - match destination_chain { - Some(chain) => { + match bump_and_chain { + Some((signing_pda_bump, chain)) => { process_outbound_its_gmp_payload( payer, other_accounts, &payload, chain, gas_value, + signing_pda_bump, None, )?; } @@ -321,6 +334,7 @@ fn process_outbound_its_gmp_payload<'a>( payload: &GMPPayload, destination_chain: String, gas_value: u64, + signing_pda_bump: u8, payload_hash: Option<[u8; 32]>, ) -> ProgramResult { let accounts_iter = &mut accounts.iter(); @@ -330,6 +344,9 @@ fn process_outbound_its_gmp_payload<'a>( let gas_service = next_account_info(accounts_iter)?; let system_program = next_account_info(accounts_iter)?; let its_root_account = next_account_info(accounts_iter)?; + let call_contract_signing_acc = next_account_info(accounts_iter)?; + let program_account = next_account_info(accounts_iter)?; + let its_root_config = InterchainTokenService::load(its_root_account)?; assert_valid_its_root_pda( its_root_account, @@ -341,11 +358,21 @@ fn process_outbound_its_gmp_payload<'a>( return Err(ProgramError::Immutable); } + let signing_pda = + axelar_solana_gateway::create_call_contract_signing_pda(crate::ID, signing_pda_bump)?; + + if &signing_pda != call_contract_signing_acc.key { + msg!("invalid call contract signing account / signing pda bump"); + return Err(ProgramError::InvalidAccountData); + } + let (payload_hash, call_contract_ix) = if let Some(payload_hash) = payload_hash { let ix = axelar_solana_gateway::instructions::call_contract_offchain_data( axelar_solana_gateway::id(), *gateway_root_account.key, - *its_root_account.key, + crate::ID, + signing_pda, + signing_pda_bump, ITS_HUB_TRUSTED_CHAIN_NAME.to_owned(), ITS_HUB_TRUSTED_CONTRACT_ADDRESS.to_owned(), payload_hash, @@ -370,7 +397,9 @@ fn process_outbound_its_gmp_payload<'a>( let ix = axelar_solana_gateway::instructions::call_contract( axelar_solana_gateway::id(), *gateway_root_account.key, - *its_root_account.key, + crate::ID, + signing_pda, + signing_pda_bump, ITS_HUB_TRUSTED_CHAIN_NAME.to_owned(), ITS_HUB_TRUSTED_CONTRACT_ADDRESS.to_owned(), hub_payload, @@ -405,11 +434,14 @@ fn process_outbound_its_gmp_payload<'a>( invoke_signed( &call_contract_ix, - &[its_root_account.clone(), gateway_root_account.clone()], + &[ + program_account.clone(), + call_contract_signing_acc.clone(), + gateway_root_account.clone(), + ], &[&[ - seed_prefixes::ITS_SEED, - gateway_root_account.key.as_ref(), - &[its_root_config.bump], + axelar_solana_gateway::seed_prefixes::CALL_CONTRACT_SIGNING_SEED, + &[signing_pda_bump], ]], )?; @@ -490,3 +522,60 @@ fn process_set_pause_status(accounts: &[AccountInfo<'_>], paused: bool) -> Progr Ok(()) } + +fn validate_its_accounts(accounts: &[AccountInfo<'_>], payload: &GMPPayload) -> ProgramResult { + // In this case we cannot derive the mint account, so we just use what we got + // and check later against the mint within the `TokenManager` PDA. + let maybe_mint = if let GMPPayload::InterchainTransfer(_) = payload { + accounts + .get(its_account_indices::TOKEN_MINT_INDEX) + .map(|account| *account.key) + } else { + None + }; + + let gateway_root_pda = accounts + .get(its_account_indices::GATEWAY_ROOT_PDA_INDEX) + .map(|account| *account.key) + .ok_or(ProgramError::InvalidAccountData)?; + let token_program = accounts + .get(its_account_indices::TOKEN_PROGRAM_INDEX) + .map(|account| *account.key) + .ok_or(ProgramError::InvalidAccountData)?; + + let (derived_its_accounts, _) = instructions::derive_its_accounts( + payload, + gateway_root_pda, + token_program, + maybe_mint, + Some(Clock::get()?.unix_timestamp), + )?; + + for element in accounts.iter().zip_longest(derived_its_accounts.iter()) { + match element { + itertools::EitherOrBoth::Both(provided, derived) => { + if provided.key != &derived.pubkey { + return Err(ProgramError::InvalidAccountData); + } + } + itertools::EitherOrBoth::Left(_) | itertools::EitherOrBoth::Right(_) => { + return Err(ProgramError::InvalidAccountData); + } + } + } + + // Now we validate the mint account passed for `InterchainTransfer` + if let Some(mint) = maybe_mint { + let token_manager_pda = accounts + .get(its_account_indices::TOKEN_MANAGER_PDA_INDEX) + .ok_or(ProgramError::InvalidAccountData)?; + + let token_manager = TokenManager::load(token_manager_pda)?; + + if token_manager.token_address.as_ref() != mint.as_ref() { + return Err(ProgramError::InvalidAccountData); + } + } + + Ok(()) +} diff --git a/solana/programs/axelar-solana-its/tests/module/flow_limits.rs b/solana/programs/axelar-solana-its/tests/module/flow_limits.rs index cbf1dd77..56270330 100644 --- a/solana/programs/axelar-solana-its/tests/module/flow_limits.rs +++ b/solana/programs/axelar-solana-its/tests/module/flow_limits.rs @@ -16,6 +16,7 @@ use solana_sdk::clock::Clock; use solana_sdk::program_pack::Pack as _; use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::Signer; +use solana_sdk::system_instruction; use spl_associated_token_account::get_associated_token_address_with_program_id; use spl_associated_token_account::instruction::create_associated_token_account; @@ -42,12 +43,25 @@ async fn test_incoming_interchain_transfer_with_limit(#[case] flow_limit: u64) { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = Pubkey::create_with_seed(&its_root_pda, "test_token", &axelar_solana_its::id()) diff --git a/solana/programs/axelar-solana-its/tests/module/from_solana_to_evm.rs b/solana/programs/axelar-solana-its/tests/module/from_solana_to_evm.rs index fd688f3a..80b3803f 100644 --- a/solana/programs/axelar-solana-its/tests/module/from_solana_to_evm.rs +++ b/solana/programs/axelar-solana-its/tests/module/from_solana_to_evm.rs @@ -218,9 +218,6 @@ async fn test_send_deploy_token_manager_from_solana_to_evm() { #[rstest] #[tokio::test] async fn test_send_interchain_transfer_from_solana_to_evm_native() { - // InterchainTokens deployed through ITS are always spl-token-2022 programs, - // hence we only test spl-token-2022 here. - let ItsProgramWrapper { mut solana_chain, chain_name: solana_id, @@ -1880,7 +1877,7 @@ async fn test_call_contract_with_interchain_token_offchain_data() { assert_eq!( emitted_event, &CallContractOffchainDataEvent { - sender_key: its_root_pda, + sender_key: axelar_solana_its::ID, destination_chain: ITS_HUB_TRUSTED_CHAIN_NAME.to_owned(), destination_contract_address: ITS_HUB_TRUSTED_CONTRACT_ADDRESS.to_owned(), payload_hash: solana_sdk::keccak::hashv(&[&offchain_data]).0 diff --git a/solana/programs/axelar-solana-its/tests/module/its_gmp_payload.rs b/solana/programs/axelar-solana-its/tests/module/its_gmp_payload.rs index c0109caa..c8411c8e 100644 --- a/solana/programs/axelar-solana-its/tests/module/its_gmp_payload.rs +++ b/solana/programs/axelar-solana-its/tests/module/its_gmp_payload.rs @@ -10,6 +10,7 @@ use solana_sdk::clock::Clock; use solana_sdk::program_pack::Pack as _; use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::Signer; +use solana_sdk::system_instruction; use spl_token_2022::extension::transfer_fee::TransferFeeConfig; use spl_token_2022::extension::{BaseStateWithExtensions, StateWithExtensions}; use spl_token_2022::state::Mint; @@ -31,12 +32,25 @@ async fn test_its_gmp_payload_deploy_token_manager( solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = Pubkey::create_with_seed(&its_root_pda, "test_token", &axelar_solana_its::id()) @@ -85,12 +99,25 @@ async fn test_its_gmp_payload_deploy_interchain_token() { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = Pubkey::create_with_seed(&its_root_pda, "test_token", &axelar_solana_its::id()) @@ -147,12 +174,25 @@ async fn test_its_gmp_payload_interchain_transfer_lock_unlock(#[case] token_prog solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = Pubkey::create_with_seed(&its_root_pda, "test_token", &axelar_solana_its::id()) @@ -270,12 +310,25 @@ async fn test_its_gmp_payload_interchain_transfer_lock_unlock_fee() { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = Pubkey::create_with_seed(&its_root_pda, "test_token", &axelar_solana_its::id()) diff --git a/solana/programs/axelar-solana-its/tests/module/main.rs b/solana/programs/axelar-solana-its/tests/module/main.rs index 2aa91dcd..16a090b9 100644 --- a/solana/programs/axelar-solana-its/tests/module/main.rs +++ b/solana/programs/axelar-solana-its/tests/module/main.rs @@ -1,4 +1,5 @@ #![allow( + missing_docs, clippy::expect_used, clippy::indexing_slicing, clippy::missing_errors_doc, @@ -53,6 +54,7 @@ use solana_sdk::instruction::Instruction; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; use solana_sdk::signer::Signer; +use solana_sdk::system_instruction; const SOLANA_CHAIN_NAME: &str = "solana-localnet"; const ITS_HUB_TRUSTED_CHAIN_NAME: &str = "axelar"; @@ -135,12 +137,25 @@ async fn axelar_solana_setup(with_memo: bool) -> ItsProgramWrapper { let _metadata = solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; ItsProgramWrapper { diff --git a/solana/programs/axelar-solana-its/tests/module/pause_unpause.rs b/solana/programs/axelar-solana-its/tests/module/pause_unpause.rs index 57543cd8..4d667d4e 100644 --- a/solana/programs/axelar-solana-its/tests/module/pause_unpause.rs +++ b/solana/programs/axelar-solana-its/tests/module/pause_unpause.rs @@ -7,7 +7,7 @@ use axelar_solana_its::state::token_manager; use evm_contracts_test_suite::ethers::abi::Bytes; use interchain_token_transfer_gmp::{DeployTokenManager, GMPPayload}; use solana_program_test::tokio; -use solana_sdk::{pubkey::Pubkey, signer::Signer}; +use solana_sdk::{pubkey::Pubkey, signer::Signer, system_instruction}; use crate::{program_test, relay_to_solana}; @@ -17,12 +17,25 @@ async fn test_its_gmp_payload_fail_when_paused() { let (its_root_pda, _) = axelar_solana_its::find_its_root_pda(&solana_chain.gateway_root_pda); solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; solana_chain @@ -82,12 +95,25 @@ async fn test_outbound_deployment_fails_when_paused() { .unwrap(); solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; solana_chain @@ -134,12 +160,25 @@ async fn test_fail_to_pause_not_being_owner() { let mut solana_chain = program_test().await; solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let tx_metadata = solana_chain diff --git a/solana/programs/axelar-solana-its/tests/module/role_management.rs b/solana/programs/axelar-solana-its/tests/module/role_management.rs index 9532692b..334ced5e 100644 --- a/solana/programs/axelar-solana-its/tests/module/role_management.rs +++ b/solana/programs/axelar-solana-its/tests/module/role_management.rs @@ -23,12 +23,25 @@ async fn test_successful_operator_transfer() { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let (its_root_pda, _) = axelar_solana_its::find_its_root_pda(&solana_chain.gateway_root_pda); @@ -78,12 +91,25 @@ async fn test_fail_transfer_when_not_holder() { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - Keypair::new().pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + Keypair::new().pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let bob = Keypair::new(); @@ -110,12 +136,25 @@ async fn test_successful_proposal_acceptance() { let mut solana_chain = program_test().await; solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let (its_root_pda, _) = axelar_solana_its::find_its_root_pda(&solana_chain.gateway_root_pda); @@ -889,12 +928,25 @@ async fn test_fail_mint_without_minter_role(#[case] token_program_id: Pubkey) { solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = @@ -959,12 +1011,25 @@ async fn test_successful_mint_with_minter_role(#[case] token_program_id: Pubkey) let mut solana_chain = program_test().await; solana_chain .fixture - .send_tx(&[axelar_solana_its::instructions::initialize( - solana_chain.fixture.payer.pubkey(), - solana_chain.gateway_root_pda, - solana_chain.fixture.payer.pubkey(), + .send_tx_with_custom_signers( + &[ + system_instruction::transfer( + &solana_chain.fixture.payer.pubkey(), + &solana_chain.upgrade_authority.pubkey(), + u32::MAX.into(), + ), + axelar_solana_its::instructions::initialize( + solana_chain.upgrade_authority.pubkey(), + solana_chain.gateway_root_pda, + solana_chain.fixture.payer.pubkey(), + ) + .unwrap(), + ], + &[ + &solana_chain.upgrade_authority.insecure_clone(), + &solana_chain.fixture.payer.insecure_clone(), + ], ) - .unwrap()]) .await; let token_id = axelar_solana_its::interchain_token_id(&solana_chain.fixture.payer.pubkey(), b"salt"); @@ -1019,3 +1084,39 @@ async fn test_successful_mint_with_minter_role(#[case] token_program_id: Pubkey) solana_chain.fixture.send_tx(&[mint_ix]).await; } + +#[tokio::test] +#[allow(clippy::unwrap_used)] +async fn test_fail_init_when_not_upgrade_authority() { + let mut solana_chain = program_test().await; + let not_upgrade_authority = Keypair::new(); + + // Create instruction with different payer + let ix = axelar_solana_its::instructions::initialize( + not_upgrade_authority.pubkey(), // Using different account instead of upgrade authority. + solana_chain.gateway_root_pda, + solana_chain.payer.pubkey(), + ) + .unwrap(); + + let signers = &[ + not_upgrade_authority, + solana_chain.fixture.payer.insecure_clone(), + ]; + + // Execute transaction and expect failure + let res = solana_chain + .send_tx_with_custom_signers(&[ix], signers) + .await + .expect_err("tx should fail without proper upgrade authority signature"); + + // Assert that the error message indicates the correct failure reason + assert!( + res.metadata + .unwrap() + .log_messages + .into_iter() + .any(|x| x.contains("Given authority is not the program upgrade authority")), + "Expected error message about invalid upgrade authority was not found!" + ); +} diff --git a/solana/programs/axelar-solana-memo-program/src/instruction.rs b/solana/programs/axelar-solana-memo-program/src/instruction.rs index de25b143..b2d3ff80 100644 --- a/solana/programs/axelar-solana-memo-program/src/instruction.rs +++ b/solana/programs/axelar-solana-memo-program/src/instruction.rs @@ -39,9 +39,11 @@ pub enum AxelarMemoInstruction { /// /// Accounts expected by this instruction: /// - /// 0. [signer] The address of payer / sender - /// 1. [] gateway root pda - /// 2. [] gateway program id + /// 0. [] Memo program id + /// 1. [w] Memo counter PDA + /// 2. [] Memo program CALL CONTRACT signing PDA + /// 3. [] gateway root pda + /// 4. [] gateway program id SendToGateway { /// Memo to send to the gateway memo: String, @@ -56,9 +58,11 @@ pub enum AxelarMemoInstruction { /// /// Accounts expected by this instruction: /// - /// 0. [signer] The address of payer / sender - /// 1. [] gateway root pda - /// 2. [] gateway program id + /// 0. [] Memo program id + /// 1. [w] Memo counter PDA + /// 2. [] Memo program CALL CONTRACT signing PDA + /// 3. [] gateway root pda + /// 4. [] gateway program id SendToGatewayOffchainMemo { /// Hash of the memo which is going to be sent directly to the relayer. memo_hash: [u8; 32], @@ -109,8 +113,11 @@ pub fn call_gateway_with_memo( destination_chain, destination_address, })?; + let signing_pda = axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); let accounts = vec![ - AccountMeta::new_readonly(*memo_counter_pda, false), + AccountMeta::new_readonly(crate::ID, false), + AccountMeta::new(*memo_counter_pda, false), + AccountMeta::new_readonly(signing_pda.0, false), AccountMeta::new_readonly(*gateway_root_pda, false), AccountMeta::new_readonly(*gateway_program_id, false), ]; @@ -138,8 +145,11 @@ pub fn call_gateway_with_offchain_memo( destination_chain, destination_address, })?; + let signing_pda = axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); let accounts = vec![ - AccountMeta::new_readonly(*memo_counter_pda, false), + AccountMeta::new_readonly(crate::ID, false), + AccountMeta::new(*memo_counter_pda, false), + AccountMeta::new_readonly(signing_pda.0, false), AccountMeta::new_readonly(*gateway_root_pda, false), AccountMeta::new_readonly(*gateway_program_id, false), ]; diff --git a/solana/programs/axelar-solana-memo-program/src/processor.rs b/solana/programs/axelar-solana-memo-program/src/processor.rs index 02459653..176041e7 100644 --- a/solana/programs/axelar-solana-memo-program/src/processor.rs +++ b/solana/programs/axelar-solana-memo-program/src/processor.rs @@ -121,22 +121,39 @@ pub fn process_native_ix( destination_address, } => { msg!("Instruction: SendToGateway"); + let program_account = next_account_info(account_info_iter)?; let counter_pda = next_account_info(account_info_iter)?; + let signing_pda_acc = next_account_info(account_info_iter)?; let gateway_root_pda = next_account_info(account_info_iter)?; let gateway_program = next_account_info(account_info_iter)?; + let counter_pda_account = counter_pda.check_initialized_pda::(program_id)?; + let signing_pda = axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); assert_counter_pda_seeds(&counter_pda_account, counter_pda.key, gateway_root_pda.key); + if &signing_pda.0 != signing_pda_acc.key { + msg!("invalid signing PDA"); + return Err(ProgramError::InvalidAccountData); + } invoke_signed( &axelar_solana_gateway::instructions::call_contract( *gateway_program.key, *gateway_root_pda.key, - *counter_pda.key, + crate::ID, + signing_pda.0, + signing_pda.1, destination_chain, destination_address, memo.into_bytes(), )?, - &[counter_pda.clone(), gateway_root_pda.clone()], - &[&[gateway_root_pda.key.as_ref(), &[counter_pda_account.bump]]], + &[ + program_account.clone(), + signing_pda_acc.clone(), + gateway_root_pda.clone(), + ], + &[&[ + axelar_solana_gateway::seed_prefixes::CALL_CONTRACT_SIGNING_SEED, + &[signing_pda.1], + ]], )?; } AxelarMemoInstruction::SendToGatewayOffchainMemo { @@ -145,22 +162,39 @@ pub fn process_native_ix( destination_address, } => { msg!("Instruction: SendToGatewayOffchainMemo"); + let program_account = next_account_info(account_info_iter)?; let counter_pda = next_account_info(account_info_iter)?; + let signing_pda_acc = next_account_info(account_info_iter)?; let gateway_root_pda = next_account_info(account_info_iter)?; let gateway_program = next_account_info(account_info_iter)?; + let counter_pda_account = counter_pda.check_initialized_pda::(program_id)?; assert_counter_pda_seeds(&counter_pda_account, counter_pda.key, gateway_root_pda.key); + let signing_pda = axelar_solana_gateway::get_call_contract_signing_pda(crate::ID); + if &signing_pda.0 != signing_pda_acc.key { + msg!("invalid signing PDA"); + return Err(ProgramError::InvalidAccountData); + } invoke_signed( &axelar_solana_gateway::instructions::call_contract_offchain_data( *gateway_program.key, *gateway_root_pda.key, - *counter_pda.key, + crate::ID, + signing_pda.0, + signing_pda.1, destination_chain, destination_address, memo_hash, )?, - &[counter_pda.clone(), gateway_root_pda.clone()], - &[&[gateway_root_pda.key.as_ref(), &[counter_pda_account.bump]]], + &[ + program_account.clone(), + signing_pda_acc.clone(), + gateway_root_pda.clone(), + ], + &[&[ + axelar_solana_gateway::seed_prefixes::CALL_CONTRACT_SIGNING_SEED, + &[signing_pda.1], + ]], )?; } AxelarMemoInstruction::Initialize { counter_pda_bump } => { diff --git a/solana/programs/axelar-solana-memo-program/tests/module/send_to_gateway.rs b/solana/programs/axelar-solana-memo-program/tests/module/send_to_gateway.rs index 465cdb30..5b73d7b3 100644 --- a/solana/programs/axelar-solana-memo-program/tests/module/send_to_gateway.rs +++ b/solana/programs/axelar-solana-memo-program/tests/module/send_to_gateway.rs @@ -57,7 +57,7 @@ async fn test_successfully_send_to_gateway() { assert_eq!( emitted_event, &CallContractEvent { - sender_key: counter_pda, + sender_key: axelar_solana_memo_program::ID, destination_chain, destination_contract_address: destination_address, payload: memo.as_bytes().to_vec(), @@ -123,7 +123,7 @@ async fn test_successfully_send_to_gateway_with_offchain_data() { assert_eq!( emitted_event, &CallContractOffchainDataEvent { - sender_key: counter_pda, + sender_key: axelar_solana_memo_program::ID, destination_chain, destination_contract_address: destination_address, payload_hash: solana_sdk::keccak::hash(memo.as_bytes()).0