diff --git a/.github/workflows/e2e-full.yml b/.github/workflows/e2e-full.yml index d83d08866..36cd83ccd 100644 --- a/.github/workflows/e2e-full.yml +++ b/.github/workflows/e2e-full.yml @@ -34,7 +34,7 @@ env: E2E_PRIVATE_CLUSTER: true E2E_WASM_LIGHT_CLIENT_TAG: ${{ inputs.wasm-eth-light-client-tag }} SOLANA_VERSION: "2.1.17" - ANCHOR_VERSION: "0.31.1" + ANCHOR_VERSION: "0.32.1" permissions: contents: read diff --git a/.github/workflows/e2e-minimal.yml b/.github/workflows/e2e-minimal.yml index b29d82a40..579591b24 100644 --- a/.github/workflows/e2e-minimal.yml +++ b/.github/workflows/e2e-minimal.yml @@ -35,7 +35,7 @@ env: E2E_PRIVATE_CLUSTER: true E2E_WASM_LIGHT_CLIENT_TAG: ${{ inputs.wasm-eth-light-client-tag }} SOLANA_VERSION: "2.1.17" - ANCHOR_VERSION: "0.31.1" + ANCHOR_VERSION: "0.32.1" permissions: contents: read diff --git a/.github/workflows/e2e-mock.yml b/.github/workflows/e2e-mock.yml index 5c6702909..0e6eee15d 100644 --- a/.github/workflows/e2e-mock.yml +++ b/.github/workflows/e2e-mock.yml @@ -23,7 +23,7 @@ env: SP1_PROVER: mock E2E_PROOF_TYPE: groth16 SOLANA_VERSION: "2.1.17" - ANCHOR_VERSION: "0.31.1" + ANCHOR_VERSION: "0.32.1" permissions: contents: read diff --git a/.github/workflows/solana.yml b/.github/workflows/solana.yml index 9935b53c7..70744592d 100644 --- a/.github/workflows/solana.yml +++ b/.github/workflows/solana.yml @@ -16,7 +16,7 @@ concurrency: env: SOLANA_VERSION: "2.1.17" - ANCHOR_VERSION: "0.31.1" + ANCHOR_VERSION: "0.32.1" jobs: build-and-test: @@ -41,7 +41,7 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - key: ${{ runner.os }}-cargo-solana-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-solana-${{ hashFiles('**/Cargo.lock') }}-anchor-${{ env.ANCHOR_VERSION }} restore-keys: | ${{ runner.os }}-cargo-solana- diff --git a/.gitignore b/.gitignore index d6bec0a8c..5930c9efc 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,9 @@ programs/relayer/config.json scripts/genesis.json network_params.yaml test-ledger + +# Solana keypairs - only localnet is tracked for E2E tests +solana-keypairs/**/* +!solana-keypairs/localnet/ +!solana-keypairs/localnet/** +!solana-keypairs/**/.gitkeep diff --git a/Cargo.lock b/Cargo.lock index 8e53e49fb..b4da37313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1231,9 +1231,9 @@ source = "git+https://github.com/Snowfork/milagro_bls?rev=bc2b5b5e8d48b7e2e1bfaa [[package]] name = "anchor-attribute-access-control" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +checksum = "7a883ca44ef14b2113615fc6d3a85fefc68b5002034e88db37f7f1f802f88aa9" dependencies = [ "anchor-syn", "proc-macro2", @@ -1243,9 +1243,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +checksum = "61c4d97763b29030412b4b80715076377edc9cc63bc3c9e667297778384b9fd2" dependencies = [ "anchor-syn", "bs58", @@ -1256,9 +1256,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +checksum = "aae3328bbf9bbd517a51621b1ba6cbec06cbbc25e8cfc7403bddf69bcf088206" dependencies = [ "anchor-syn", "quote", @@ -1267,9 +1267,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +checksum = "cf2398a6d9e16df1ee9d7d37d970a8246756de898c8dd16ef6bdbe4da20cf39a" dependencies = [ "anchor-syn", "quote", @@ -1278,9 +1278,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +checksum = "f12758f4ec2f0e98d4d56916c6fe95cb23d74b8723dd902c762c5ef46ebe7b65" dependencies = [ "anchor-syn", "proc-macro2", @@ -1290,9 +1290,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +checksum = "8c7193b5af2649813584aae6e3569c46fd59616a96af2083c556b13136c3830f" dependencies = [ "anchor-lang-idl", "anchor-syn", @@ -1307,9 +1307,9 @@ dependencies = [ [[package]] name = "anchor-client" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3e91b12501d37c8f07de9da8c7d22067998a7760a515231187afb47ded225" +checksum = "1a03f35d3cb6b26508da163e4f9bc7adc2a5445a1cf38bb1da6e6e3919c7d53d" dependencies = [ "anchor-lang", "anyhow", @@ -1317,7 +1317,9 @@ dependencies = [ "regex", "serde", "solana-account-decoder", - "solana-client", + "solana-pubsub-client", + "solana-rpc-client", + "solana-rpc-client-api", "solana-sdk", "thiserror 1.0.69", "tokio", @@ -1326,9 +1328,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +checksum = "d332d1a13c0fca1a446de140b656e66110a5e8406977dcb6a41e5d6f323760b0" dependencies = [ "anchor-syn", "quote", @@ -1337,9 +1339,9 @@ dependencies = [ [[package]] name = "anchor-derive-serde" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +checksum = "8656e4af182edaeae665fa2d2d7ee81148518b5bd0be9a67f2a381bb17da7d46" dependencies = [ "anchor-syn", "borsh-derive-internal", @@ -1350,9 +1352,9 @@ dependencies = [ [[package]] name = "anchor-derive-space" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +checksum = "dcff2a083560cd79817db07d89a4de39a2c4b2eaa00c1742cf0df49b25ff2bed" dependencies = [ "proc-macro2", "quote", @@ -1361,9 +1363,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +checksum = "e67d85d5376578f12d840c29ff323190f6eecd65b00a0b5f2b2f232751d049cc" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -1378,7 +1380,26 @@ dependencies = [ "bincode", "borsh 0.10.4", "bytemuck", - "solana-program", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface 3.0.0", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", "thiserror 1.0.69", ] @@ -1408,9 +1429,9 @@ dependencies = [ [[package]] name = "anchor-syn" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +checksum = "b93b69aa7d099b59378433f6d7e20e1008fc10c69e48b220270e5b3f2ec4c8be" dependencies = [ "anyhow", "bs58", @@ -8772,7 +8793,7 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-instruction", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-nonce", "solana-program-option", "solana-program-pack", @@ -9332,6 +9353,7 @@ dependencies = [ "prost", "solana-ibc-constants", "solana-ibc-proto", + "solana-program", ] [[package]] @@ -9379,6 +9401,19 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-invoke" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f5693c6de226b3626658377168b0184e94e8292ff16e3d31d4766e65627565" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", +] + [[package]] name = "solana-keccak-hasher" version = "2.2.1" @@ -9437,6 +9472,21 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "solana-loader-v3-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4be76cfa9afd84ca2f35ebc09f0da0f0092935ccdac0595d98447f259538c2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", +] + [[package]] name = "solana-loader-v3-interface" version = "5.0.0" @@ -9744,7 +9794,7 @@ dependencies = [ "solana-keccak-hasher", "solana-last-restart-slot", "solana-loader-v2-interface", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-loader-v4-interface", "solana-message", "solana-msg", @@ -10717,7 +10767,7 @@ dependencies = [ "solana-hash", "solana-instruction", "solana-loader-v2-interface", - "solana-loader-v3-interface", + "solana-loader-v3-interface 5.0.0", "solana-message", "solana-program-option", "solana-pubkey", diff --git a/Cargo.toml b/Cargo.toml index a45b717ff..734067238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,13 +160,14 @@ cw-ics08-wasm-eth-v1_2_0 = { package = "cw-ics08-wasm-eth", git = "https://githu ethereum-light-client-v1_2_0 = { package = "ethereum-light-client", git = "https://github.com/cosmos/solidity-ibc-eureka", tag = "cw-ics08-wasm-eth-v1.2.0", default-features = false } # Solana dependencies +solana-program = { version = "2.0" } solana-client = { version = "2.0" } solana-sdk = { version = "2.0" } solana-transaction-status = { version = "2.0" } borsh = { version = "1.3" } bincode = { version = "1.3" } -anchor-lang = { version = "0.31.1" } -anchor-client = { version = "0.31.1" } +anchor-lang = { version = "0.32.1" } +anchor-client = { version = "0.32.1" } # TODO: Remove these dependencies once ethereum wasm client v1.2.0 backwards compatibility is not needed ibc-eureka-relayer-eth-to-cosmos-v1_2 = { package = "ibc-eureka-relayer-eth-to-cosmos", git = "https://github.com/cosmos/solidity-ibc-eureka", rev = "d9f58589bee5881561cd8c769750a35448e5ebc8", default-features = false } diff --git a/README.md b/README.md index f80680e70..3d1c8f49f 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ anchor deploy > ``` > > Then navigate to the Solana programs directory and use anchor-nix commands: -> +> > ```sh > cd programs/solana > anchor-nix build @@ -146,6 +146,10 @@ anchor deploy > anchor-nix deploy > ``` +### Solana Programs + +For detailed information about Solana IBC programs including deployment, key generation, access control, and upgradability, see the **[Solana Programs README](programs/solana/README.md)**. + ## Unit Testing There are multiple unit tests for the solidity contracts located in the `test/` directory. The tests are written in Solidity using [foundry/forge](https://book.getfoundry.sh/forge/writing-tests). diff --git a/e2e/interchaintestv8/solana/anchor.go b/e2e/interchaintestv8/solana/anchor.go index c7d6953a6..de16e3abc 100644 --- a/e2e/interchaintestv8/solana/anchor.go +++ b/e2e/interchaintestv8/solana/anchor.go @@ -63,99 +63,187 @@ func DeploySolanaProgram(ctx context.Context, programSoFile, programKeypairFile, return getProgramIDAndSignatureFromSolanaDeploy(stdoutBytes) } -func AnchorDeploy(ctx context.Context, dir, programName, programKeypairFile, walletFile string, args ...string) (solana.PublicKey, solana.Signature, error) { - absWalletFile, err := filepath.Abs(walletFile) +// Parses raw Solana CLI deploy output and extracts Program ID and Signature. +func getProgramIDAndSignatureFromSolanaDeploy(stdout []byte) (solana.PublicKey, solana.Signature, error) { + outputStr := string(stdout) + + programIDRe := regexp.MustCompile(`(?m)Program Id:\s+([1-9A-HJ-NP-Za-km-z]{32,44})`) + signatureRe := regexp.MustCompile(`(?m)Signature:\s+([1-9A-HJ-NP-Za-km-z]{32,88})`) + + programIDMatch := programIDRe.FindStringSubmatch(outputStr) + signatureMatch := signatureRe.FindStringSubmatch(outputStr) + + if len(programIDMatch) < 2 { + return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("program id not found in output") + } + if len(signatureMatch) < 2 { + return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("signature not found in output") + } + + programID, err := solana.PublicKeyFromBase58(programIDMatch[1]) if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("failed to get absolute path for wallet file: %w", err) + return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Program Id: %w", err) } - absKeypairFile, err := filepath.Abs(programKeypairFile) + signature, err := solana.SignatureFromBase58(signatureMatch[1]) if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("failed to get absolute path for program keypair file: %w", err) + return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Signature: %w", err) + } + + return programID, signature, nil +} + +// WriteProgramBuffer writes a program binary to a buffer account for later upgrade. +// Returns the buffer account public key. +func WriteProgramBuffer(ctx context.Context, programSoFile, payerKeypairFile, rpcURL string) (solana.PublicKey, error) { + absProgramFile, err := filepath.Abs(programSoFile) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to get absolute path for program file: %w", err) + } + + absPayerFile, err := filepath.Abs(payerKeypairFile) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to get absolute path for payer keypair file: %w", err) } - args = append(args, "deploy", "-p", programName, "--provider.wallet", absWalletFile, "--program-keypair", absKeypairFile) cmd := exec.Command( - "anchor", args..., + "solana", "program", "write-buffer", + "--url", rpcURL, + "--keypair", absPayerFile, + "--use-rpc", + absProgramFile, ) - cmd.Dir = dir cmd.Env = os.Environ() var stdoutBuf bytes.Buffer - multiWriter := io.MultiWriter(os.Stdout, &stdoutBuf) cmd.Stdout = multiWriter cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - fmt.Println("Error deploy command", cmd.Args, err) - return solana.PublicKey{}, solana.Signature{}, err + fmt.Println("Error write-buffer command", cmd.Args, err) + return solana.PublicKey{}, err } - // Get the output as byte slices stdoutBytes := stdoutBuf.Bytes() - - return getProgramIDAndSignatureFromAnchorDeploy(stdoutBytes) + return getBufferAddressFromWriteBuffer(stdoutBytes) } -// Parses raw Solana CLI deploy output and extracts Program ID and Signature. -func getProgramIDAndSignatureFromSolanaDeploy(stdout []byte) (solana.PublicKey, solana.Signature, error) { +// Parses raw Solana CLI write-buffer output and extracts buffer address. +func getBufferAddressFromWriteBuffer(stdout []byte) (solana.PublicKey, error) { outputStr := string(stdout) - programIDRe := regexp.MustCompile(`(?m)Program Id:\s+([1-9A-HJ-NP-Za-km-z]{32,44})`) - signatureRe := regexp.MustCompile(`(?m)Signature:\s+([1-9A-HJ-NP-Za-km-z]{32,88})`) + bufferRe := regexp.MustCompile(`(?m)Buffer:\s+([1-9A-HJ-NP-Za-km-z]{32,44})`) - programIDMatch := programIDRe.FindStringSubmatch(outputStr) - signatureMatch := signatureRe.FindStringSubmatch(outputStr) + bufferMatch := bufferRe.FindStringSubmatch(outputStr) - if len(programIDMatch) < 2 { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("program id not found in output") - } - if len(signatureMatch) < 2 { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("signature not found in output") + if len(bufferMatch) < 2 { + return solana.PublicKey{}, fmt.Errorf("buffer address not found in output") } - programID, err := solana.PublicKeyFromBase58(programIDMatch[1]) + bufferAddr, err := solana.PublicKeyFromBase58(bufferMatch[1]) if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Program Id: %w", err) + return solana.PublicKey{}, fmt.Errorf("invalid buffer address: %w", err) } - signature, err := solana.SignatureFromBase58(signatureMatch[1]) + return bufferAddr, nil +} + +// SetBufferAuthority changes the buffer authority to a new authority. +// This is required when preparing a buffer for upgrade via AccessManager. +func SetBufferAuthority(ctx context.Context, bufferAddress, newAuthority solana.PublicKey, currentAuthorityKeypairFile, rpcURL string) error { + absAuthorityFile, err := filepath.Abs(currentAuthorityKeypairFile) if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Signature: %w", err) + return fmt.Errorf("failed to get absolute path for authority keypair file: %w", err) } - return programID, signature, nil -} + cmd := exec.Command( + "solana", "program", "set-buffer-authority", + "--url", rpcURL, + "--keypair", absAuthorityFile, // Fee payer + "--buffer-authority", absAuthorityFile, // Current authority (signer) + "--new-buffer-authority", newAuthority.String(), + bufferAddress.String(), + ) -// Parses raw Anchor CLI deploy output and extracts Program ID and Signature. -func getProgramIDAndSignatureFromAnchorDeploy(stdout []byte) (solana.PublicKey, solana.Signature, error) { - outputStr := string(stdout) + cmd.Env = os.Environ() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr - programIDRe := regexp.MustCompile(`(?m)^Program Id:\s+([1-9A-HJ-NP-Za-km-z]{32,44})$`) - signatureRe := regexp.MustCompile(`(?m)^Signature:\s+([1-9A-HJ-NP-Za-km-z]{32,88})$`) + if err := cmd.Run(); err != nil { + fmt.Println("Error set-buffer-authority command", cmd.Args, err) + return err + } - programIDMatch := programIDRe.FindStringSubmatch(outputStr) - signatureMatch := signatureRe.FindStringSubmatch(outputStr) + return nil +} - if len(programIDMatch) < 2 { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("program id not found") +// SetUpgradeAuthority changes the program upgrade authority to a new authority. +// This is required to transfer control from deployer to AccessManager. +// Supports setting PDAs as new authority via --skip-new-upgrade-authority-signer-check flag. +func SetUpgradeAuthority(ctx context.Context, programID, newAuthority solana.PublicKey, currentAuthorityKeypairFile, rpcURL string) error { + absAuthorityFile, err := filepath.Abs(currentAuthorityKeypairFile) + if err != nil { + return fmt.Errorf("failed to get absolute path for authority keypair file: %w", err) } - if len(signatureMatch) < 2 { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("signature not found") + + cmd := exec.Command( + "solana", "program", "set-upgrade-authority", + "--url", rpcURL, + "--keypair", absAuthorityFile, // Fee payer + "--upgrade-authority", absAuthorityFile, // Current authority (signer) + "--new-upgrade-authority", newAuthority.String(), + "--skip-new-upgrade-authority-signer-check", // Allow setting PDA as authority + programID.String(), + ) + + cmd.Env = os.Environ() + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + fmt.Println("Error set-upgrade-authority command", cmd.Args, err) + return err } - programID, err := solana.PublicKeyFromBase58(programIDMatch[1]) + return nil +} + +// UpgradeProgramDirect attempts to upgrade a program directly using BPF Loader (bypassing AccessManager). +// This is used in negative tests to verify that the old authority cannot bypass AccessManager +// after upgrade authority has been transferred to the AccessManager PDA. +func UpgradeProgramDirect(ctx context.Context, programID, bufferAddress solana.PublicKey, upgradeAuthorityKeypairFile, rpcURL string) error { + absAuthorityFile, err := filepath.Abs(upgradeAuthorityKeypairFile) if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Program Id: %w", err) + return fmt.Errorf("failed to get absolute path for authority keypair file: %w", err) } - signature, err := solana.SignatureFromBase58(signatureMatch[1]) - if err != nil { - return solana.PublicKey{}, solana.Signature{}, fmt.Errorf("invalid Signature: %w", err) + cmd := exec.Command( + "solana", "program", "deploy", + "--url", rpcURL, + "--keypair", absAuthorityFile, // Fee payer + "--upgrade-authority", absAuthorityFile, // Upgrade authority (should fail if not current authority) + "--program-id", programID.String(), + "--buffer", bufferAddress.String(), + "--use-rpc", + ) + + cmd.Env = os.Environ() + + var stderrBuf bytes.Buffer + cmd.Stdout = os.Stdout + cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + if err := cmd.Run(); err != nil { + // Include stderr in the error message for better error checking + stderrStr := stderrBuf.String() + if stderrStr != "" { + return fmt.Errorf("%w: %s", err, stderrStr) + } + return err } - return programID, signature, nil + return nil } diff --git a/e2e/interchaintestv8/solana/client.go b/e2e/interchaintestv8/solana/client.go index 74ed6ab3b..1d91d805a 100644 --- a/e2e/interchaintestv8/solana/client.go +++ b/e2e/interchaintestv8/solana/client.go @@ -337,10 +337,8 @@ func mustWrite(err error) { } func (s *Solana) GetSolanaClockTime(ctx context.Context) (int64, error) { - clockSysvarPubkey := solana.MustPublicKeyFromBase58("SysvarC1ock11111111111111111111111111111111") - // Use confirmed commitment to match relayer read commitment level - accountInfo, err := s.RPCClient.GetAccountInfoWithOpts(ctx, clockSysvarPubkey, &rpc.GetAccountInfoOpts{ + accountInfo, err := s.RPCClient.GetAccountInfoWithOpts(ctx, solana.SysVarClockPubkey, &rpc.GetAccountInfoOpts{ Commitment: rpc.CommitmentConfirmed, }) if err != nil { @@ -358,3 +356,15 @@ func (s *Solana) GetSolanaClockTime(ctx context.Context) (int64, error) { unixTimestamp := int64(binary.LittleEndian.Uint64(data[32:40])) return unixTimestamp, nil } + +// GetProgramDataAddress derives the ProgramData account address for an upgradeable program +func GetProgramDataAddress(programID solana.PublicKey) (solana.PublicKey, error) { + pda, _, err := solana.FindProgramAddress( + [][]byte{programID.Bytes()}, + solana.BPFLoaderUpgradeableProgramID, + ) + if err != nil { + return solana.PublicKey{}, fmt.Errorf("failed to derive program data address: %w", err) + } + return pda, nil +} diff --git a/packages/go-anchor/dummyibcapp/accounts.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/accounts.go similarity index 100% rename from packages/go-anchor/dummyibcapp/accounts.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/accounts.go diff --git a/packages/go-anchor/dummyibcapp/constants.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/constants.go similarity index 100% rename from packages/go-anchor/dummyibcapp/constants.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/constants.go diff --git a/packages/go-anchor/dummyibcapp/discriminators.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/discriminators.go similarity index 100% rename from packages/go-anchor/dummyibcapp/discriminators.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/discriminators.go diff --git a/packages/go-anchor/dummyibcapp/doc.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/doc.go similarity index 100% rename from packages/go-anchor/dummyibcapp/doc.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/doc.go diff --git a/packages/go-anchor/dummyibcapp/errors.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/errors.go similarity index 100% rename from packages/go-anchor/dummyibcapp/errors.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/errors.go diff --git a/packages/go-anchor/dummyibcapp/fetchers.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/fetchers.go similarity index 100% rename from packages/go-anchor/dummyibcapp/fetchers.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/fetchers.go diff --git a/packages/go-anchor/dummyibcapp/instructions.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/instructions.go similarity index 100% rename from packages/go-anchor/dummyibcapp/instructions.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/instructions.go diff --git a/packages/go-anchor/dummyibcapp/program-id.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/program-id.go similarity index 100% rename from packages/go-anchor/dummyibcapp/program-id.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/program-id.go diff --git a/packages/go-anchor/dummyibcapp/tests_test.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/tests_test.go similarity index 100% rename from packages/go-anchor/dummyibcapp/tests_test.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/tests_test.go diff --git a/packages/go-anchor/dummyibcapp/types.go b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/types.go similarity index 98% rename from packages/go-anchor/dummyibcapp/types.go rename to e2e/interchaintestv8/solana/go-anchor/dummyibcapp/types.go index ecf9296b5..0a62e89b7 100644 --- a/packages/go-anchor/dummyibcapp/types.go +++ b/e2e/interchaintestv8/solana/go-anchor/dummyibcapp/types.go @@ -476,9 +476,6 @@ type Ics26RouterStateClient struct { // Counterparty chain information CounterpartyInfo SolanaIbcTypesRouterCounterpartyInfo `json:"counterpartyInfo"` - // Authority that registered this client - Authority solanago.PublicKey `json:"authority"` - // Whether the client is active Active bool `json:"active"` @@ -507,11 +504,6 @@ func (obj Ics26RouterStateClient) MarshalWithEncoder(encoder *binary.Encoder) (e if err != nil { return errors.NewField("CounterpartyInfo", err) } - // Serialize `Authority`: - err = encoder.Encode(obj.Authority) - if err != nil { - return errors.NewField("Authority", err) - } // Serialize `Active`: err = encoder.Encode(obj.Active) if err != nil { @@ -556,11 +548,6 @@ func (obj *Ics26RouterStateClient) UnmarshalWithDecoder(decoder *binary.Decoder) if err != nil { return errors.NewField("CounterpartyInfo", err) } - // Deserialize `Authority`: - err = decoder.Decode(&obj.Authority) - if err != nil { - return errors.NewField("Authority", err) - } // Deserialize `Active`: err = decoder.Decode(&obj.Active) if err != nil { @@ -772,13 +759,15 @@ func UnmarshalIcs26RouterStateIbcApp(buf []byte) (*Ics26RouterStateIbcApp, error } // Router state account -// TODO: Implement multi-router ACL type Ics26RouterStateRouterState struct { // Schema version for upgrades Version SolanaIbcTypesRouterAccountVersion `json:"version"` - // Authority that can perform restricted operations - Authority solanago.PublicKey `json:"authority"` + // Whether the router is paused (emergency stop) + Paused bool `json:"paused"` + + // Access manager program ID for role-based access control + AccessManager solanago.PublicKey `json:"accessManager"` // Reserved space for future fields Reserved [256]uint8 `json:"reserved"` @@ -790,10 +779,15 @@ func (obj Ics26RouterStateRouterState) MarshalWithEncoder(encoder *binary.Encode if err != nil { return errors.NewField("Version", err) } - // Serialize `Authority`: - err = encoder.Encode(obj.Authority) + // Serialize `Paused`: + err = encoder.Encode(obj.Paused) if err != nil { - return errors.NewField("Authority", err) + return errors.NewField("Paused", err) + } + // Serialize `AccessManager`: + err = encoder.Encode(obj.AccessManager) + if err != nil { + return errors.NewField("AccessManager", err) } // Serialize `Reserved`: err = encoder.Encode(obj.Reserved) @@ -819,10 +813,15 @@ func (obj *Ics26RouterStateRouterState) UnmarshalWithDecoder(decoder *binary.Dec if err != nil { return errors.NewField("Version", err) } - // Deserialize `Authority`: - err = decoder.Decode(&obj.Authority) + // Deserialize `Paused`: + err = decoder.Decode(&obj.Paused) if err != nil { - return errors.NewField("Authority", err) + return errors.NewField("Paused", err) + } + // Deserialize `AccessManager`: + err = decoder.Decode(&obj.AccessManager) + if err != nil { + return errors.NewField("AccessManager", err) } // Deserialize `Reserved`: err = decoder.Decode(&obj.Reserved) diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go index 50e3c59fe..dfa7d8892 100644 --- a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/discriminators.go @@ -14,8 +14,9 @@ var () // Instruction discriminators var ( - Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} - Instruction_Increment = [8]byte{11, 18, 104, 9, 104, 174, 59, 33} - Instruction_Decrement = [8]byte{106, 227, 168, 59, 248, 27, 150, 101} - Instruction_GetCounter = [8]byte{178, 42, 93, 7, 140, 213, 93, 150} + Instruction_Initialize = [8]byte{175, 175, 109, 31, 13, 152, 155, 237} + Instruction_Increment = [8]byte{11, 18, 104, 9, 104, 174, 59, 33} + Instruction_Decrement = [8]byte{106, 227, 168, 59, 248, 27, 150, 101} + Instruction_GetCounter = [8]byte{178, 42, 93, 7, 140, 213, 93, 150} + Instruction_ProcessTestPayload = [8]byte{115, 179, 149, 147, 138, 1, 93, 55} ) diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go index 2e5d28a3c..f88c13193 100644 --- a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/doc.go @@ -14,4 +14,5 @@ package gmp_counter_app // - `increment`: Increment a user's counter (called by GMP) // - `decrement`: Decrement a user's counter (called by GMP) // - `get_counter`: Get a user's current counter value +// - `process_test_payload`: Test instruction for GMP scenarios (large payloads, many accounts) // diff --git a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go index f57343088..c14e88bec 100644 --- a/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go +++ b/e2e/interchaintestv8/solana/go-anchor/gmpcounter/instructions.go @@ -199,3 +199,49 @@ func NewGetCounterInstruction( buf__.Bytes(), ), nil } + +// Builds a "process_test_payload" instruction. +// Process test payload (for GMP testing: large payloads, many accounts) // Always returns "ok" as acknowledgement +func NewProcessTestPayloadInstruction( + // Params: + dataParam []byte, + + // Accounts: + appStateAccount solanago.PublicKey, + payerAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_ProcessTestPayload[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `dataParam`: + err = enc__.Encode(dataParam) + if err != nil { + return nil, errors.NewField("dataParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "app_state": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(appStateAccount, false, false)) + // Account 1 "payer": Writable, Signer, Required + accounts__.Append(solanago.NewAccountMeta(payerAccount, true, true)) + // Account 2 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} diff --git a/e2e/interchaintestv8/solana/ibc.go b/e2e/interchaintestv8/solana/ibc.go index dee04d931..2c6ce1a90 100644 --- a/e2e/interchaintestv8/solana/ibc.go +++ b/e2e/interchaintestv8/solana/ibc.go @@ -8,6 +8,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" ics07_tendermint "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics07tendermint" ics26_router "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics26router" ics27_gmp "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics27gmp" @@ -32,6 +33,7 @@ func (s *Solana) GetNextSequenceNumber(ctx context.Context, clientSequencePDA so } func (s *Solana) CreateIBCAddressLookupTableAccounts(cosmosChainID string, gmpPortID string, clientID string, userPubKey solana.PublicKey) []solana.PublicKey { + accessManagerPDA, _ := AccessManager.AccessManagerPDA(access_manager.ProgramID) routerStatePDA, _ := Ics26Router.RouterStatePDA(ics26_router.ProgramID) ibcAppPDA, _ := Ics26Router.IbcAppPDA(ics26_router.ProgramID, []byte(gmpPortID)) gmpAppStatePDA, _ := Ics27Gmp.AppStateGmpportPDA(ics27_gmp.ProgramID) @@ -42,10 +44,12 @@ func (s *Solana) CreateIBCAddressLookupTableAccounts(cosmosChainID string, gmpPo return []solana.PublicKey{ solana.SystemProgramID, ComputeBudgetProgramID(), + access_manager.ProgramID, solana.SysVarInstructionsPubkey, ics26_router.ProgramID, ics07_tendermint.ProgramID, ics27_gmp.ProgramID, + accessManagerPDA, routerStatePDA, userPubKey, ibcAppPDA, diff --git a/e2e/interchaintestv8/solana/pda.go b/e2e/interchaintestv8/solana/pda.go index df1d75763..c5bcf61cc 100644 --- a/e2e/interchaintestv8/solana/pda.go +++ b/e2e/interchaintestv8/solana/pda.go @@ -13,6 +13,7 @@ import ( solanago "github.com/gagliardetto/solana-go" ) +type accessManagerPDAs struct{} type dummyIbcAppPDAs struct{} type gmpCounterAppPDAs struct{} type ics07TendermintPDAs struct{} @@ -21,6 +22,7 @@ type ics27GmpPDAs struct{} type mockLightClientPDAs struct{} var ( + AccessManager = accessManagerPDAs{} DummyIbcApp = dummyIbcAppPDAs{} GmpCounterApp = gmpCounterAppPDAs{} Ics07Tendermint = ics07TendermintPDAs{} @@ -29,6 +31,28 @@ var ( MockLightClient = mockLightClientPDAs{} ) +func (accessManagerPDAs) AccessManagerPDA(programID solanago.PublicKey) (solanago.PublicKey, uint8) { + pda, bump, err := solanago.FindProgramAddress( + [][]byte{[]byte("access_manager")}, + programID, + ) + if err != nil { + panic(fmt.Sprintf("failed to derive AccessManager.AccessManagerPDA PDA: %v", err)) + } + return pda, bump +} + +func (accessManagerPDAs) UpgradeAuthorityPDA(programID solanago.PublicKey, targetProgram []byte) (solanago.PublicKey, uint8) { + pda, bump, err := solanago.FindProgramAddress( + [][]byte{[]byte("upgrade_authority"), targetProgram}, + programID, + ) + if err != nil { + panic(fmt.Sprintf("failed to derive AccessManager.UpgradeAuthorityPDA PDA: %v", err)) + } + return pda, bump +} + func (dummyIbcAppPDAs) AppStateTransferPDA(programID solanago.PublicKey) (solanago.PublicKey, uint8) { pda, bump, err := solanago.FindProgramAddress( [][]byte{[]byte("app_state"), []byte("transfer")}, diff --git a/e2e/interchaintestv8/solana/test_helpers.go b/e2e/interchaintestv8/solana/test_helpers.go index 93b6fa7aa..b1380be74 100644 --- a/e2e/interchaintestv8/solana/test_helpers.go +++ b/e2e/interchaintestv8/solana/test_helpers.go @@ -328,9 +328,13 @@ func (s *Solana) SubmitChunkedUpdateClient(ctx context.Context, t *testing.T, re close(chunkResults) chunksTotal := time.Since(chunksStart) - avgChunkTime := chunksTotal / time.Duration(chunkCount) - t.Logf("--- Phase 1 Complete: All %d chunks uploaded in %v (avg: %v/chunk) ---", - chunkCount, chunksTotal, avgChunkTime) + if chunkCount > 0 { + avgChunkTime := chunksTotal / time.Duration(chunkCount) + t.Logf("--- Phase 1 Complete: All %d chunks uploaded in %v (avg: %v/chunk) ---", + chunkCount, chunksTotal, avgChunkTime) + } else { + t.Logf("--- Phase 1 Complete: No chunks needed (update fits in single transaction) ---") + } t.Logf("--- Phase 2: Assembling and updating client ---") assemblyStart := time.Now() diff --git a/e2e/interchaintestv8/solana_gmp_test.go b/e2e/interchaintestv8/solana_gmp_test.go index 53a4a5045..f802b6fc5 100644 --- a/e2e/interchaintestv8/solana_gmp_test.go +++ b/e2e/interchaintestv8/solana_gmp_test.go @@ -1,17 +1,20 @@ package main import ( + "bytes" "context" "crypto/sha256" "encoding/binary" "encoding/hex" "fmt" + "strings" "testing" "time" "github.com/cosmos/gogoproto/proto" gmp_counter_app "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/gmpcounter" malicious_caller "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/maliciouscaller" + borsh "github.com/gagliardetto/binary" "github.com/stretchr/testify/suite" solanago "github.com/gagliardetto/solana-go" @@ -26,8 +29,10 @@ import ( gmptypes "github.com/cosmos/ibc-go/v10/modules/apps/27-gmp/types" channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" + "github.com/cosmos/interchaintest/v10/chain/cosmos" "github.com/cosmos/interchaintest/v10/ibc" + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" ics26_router "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics26router" ics27_gmp "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics27gmp" @@ -64,6 +69,85 @@ const ( DummyTargetProgramID = "4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi" ) +// ChunkingTestScenarios defines the test configurations to run +// +// Limits discovered through testing: +// - Payload size (isolated): ≤1100B safe, ≥1200B fails with ProgramFailedToComplete (memory limit) +// - Account count (isolated): ≤15 accounts safe, ≥16 fails with transaction too large (1652 > 1644 bytes) +// +// Combined payload + accounts limits: +// - 1000B payload: ≤4 accounts safe, ≥8 accounts fails +// - Pattern: Higher payload requires fewer accounts (shared memory budget) +var ChunkingTestScenarios = []ChunkingTestScenario{ + // REQUIRED: Baseline scenario (must pass) + { + Name: "baseline", + PayloadSize: 500, + AccountsCount: 5, + RequiredToPass: true, + }, + // Payload size limit (isolated) + { + Name: "max_payload_1100b", + PayloadSize: 1100, + AccountsCount: 0, + RequiredToPass: true, + }, + { + Name: "oversize_payload_1200b", + PayloadSize: 1200, + AccountsCount: 0, + RequiredToPass: false, + }, + // Account count limit (isolated) + { + Name: "max_accounts_15", + PayloadSize: 100, + AccountsCount: 15, + RequiredToPass: true, + }, + { + Name: "too_many_accounts_16", + PayloadSize: 100, + AccountsCount: 16, + RequiredToPass: false, + }, + // Combined payload + accounts (demonstrates memory budget trade-off) + { + Name: "large_payload_few_accounts", + PayloadSize: 1000, + AccountsCount: 4, + RequiredToPass: true, + }, + { + Name: "large_payload_many_accounts", + PayloadSize: 1000, + AccountsCount: 8, + RequiredToPass: false, + }, + { + Name: "medium_payload_max_accounts", + PayloadSize: 500, + AccountsCount: 15, + RequiredToPass: true, + }, +} + +// ChunkingTestScenario defines a single test scenario configuration +type ChunkingTestScenario struct { + Name string // Descriptive name for the scenario + PayloadSize int // Size of payload in bytes + AccountsCount int // Number of additional accounts to include + RequiredToPass bool // If true, the entire test fails if this scenario fails +} + +// ChunkingTestResult stores the result of a test scenario +type ChunkingTestResult struct { + Scenario ChunkingTestScenario + Success bool + Error string +} + // gmpAccountPDA derives GMP account PDA with sender hash // This is a specialized PDA that uses SHA256 hashing and is not in the IDL. // GMP accounts are stateless - no account storage, only PDA validation. @@ -95,17 +179,17 @@ func (s *IbcEurekaSolanaTestSuite) initializeGMPCounterApp(ctx context.Context) counterAppStatePDA, _ := solana.GmpCounterApp.CounterAppStatePDA(s.GMPCounterProgramID) initInstruction, err := gmp_counter_app.NewInitializeInstruction( - s.SolanaUser.PublicKey(), // authority + s.SolanaRelayer.PublicKey(), // authority counterAppStatePDA, - s.SolanaUser.PublicKey(), // payer + s.SolanaRelayer.PublicKey(), // payer solanago.SystemProgramID, ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), initInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("GMP Counter app initialized") })) @@ -120,20 +204,20 @@ func (s *IbcEurekaSolanaTestSuite) initializeICS27GMP(ctx context.Context) solan // Find GMP app state PDA (using standard pattern with port_id) gmpAppStatePDA, _ := solana.Ics27Gmp.AppStateGmpportPDA(ics27_gmp.ProgramID) - // Initialize ICS27 GMP app using the fixed version (see instructions-fixed.go) - // Using GMP port for proper GMP functionality - initInstruction, err := ics27_gmp.NewInitializeInstructionFixed( - gmpAppStatePDA, // app state account - s.SolanaUser.PublicKey(), // payer - s.SolanaUser.PublicKey(), // authority - solanago.SystemProgramID, // system program + // Initialize ICS27 GMP app + initInstruction, err := ics27_gmp.NewInitializeInstruction( + access_manager.ProgramID, // access_manager program ID + gmpAppStatePDA, // app state account + s.SolanaRelayer.PublicKey(), // payer + solanago.SystemProgramID, // system program + solanago.SysVarInstructionsPubkey, // instructions sysvar ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), initInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("ICS27 GMP program initialized at: %s", s.ICS27GMPProgramID) @@ -145,23 +229,26 @@ func (s *IbcEurekaSolanaTestSuite) initializeICS27GMP(ctx context.Context) solan s.Require().True(s.Run("Register ICS27 GMP with Router", func() { routerStateAccount, _ := solana.Ics26Router.RouterStatePDA(ics26_router.ProgramID) + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) ibcAppAccount, _ := solana.Ics26Router.IbcAppPDA(ics26_router.ProgramID, []byte(GMPPortID)) registerInstruction, err := ics26_router.NewAddIbcAppInstruction( GMPPortID, routerStateAccount, + accessControlAccount, ibcAppAccount, s.ICS27GMPProgramID, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, + solanago.SysVarInstructionsPubkey, ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), registerInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), registerInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("ICS27 GMP registered with router on port: %s (using proper GMP port)", GMPPortID) })) @@ -325,7 +412,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCounterFromCosmos() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) // Now retrieve and relay the GMP packet resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ @@ -339,7 +426,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCounterFromCosmos() { s.Require().NotEmpty(resp.Tx, "Relay should return transaction") // Execute on Solana using chunked submission - solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("%s: GMP execution completed on Solana", userLabel) @@ -539,7 +626,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { s.Require().True(s.Run("Setup SPL Token Infrastructure", func() { s.Require().True(s.Run("Create Test SPL Token Mint", func() { var err error - tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaUser, 6) + tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaRelayer, 6) s.Require().NoError(err) s.T().Logf("Created test SPL token mint: %s (6 decimals)", tokenMint.String()) })) @@ -553,7 +640,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { var err error // Create source token account (owned by ICS27 PDA) - sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, ics27AccountPDA) + sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, ics27AccountPDA) s.Require().NoError(err) s.T().Logf("Created source token account (owned by ICS27 PDA): %s", sourceTokenAccount.String()) @@ -561,7 +648,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { recipientWallet, err = s.SolanaChain.CreateAndFundWallet() s.Require().NoError(err) - destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, recipientWallet.PublicKey()) + destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, recipientWallet.PublicKey()) s.Require().NoError(err) s.T().Logf("Created destination token account (owned by recipient): %s", destTokenAccount.String()) })) @@ -569,7 +656,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { s.Require().True(s.Run("Mint Tokens to ICS27 PDA", func() { // Mint 10 tokens (10,000,000 with 6 decimals) mintAmount := SPLTokenMintAmount - err := s.SolanaChain.MintTokensTo(ctx, s.SolanaUser, tokenMint, sourceTokenAccount, mintAmount) + err := s.SolanaChain.MintTokensTo(ctx, s.SolanaRelayer, tokenMint, sourceTokenAccount, mintAmount) s.Require().NoError(err) balance, err := s.SolanaChain.GetTokenBalance(ctx, sourceTokenAccount) @@ -662,7 +749,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) })) s.Require().True(s.Run("Retrieve relay tx from relayer", func() { @@ -676,7 +763,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSPLTokenTransferFromCosmos() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("SPL transfer executed on Solana: %s", solanaRelayTxSig) })) @@ -748,7 +835,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSendCallFromSolana() { var computedAddress sdk.AccAddress s.Require().True(s.Run("Fund pre-computed ICS27 address on Cosmos", func() { - solanaUserAddress := s.SolanaUser.PublicKey().String() + solanaUserAddress := s.SolanaRelayer.PublicKey().String() // Use CosmosClientID (08-wasm-0) - the dest_client on Cosmos // The GMP keeper creates accounts using NewAccountIdentifier(destClient, sender, salt) @@ -839,8 +926,8 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSendCallFromSolana() { Memo: "send from Solana to Cosmos", }, gmpAppStatePDA, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), + s.SolanaRelayer.PublicKey(), ics26_router.ProgramID, routerStatePDA, clientSequencePDA, @@ -856,12 +943,12 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSendCallFromSolana() { s.Require().True(s.Run("Broadcast transaction", func() { tx, err := s.SolanaChain.NewTransactionFromInstructions( - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), sendCallInstruction, ) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.Require().NotEmpty(sig) @@ -931,7 +1018,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSendCallFromSolana() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaRelayer) })) s.Require().True(s.Run("Relay acknowledgement", func() { @@ -945,7 +1032,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPSendCallFromSolana() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Acknowledgement transaction broadcasted: %s", sig) })) @@ -1042,7 +1129,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromSolana() { var computedAddress sdk.AccAddress s.Require().True(s.Run("Fund pre-computed ICS27 address on Cosmos", func() { - solanaUserAddress := s.SolanaUser.PublicKey().String() + solanaUserAddress := s.SolanaRelayer.PublicKey().String() res, err := e2esuite.GRPCQuery[gmptypes.QueryAccountAddressResponse](ctx, simd, &gmptypes.QueryAccountAddressRequest{ ClientId: CosmosClientID, @@ -1127,8 +1214,8 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromSolana() { Memo: "timeout test from Solana", }, gmpAppStatePDA, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), + s.SolanaRelayer.PublicKey(), ics26_router.ProgramID, routerStatePDA, clientSequencePDA, @@ -1144,12 +1231,12 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromSolana() { s.Require().True(s.Run("Broadcast transaction", func() { tx, err := s.SolanaChain.NewTransactionFromInstructions( - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), sendCallInstruction, ) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.Require().NotEmpty(sig) @@ -1190,7 +1277,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromSolana() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaRelayer) })) s.Require().True(s.Run("Relay timeout transaction", func() { @@ -1204,7 +1291,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromSolana() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Timeout transaction broadcasted: %s", sig) @@ -1326,7 +1413,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromCosmos() { s.Require().True(s.Run("Setup SPL Token Infrastructure", func() { s.Require().True(s.Run("Create Test SPL Token Mint", func() { var err error - tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaUser, SPLTokenDecimals) + tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaRelayer, SPLTokenDecimals) s.Require().NoError(err) s.T().Logf("Created test SPL token mint: %s", tokenMint.String()) })) @@ -1338,10 +1425,10 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromCosmos() { s.Require().True(s.Run("Create and Fund Token Account", func() { var err error - sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, ics27AccountPDA) + sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, ics27AccountPDA) s.Require().NoError(err) - err = s.SolanaChain.MintTokensTo(ctx, s.SolanaUser, tokenMint, sourceTokenAccount, tokenAmount) + err = s.SolanaChain.MintTokensTo(ctx, s.SolanaRelayer, tokenMint, sourceTokenAccount, tokenAmount) s.Require().NoError(err) s.T().Logf("Created and funded source token account: %s", sourceTokenAccount.String()) })) @@ -1360,7 +1447,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromCosmos() { recipientWallet, err = s.SolanaChain.CreateAndFundWallet() s.Require().NoError(err) - destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, recipientWallet.PublicKey()) + destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, recipientWallet.PublicKey()) s.Require().NoError(err) splTransferInstruction := token.NewTransferInstruction( @@ -1419,7 +1506,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromCosmos() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) })) resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ @@ -1483,7 +1570,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPTimeoutFromCosmos() { })) s.Require().True(s.Run("Verify recvPacket fails on Solana after timeout", func() { - _, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), recvRelayTxs, s.SolanaUser) + _, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), recvRelayTxs, s.SolanaRelayer) s.Require().Error(err) })) })) @@ -1561,7 +1648,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromCosmos() { s.Require().True(s.Run("Setup SPL Token Infrastructure", func() { s.Require().True(s.Run("Create Test SPL Token Mint", func() { var err error - tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaUser, SPLTokenDecimals) + tokenMint, err = s.SolanaChain.CreateSPLTokenMint(ctx, s.SolanaRelayer, SPLTokenDecimals) s.Require().NoError(err) s.T().Logf("Created test SPL token mint: %s (decimals: %d)", tokenMint.String(), SPLTokenDecimals) })) @@ -1575,7 +1662,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromCosmos() { var err error // Create source token account (owned by ICS27 PDA) - sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, ics27AccountPDA) + sourceTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, ics27AccountPDA) s.Require().NoError(err) s.T().Logf("Created source token account (owned by ICS27 PDA): %s", sourceTokenAccount.String()) @@ -1583,14 +1670,14 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromCosmos() { recipientWallet, err = s.SolanaChain.CreateAndFundWallet() s.Require().NoError(err) - destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaUser, tokenMint, recipientWallet.PublicKey()) + destTokenAccount, err = s.SolanaChain.CreateTokenAccount(ctx, s.SolanaRelayer, tokenMint, recipientWallet.PublicKey()) s.Require().NoError(err) s.T().Logf("Created destination token account (owned by recipient): %s", destTokenAccount.String()) })) s.Require().True(s.Run("Mint Insufficient Tokens to ICS27 PDA", func() { // CRITICAL: Mint ONLY 5 tokens (we'll try to transfer 10 later) - err := s.SolanaChain.MintTokensTo(ctx, s.SolanaUser, tokenMint, sourceTokenAccount, insufficientAmount) + err := s.SolanaChain.MintTokensTo(ctx, s.SolanaRelayer, tokenMint, sourceTokenAccount, insufficientAmount) s.Require().NoError(err) balance, err := s.SolanaChain.GetTokenBalance(ctx, sourceTokenAccount) @@ -1682,7 +1769,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromCosmos() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) })) s.Require().True(s.Run("Relay packet to Solana (will fail)", func() { @@ -1698,7 +1785,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromCosmos() { // Transaction will fail due to CPI error (insufficient balance for SPL token transfer) // Expected error: SPL Token program InstructionError with Custom error code 1 (InsufficientFunds) - _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().Error(err) s.T().Logf("Received error: %v", err) // Expected Solana error format: map[InstructionError:[%!s(float64=2) map[Custom:%!s(float64=1)]]] @@ -1746,7 +1833,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromSolana() { var computedAddress sdk.AccAddress s.Require().True(s.Run("Compute ICS27 address on Cosmos (will have zero balance)", func() { - solanaUserAddress := s.SolanaUser.PublicKey().String() + solanaUserAddress := s.SolanaRelayer.PublicKey().String() res, err := e2esuite.GRPCQuery[gmptypes.QueryAccountAddressResponse](ctx, simd, &gmptypes.QueryAccountAddressRequest{ ClientId: CosmosClientID, @@ -1831,8 +1918,8 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromSolana() { Memo: "send from Solana to Cosmos (will fail on execution)", }, gmpAppStatePDA, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), + s.SolanaRelayer.PublicKey(), ics26_router.ProgramID, routerStatePDA, clientSequencePDA, @@ -1847,12 +1934,12 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromSolana() { s.Require().True(s.Run("Broadcast transaction", func() { tx, err := s.SolanaChain.NewTransactionFromInstructions( - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), sendCallInstruction, ) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.Require().NotEmpty(sig) @@ -1906,7 +1993,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromSolana() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(updateResp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) })) s.Require().True(s.Run("Get acknowledgment relay transactions", func() { @@ -1923,7 +2010,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPFailedExecutionFromSolana() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx) - sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + sig, err := s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Error acknowledgment successfully relayed to Solana: %s", sig) })) @@ -2000,7 +2087,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { instructionData, accountMetas, ics27_gmp.ProgramID, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), ) if err != nil { return nil, err @@ -2022,7 +2109,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2042,7 +2129,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Encoding: testvalues.Ics27ProtobufEncoding, Value: packetDataBytes, }, - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program, but call it directly (not via CPI) @@ -2053,7 +2140,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router, but we're calling directly! solanago.SysVarInstructionsPubkey, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) @@ -2071,10 +2158,10 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { s.T().Log("Attempting direct call to on_recv_packet (bypassing router)...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), gmpIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), gmpIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - validate_cpi_caller detects direct call via instructions sysvar s.Require().Error(err, "on_recv_packet should reject direct call") @@ -2096,7 +2183,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2116,7 +2203,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Encoding: testvalues.Ics27ProtobufEncoding, Value: packetDataBytes, }, - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program @@ -2126,7 +2213,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router solanago.SysVarInstructionsPubkey, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) @@ -2148,10 +2235,10 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { s.T().Log("Attempting unauthorized CPI to on_recv_packet...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), proxyIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), proxyIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - on_recv_packet has instructions sysvar validation s.Require().Error(err, "on_recv_packet should reject unauthorized CPI") @@ -2173,7 +2260,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2194,7 +2281,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Value: packetDataBytes, }, Acknowledgement: []byte("test ack"), - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program, but call it directly (not via CPI) @@ -2203,17 +2290,17 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router, but we're calling directly! solanago.SysVarInstructionsPubkey, // instruction_sysvar - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) s.T().Log("Attempting direct call to on_ack_packet (bypassing router)...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), gmpIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), gmpIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - validate_cpi_caller detects direct call via instructions sysvar s.Require().Error(err, "on_ack_packet should reject direct call") @@ -2235,7 +2322,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2256,7 +2343,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Value: packetDataBytes, }, Acknowledgement: []byte("test ack"), - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program @@ -2265,7 +2352,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router solanago.SysVarInstructionsPubkey, // instruction_sysvar - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) @@ -2276,10 +2363,10 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { s.T().Log("Attempting unauthorized CPI to on_ack_packet...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), proxyIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), proxyIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - on_ack_packet has instructions sysvar validation s.Require().Error(err, "on_ack_packet should reject unauthorized CPI") @@ -2301,7 +2388,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2321,7 +2408,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Encoding: testvalues.Ics27ProtobufEncoding, Value: packetDataBytes, }, - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program, but call it directly (not via CPI) @@ -2330,17 +2417,17 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router, but we're calling directly! solanago.SysVarInstructionsPubkey, // instruction_sysvar - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) s.T().Log("Attempting direct call to on_timeout_packet (bypassing router)...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), gmpIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), gmpIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - validate_cpi_caller detects direct call via instructions sysvar s.Require().Error(err, "on_timeout_packet should reject direct call") @@ -2362,7 +2449,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { mockPacketData := gmptypes.GMPPacketData{ Sender: "cosmos1test", - Receiver: s.SolanaUser.PublicKey().String(), + Receiver: s.SolanaRelayer.PublicKey().String(), Salt: []byte{}, Payload: []byte("test payload"), Memo: "", @@ -2382,7 +2469,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { Encoding: testvalues.Ics27ProtobufEncoding, Value: packetDataBytes, }, - Relayer: s.SolanaUser.PublicKey(), + Relayer: s.SolanaRelayer.PublicKey(), } // Build instruction with CORRECT router_program @@ -2391,7 +2478,7 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { gmpAppStatePDA, ics26_router.ProgramID, // Correct router solanago.SysVarInstructionsPubkey, // instruction_sysvar - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) @@ -2402,10 +2489,10 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { s.T().Log("Attempting unauthorized CPI to on_timeout_packet...") - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), proxyIx) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), proxyIx) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, s.SolanaRelayer) // Should FAIL - on_timeout_packet has instructions sysvar validation s.Require().Error(err, "on_timeout_packet should reject unauthorized CPI") @@ -2419,3 +2506,295 @@ func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPCPISecurity() { s.T().Log("✓ on_timeout_packet SECURE - validates CPI caller via instructions sysvar") })) } + +func (s *IbcEurekaSolanaGMPTestSuite) Test_GMPChunking() { + ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupSuite(ctx) + s.initializeICS27GMP(ctx) + + simd := s.CosmosChains[0] + + // Initialize GMP counter app (shared across all scenarios) + gmpCounterProgramID := s.initializeGMPCounterApp(ctx) + + cosmosUser := s.CosmosUsers[0] + + var results []ChunkingTestResult + + // Run all scenarios sequentially and collect results + s.Require().True(s.Run("Execute Scenarios", func() { + s.T().Logf("Running %d chunking test scenarios sequentially", len(ChunkingTestScenarios)) + + results = make([]ChunkingTestResult, 0, len(ChunkingTestScenarios)) + for _, scenario := range ChunkingTestScenarios { + requiredStr := "" + if scenario.RequiredToPass { + requiredStr = " [REQUIRED]" + } + s.T().Logf("\n=== Starting: %s%s ===", scenario.Name, requiredStr) + s.T().Logf(" Payload: %d bytes, Accounts: %d", + scenario.PayloadSize, scenario.AccountsCount) + + result := s.runChunkingScenario(ctx, simd, cosmosUser, gmpCounterProgramID, scenario) + + if result.Success { + s.T().Logf("✓ Scenario %s: SUCCESS%s", scenario.Name, requiredStr) + } else { + s.T().Logf("✗ Scenario %s: FAILED%s - %s", scenario.Name, requiredStr, result.Error) + } + + results = append(results, result) + } + })) + + // Print summary + s.Require().True(s.Run("Verify Results", func() { + s.T().Logf("\n=== Chunking Test Summary ===") + s.T().Logf("%-40s %8s %5s %5s %s", "Scenario", "Payload", "Accs", "Req?", "Status") + s.T().Logf("%s", strings.Repeat("-", 80)) + + successCount := 0 + requiredCount := 0 + requiredSuccessCount := 0 + var failedRequiredScenarios []string + + for _, result := range results { + if result.Scenario.RequiredToPass { + requiredCount++ + if result.Success { + requiredSuccessCount++ + } else { + failedRequiredScenarios = append(failedRequiredScenarios, result.Scenario.Name) + } + } + if result.Success { + successCount++ + } + + required := " " + if result.Scenario.RequiredToPass { + required = "✓" + } + + status := "✓ PASS" + if !result.Success { + status = "✗ FAIL" + } + + s.T().Logf("%-40s %6dB %5d %5s %s", + result.Scenario.Name, + result.Scenario.PayloadSize, + result.Scenario.AccountsCount, + required, + status, + ) + } + + s.T().Logf("%s", strings.Repeat("-", 80)) + + if len(failedRequiredScenarios) > 0 { + s.T().Logf("\nErrors:") + for _, result := range results { + if !result.Success && result.Error != "" { + s.T().Logf(" [%s]: %s", result.Scenario.Name, result.Error) + } + } + } + + s.T().Logf("\nSummary:") + s.T().Logf(" Total: %d/%d passed (%.1f%%)", successCount, len(results), float64(successCount)/float64(len(results))*100) + if requiredCount > 0 { + s.T().Logf(" Required: %d/%d passed (%.1f%%)", requiredSuccessCount, requiredCount, float64(requiredSuccessCount)/float64(requiredCount)*100) + } + + if len(failedRequiredScenarios) > 0 { + s.Require().Fail("Required scenario(s) failed", "The following required scenarios failed: %v", failedRequiredScenarios) + } + })) +} + +// runChunkingScenario runs a single chunking test scenario +func (s *IbcEurekaSolanaGMPTestSuite) runChunkingScenario( + ctx context.Context, + simd *cosmos.CosmosChain, + cosmosUser ibc.Wallet, + gmpCounterProgramID solanago.PublicKey, + scenario ChunkingTestScenario, +) ChunkingTestResult { + result := ChunkingTestResult{ + Scenario: scenario, + Success: false, + } + + var cosmosGMPTxHash []byte + + // Send GMP call + if !s.Run("Send GMP Call", func() { + sendErr := func() error { + // Create test payload + payload := make([]byte, scenario.PayloadSize) + for i := range payload { + payload[i] = byte(i % 256) + } + + counterAppStatePDA, _ := solana.GmpCounterApp.CounterAppStatePDA(gmpCounterProgramID) + + // Generate additional dummy accounts + additionalAccounts := make([]*solanatypes.SolanaAccountMeta, scenario.AccountsCount) + for i := 0; i < scenario.AccountsCount; i++ { + dummyAccount := solanago.NewWallet().PublicKey() + additionalAccounts[i] = &solanatypes.SolanaAccountMeta{ + Pubkey: dummyAccount.Bytes(), + IsSigner: false, + IsWritable: false, + } + } + + // Create Borsh-encoded instruction data + buf := new(bytes.Buffer) + enc := borsh.NewBorshEncoder(buf) + + // Encode data as Vec (length prefix + bytes) + err := enc.Encode(payload) + if err != nil { + return fmt.Errorf("failed to encode payload: %w", err) + } + + instructionData := append(gmp_counter_app.Instruction_ProcessTestPayload[:], buf.Bytes()...) + + payerPosition := uint32(1) + solanaInstruction := &solanatypes.GMPSolanaPayload{ + Data: instructionData, + Accounts: append([]*solanatypes.SolanaAccountMeta{ + {Pubkey: counterAppStatePDA.Bytes(), IsSigner: false, IsWritable: false}, + {Pubkey: solanago.SystemProgramID.Bytes(), IsSigner: false, IsWritable: false}, + }, additionalAccounts...), + PayerPosition: &payerPosition, + } + + marshaledPayload, err := proto.Marshal(solanaInstruction) + if err != nil { + return fmt.Errorf("failed to marshal protobuf: %w", err) + } + + timeout := uint64(time.Now().Add(3 * time.Hour).Unix()) + + resp, err := s.BroadcastMessages(ctx, simd, cosmosUser, 2_000_000, &gmptypes.MsgSendCall{ + SourceClient: CosmosClientID, + Sender: cosmosUser.FormattedAddress(), + Receiver: gmpCounterProgramID.String(), + Salt: []byte{}, + Payload: marshaledPayload, + TimeoutTimestamp: timeout, + Memo: fmt.Sprintf("chunking test %s - %d bytes, %d accounts", + scenario.Name, scenario.PayloadSize, scenario.AccountsCount), + Encoding: testvalues.Ics27ProtobufEncoding, + }) + if err != nil { + return fmt.Errorf("failed to broadcast message: %w", err) + } + + cosmosGMPTxHash, err = hex.DecodeString(resp.TxHash) + if err != nil { + return fmt.Errorf("failed to decode tx hash: %w", err) + } + + s.T().Logf("GMP packet sent: %s", resp.TxHash) + return nil + }() + + if sendErr != nil { + result.Error = fmt.Sprintf("send failed: %v", sendErr) + } + }) { + return result + } + + var solanaRelayTxSig solanago.Signature + + // Relay and execute + if !s.Run("Relay to Solana", func() { + relayErr := func() error { + updateResp, err := s.RelayerClient.UpdateClient(context.Background(), &relayertypes.UpdateClientRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + DstClientId: SolanaClientID, + }) + if err != nil { + return fmt.Errorf("update client failed: %w", err) + } + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), updateResp, s.SolanaRelayer) + + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: simd.Config().ChainID, + DstChain: testvalues.SolanaChainID, + SourceTxIds: [][]byte{cosmosGMPTxHash}, + SrcClientId: CosmosClientID, + DstClientId: SolanaClientID, + }) + if err != nil { + return fmt.Errorf("relay by tx failed: %w", err) + } + + solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) + if err != nil { + return fmt.Errorf("submit chunked relay packets failed: %w", err) + } + + s.T().Logf("✓ Packet successfully processed on Solana: %s", solanaRelayTxSig) + + err = s.SolanaChain.WaitForTxStatus(solanaRelayTxSig, rpc.ConfirmationStatusConfirmed) + if err != nil { + return fmt.Errorf("failed to wait for transaction confirmation: %w", err) + } + + s.T().Logf("✓ Transaction confirmed on Solana") + return nil + }() + + if relayErr != nil { + result.Error = fmt.Sprintf("relay failed: %v", relayErr) + } + }) { + return result + } + + // Relay acknowledgement back to Cosmos + if !s.Run("Relay ACK to Cosmos", func() { + ackErr := func() error { + resp, err := s.RelayerClient.RelayByTx(context.Background(), &relayertypes.RelayByTxRequest{ + SrcChain: testvalues.SolanaChainID, + DstChain: simd.Config().ChainID, + SourceTxIds: [][]byte{[]byte(solanaRelayTxSig.String())}, + SrcClientId: SolanaClientID, + DstClientId: CosmosClientID, + }) + if err != nil { + return fmt.Errorf("relay ack failed: %w", err) + } + if resp.Tx == nil { + return fmt.Errorf("relay should return transaction body") + } + + _, err = s.BroadcastSdkTxBody(ctx, simd, cosmosUser, CosmosDefaultGasLimit, resp.Tx) + if err != nil { + return fmt.Errorf("broadcast ack failed: %w", err) + } + + s.T().Logf("✓ Acknowledgement relayed back to Cosmos") + return nil + }() + + if ackErr != nil { + result.Error = fmt.Sprintf("ack relay failed: %v", ackErr) + } + }) { + return result + } + + // Success! + result.Success = true + return result +} diff --git a/e2e/interchaintestv8/solana_test.go b/e2e/interchaintestv8/solana_test.go index 6171acd4f..579fdd769 100644 --- a/e2e/interchaintestv8/solana_test.go +++ b/e2e/interchaintestv8/solana_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + dummy_ibc_app "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/dummyibcapp" gmp_counter_app "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/gmpcounter" malicious_caller "github.com/cosmos/solidity-ibc-eureka/e2e/interchaintestv8/solana/go-anchor/maliciouscaller" bin "github.com/gagliardetto/binary" @@ -27,7 +28,7 @@ import ( channeltypesv2 "github.com/cosmos/ibc-go/v10/modules/core/04-channel/v2/types" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" - dummy_ibc_app "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/dummyibcapp" + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" ics07_tendermint "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics07tendermint" ics26_router "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics26router" ics27_gmp "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics27gmp" @@ -59,7 +60,7 @@ const ( type IbcEurekaSolanaTestSuite struct { e2esuite.TestSuite - SolanaUser *solanago.Wallet + SolanaRelayer *solanago.Wallet RelayerClient relayertypes.RelayerServiceClient ICS27GMPProgramID solanago.PublicKey @@ -108,7 +109,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { s.Require().True(s.Run("Deploy IBC core contracts", func() { solanaUser := solanago.NewWallet() - s.T().Logf("Created SolanaUser wallet: %s", solanaUser.PublicKey()) + s.T().Logf("Created SolanaRelayer wallet: %s", solanaUser.PublicKey()) fundWallet := func(name string, pubkey solanago.PublicKey, amount uint64) e2esuite.ParallelTask { return e2esuite.ParallelTask{ @@ -129,11 +130,11 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { // Fund single deployer with sufficient funds for all program deployments const deployerFunding = 100 * testvalues.InitialSolBalance err := e2esuite.RunParallelTasks( - fundWallet("SolanaUser", solanaUser.PublicKey(), testvalues.InitialSolBalance), + fundWallet("SolanaRelayer", solanaUser.PublicKey(), testvalues.InitialSolBalance), fundWallet("Deployer", solana.DeployerPubkey, deployerFunding), ) s.Require().NoError(err, "Failed to fund wallets") - s.SolanaUser = solanaUser + s.SolanaRelayer = solanaUser s.T().Log("All wallets funded successfully") })) @@ -141,7 +142,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { // Deploy ALL programs in parallel using single deployer s.T().Log("Deploying Solana programs in parallel...") - const keypairDir = "e2e/interchaintestv8/solana/keypairs" + const keypairDir = "solana-keypairs/localnet" const deployerPath = keypairDir + "/deployer_wallet.json" deployProgram := func(displayName, programName string) e2esuite.ParallelTaskWithResult[solanago.PublicKey] { @@ -160,6 +161,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { } deployResults, err := e2esuite.RunParallelTasksWithResults( + deployProgram("Deploy Access Manager", "access_manager"), deployProgram("Deploy ICS07 Tendermint", "ics07_tendermint"), deployProgram("Deploy ICS26 Router", "ics26_router"), deployProgram("Deploy ICS27 GMP", "ics27_gmp"), @@ -169,6 +171,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { ) s.Require().NoError(err, "Program deployment failed") + access_manager.ProgramID = deployResults["Deploy Access Manager"] ics07_tendermint.ProgramID = deployResults["Deploy ICS07 Tendermint"] ics26_router.ProgramID = deployResults["Deploy ICS26 Router"] s.ICS27GMPProgramID = deployResults["Deploy ICS27 GMP"] @@ -184,21 +187,69 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { })) })) + s.Require().True(s.Run("Initialize Access Control", func() { + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + initInstruction, err := access_manager.NewInitializeInstruction( + s.SolanaRelayer.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), + solanago.SystemProgramID, + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), initInstruction) + s.Require().NoError(err) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetryAndTimeout(ctx, tx, rpc.CommitmentConfirmed, 30, s.SolanaRelayer) + s.Require().NoError(err) + s.T().Log("Access control initialized") + })) + + s.Require().True(s.Run("Grant RELAYER_ROLE and ID_CUSTOMIZER_ROLE to SolanaRelayer", func() { + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + const RELAYER_ROLE = uint64(1) + const ID_CUSTOMIZER_ROLE = uint64(6) + + grantRelayerRoleInstruction, err := access_manager.NewGrantRoleInstruction( + RELAYER_ROLE, + s.SolanaRelayer.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err) + + grantIdCustomizerRoleInstruction, err := access_manager.NewGrantRoleInstruction( + ID_CUSTOMIZER_ROLE, + s.SolanaRelayer.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), grantRelayerRoleInstruction, grantIdCustomizerRoleInstruction) + s.Require().NoError(err) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetryAndTimeout(ctx, tx, rpc.CommitmentConfirmed, 30, s.SolanaRelayer) + s.Require().NoError(err) + s.T().Log("Granted RELAYER_ROLE and ID_CUSTOMIZER_ROLE to SolanaRelayer") + })) + s.Require().True(s.Run("Initialize ICS26 Router", func() { routerStateAccount, _ := solana.Ics26Router.RouterStatePDA(ics26_router.ProgramID) - initInstruction, err := ics26_router.NewInitializeInstruction(s.SolanaUser.PublicKey(), routerStateAccount, s.SolanaUser.PublicKey(), solanago.SystemProgramID) + initInstruction, err := ics26_router.NewInitializeInstruction(access_manager.ProgramID, routerStateAccount, s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, solanago.SysVarInstructionsPubkey) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), initInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetryAndTimeout(ctx, tx, rpc.CommitmentConfirmed, 30, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetryAndTimeout(ctx, tx, rpc.CommitmentConfirmed, 30, s.SolanaRelayer) s.Require().NoError(err) })) s.Require().True(s.Run("Create Address Lookup Table", func() { simd := s.CosmosChains[0] cosmosChainID := simd.Config().ChainID - altAddress := s.SolanaChain.CreateIBCAddressLookupTable(ctx, s.T(), s.Require(), s.SolanaUser, cosmosChainID, GMPPortID, SolanaClientID) + altAddress := s.SolanaChain.CreateIBCAddressLookupTable(ctx, s.T(), s.Require(), s.SolanaRelayer, cosmosChainID, GMPPortID, SolanaClientID) s.SolanaAltAddress = altAddress.String() s.T().Logf("Created Address Lookup Table: %s", s.SolanaAltAddress) })) @@ -221,7 +272,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { ICS07ProgramID: ics07_tendermint.ProgramID.String(), ICS26RouterProgramID: ics26_router.ProgramID.String(), CosmosSignerAddress: s.CosmosUsers[0].FormattedAddress(), - SolanaFeePayer: s.SolanaUser.PublicKey().String(), + SolanaFeePayer: s.SolanaRelayer.PublicKey().String(), SolanaAltAddress: s.SolanaAltAddress, MockWasmClient: s.UseMockWasmClient, } @@ -295,7 +346,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { return fmt.Errorf("failed to decode tx: %w", err) } - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, unsignedSolanaTx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, unsignedSolanaTx, rpc.CommitmentConfirmed, s.SolanaRelayer) if err != nil { return fmt.Errorf("failed to broadcast tx: %w", err) } @@ -358,6 +409,7 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { Add("Add Client to Router on Solana", func() error { s.T().Log("Adding client to Router on Solana...") routerStateAccount, _ := solana.Ics26Router.RouterStatePDA(ics26_router.ProgramID) + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) clientAccount, _ := solana.Ics26Router.ClientPDA(ics26_router.ProgramID, []byte(SolanaClientID)) clientSequenceAccount, _ := solana.Ics26Router.ClientSequencePDA(ics26_router.ProgramID, []byte(SolanaClientID)) @@ -369,25 +421,27 @@ func (s *IbcEurekaSolanaTestSuite) SetupSuite(ctx context.Context) { addClientInstruction, err := ics26_router.NewAddClientInstruction( SolanaClientID, counterpartyInfo, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), routerStateAccount, + accessControlAccount, clientAccount, clientSequenceAccount, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), ics07_tendermint.ProgramID, solanago.SystemProgramID, + solanago.SysVarInstructionsPubkey, ) if err != nil { return fmt.Errorf("failed to create add client instruction: %w", err) } - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), addClientInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), addClientInstruction) if err != nil { return fmt.Errorf("failed to create transaction: %w", err) } // Use confirmed commitment - relayer reads Solana state with confirmed commitment - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) if err != nil { return fmt.Errorf("failed to broadcast tx: %w", err) } @@ -462,41 +516,44 @@ func (s *IbcEurekaSolanaTestSuite) setupDummyApp(ctx context.Context) { appStateAccount, _ := solana.DummyIbcApp.AppStateTransferPDA(s.DummyAppProgramID) initInstruction, err := dummy_ibc_app.NewInitializeInstruction( - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), appStateAccount, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), initInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), initInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Dummy app initialized at: %s", s.DummyAppProgramID) })) s.Require().True(s.Run("Register Dummy App with Router", func() { routerStateAccount, _ := solana.Ics26Router.RouterStatePDA(ics26_router.ProgramID) + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) ibcAppAccount, _ := solana.Ics26Router.IbcAppPDA(ics26_router.ProgramID, []byte(transfertypes.PortID)) registerInstruction, err := ics26_router.NewAddIbcAppInstruction( transfertypes.PortID, routerStateAccount, + accessControlAccount, ibcAppAccount, s.DummyAppProgramID, - s.SolanaUser.PublicKey(), - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), + s.SolanaRelayer.PublicKey(), solanago.SystemProgramID, + solanago.SysVarInstructionsPubkey, ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), registerInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), registerInstruction) s.Require().NoError(err) - _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Dummy app registered with router on transfer port") })) @@ -516,7 +573,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { var cosmosPacketRelayTxHash []byte s.Require().True(s.Run("Send ICS20 transfer using send_packet", func() { - initialBalance := s.SolanaUser.PublicKey() + initialBalance := s.SolanaRelayer.PublicKey() balanceResp, err := s.SolanaChain.RPCClient.GetBalance(ctx, initialBalance, "confirmed") s.Require().NoError(err) initialLamports := balanceResp.Value @@ -529,7 +586,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { transferData := transfertypes.NewFungibleTokenPacketData( SolDenom, // denom fmt.Sprintf("%d", TestTransferAmount), // amount as string - s.SolanaUser.PublicKey().String(), // sender + s.SolanaRelayer.PublicKey().String(), // sender receiver, // receiver "Test via send_packet", // memo ) @@ -571,7 +628,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { sendPacketInstruction, err := dummy_ibc_app.NewSendPacketInstruction( packetMsg, appState, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), routerState, ibcApp, clientSequence, @@ -583,15 +640,15 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { ) s.Require().NoError(err) - tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaUser.PublicKey(), sendPacketInstruction) + tx, err := s.SolanaChain.NewTransactionFromInstructions(s.SolanaRelayer.PublicKey(), sendPacketInstruction) s.Require().NoError(err) - solanaTxSig, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + solanaTxSig, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("send_packet transaction: %s", solanaTxSig) s.T().Logf("Sent ICS20 transfer packet with %d bytes of data", len(packetData)) - finalBalance, err := s.SolanaChain.RPCClient.GetBalance(ctx, s.SolanaUser.PublicKey(), "confirmed") + finalBalance, err := s.SolanaChain.RPCClient.GetBalance(ctx, s.SolanaRelayer.PublicKey(), "confirmed") s.Require().NoError(err) s.T().Logf("Final SOL balance: %d lamports (change: %d lamports for fees)", finalBalance.Value, initialLamports-finalBalance.Value) s.T().Logf("Note: send_packet sends IBC transfer data without local escrow - tokens should be minted on destination") @@ -670,7 +727,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaRelayer) s.Require().NoError(err, "Failed to submit chunked update client transactions") })) @@ -685,7 +742,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendPacket() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.SolanaChain.VerifyPacketCommitmentDeleted(ctx, s.T(), s.Require(), SolanaClientID, 1) @@ -706,7 +763,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { var solanaTxSig solanago.Signature var cosmosRelayTxHash []byte s.Require().True(s.Run("Send SOL transfer from Solana", func() { - initialBalance := s.SolanaUser.PublicKey() + initialBalance := s.SolanaRelayer.PublicKey() balanceResp, err := s.SolanaChain.RPCClient.GetBalance(ctx, initialBalance, "confirmed") s.Require().NoError(err) initialLamports := balanceResp.Value @@ -759,7 +816,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { sendTransferInstruction, err := dummy_ibc_app.NewSendTransferInstruction( transferMsg, appState, - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), escrow, escrowState, routerState, @@ -776,17 +833,17 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { computeBudgetInstruction := solana.NewComputeBudgetInstruction(400000) tx, err := s.SolanaChain.NewTransactionFromInstructions( - s.SolanaUser.PublicKey(), + s.SolanaRelayer.PublicKey(), computeBudgetInstruction, sendTransferInstruction, ) s.Require().NoError(err) - solanaTxSig, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + solanaTxSig, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Transfer transaction sent: %s", solanaTxSig) - finalLamports, balanceChanged := s.SolanaChain.WaitForBalanceChange(ctx, s.SolanaUser.PublicKey(), initialLamports) + finalLamports, balanceChanged := s.SolanaChain.WaitForBalanceChange(ctx, s.SolanaRelayer.PublicKey(), initialLamports) s.Require().True(balanceChanged, "Balance should change after transfer") s.T().Logf("Final SOL balance: %d lamports", finalLamports) @@ -860,7 +917,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { s.Require().NoError(err, "Relayer failed to generate update txs") s.Require().NotEmpty(resp.Tx, "Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaRelayer) s.Require().NoError(err, "Failed to submit chunked update client transactions") })) @@ -875,7 +932,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_SolanaToCosmosTransfer_SendTransfer() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + _, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.SolanaChain.VerifyPacketCommitmentDeleted(ctx, s.T(), s.Require(), SolanaClientID, 1) @@ -899,7 +956,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { s.Require().True(s.Run("Send ICS20 transfer from Cosmos to Solana", func() { cosmosUserWallet := s.CosmosUsers[0] cosmosUserAddress := cosmosUserWallet.FormattedAddress() - solanaUserAddress := s.SolanaUser.PublicKey().String() + solanaUserAddress := s.SolanaRelayer.PublicKey().String() transferCoin := sdk.NewCoin(simd.Config().Denom, sdkmath.NewInt(TestTransferAmount)) var initialBalance int64 @@ -977,7 +1034,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { s.Require().NoError(err, "Relayer Update Client failed") s.Require().NotEmpty(resp.Tx, "Relayer Update client should return transaction") - s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaUser) + s.SolanaChain.SubmitChunkedUpdateClient(ctx, s.T(), s.Require(), resp, s.SolanaRelayer) s.Require().NoError(err, "Failed to submit chunked update client transactions") })) @@ -992,7 +1049,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CosmosToSolanaTransfer() { s.Require().NoError(err) s.Require().NotEmpty(resp.Tx, "Relay should return transaction") - solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaUser) + solanaRelayTxSig, err = s.SolanaChain.SubmitChunkedRelayPackets(ctx, s.T(), resp, s.SolanaRelayer) s.Require().NoError(err) s.SolanaChain.VerifyPacketCommitmentDeleted(ctx, s.T(), s.Require(), SolanaClientID, 1) @@ -1073,7 +1130,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { testClientID := SolanaClientID testSequence := uint64(99999) - relayer := s.SolanaUser.PublicKey() + relayer := s.SolanaRelayer.PublicKey() payloadData0 := []byte("payload chunk 0 data for testing orphaned chunks cleanup") payloadData1 := []byte("payload chunk 1 data for testing orphaned chunks cleanup") @@ -1160,7 +1217,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { tx0, err := s.SolanaChain.NewTransactionFromInstructions(relayer, uploadPayload0Instruction) s.Require().NoError(err) - sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaUser) + sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded payload chunk 0: %s", sig0) @@ -1183,7 +1240,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { tx1, err := s.SolanaChain.NewTransactionFromInstructions(relayer, uploadPayload1Instruction) s.Require().NoError(err) - sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaUser) + sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded payload chunk 1: %s", sig1) })) @@ -1208,7 +1265,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { tx0, err := s.SolanaChain.NewTransactionFromInstructions(relayer, uploadProof0Instruction) s.Require().NoError(err) - sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaUser) + sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded proof chunk 0: %s", sig0) @@ -1232,7 +1289,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { tx1, err := s.SolanaChain.NewTransactionFromInstructions(relayer, uploadProof1Instruction) s.Require().NoError(err) - sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaUser) + sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded proof chunk 1: %s", sig1) })) @@ -1302,7 +1359,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedChunks() { tx, err := s.SolanaChain.NewTransactionFromInstructions(relayer, cleanupInstruction) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Cleanup chunks transaction: %s", sig) })) @@ -1353,7 +1410,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedTendermintHeaderChunks() simd := s.CosmosChains[0] cosmosChainID := simd.Config().ChainID testHeight := uint64(99999) - submitter := s.SolanaUser.PublicKey() + submitter := s.SolanaRelayer.PublicKey() clientStatePDA, _, err := solanago.FindProgramAddress( [][]byte{ @@ -1422,7 +1479,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedTendermintHeaderChunks() tx0, err := s.SolanaChain.NewTransactionFromInstructions(submitter, chunk0Instruction) s.Require().NoError(err) - sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaUser) + sig0, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx0, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded header chunk 0: %s", sig0) @@ -1445,7 +1502,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedTendermintHeaderChunks() tx1, err := s.SolanaChain.NewTransactionFromInstructions(submitter, chunk1Instruction) s.Require().NoError(err) - sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaUser) + sig1, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx1, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Uploaded header chunk 1: %s", sig1) })) @@ -1489,7 +1546,7 @@ func (s *IbcEurekaSolanaTestSuite) Test_CleanupOrphanedTendermintHeaderChunks() tx, err := s.SolanaChain.NewTransactionFromInstructions(submitter, cleanupInstruction) s.Require().NoError(err) - sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaUser) + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) s.Require().NoError(err) s.T().Logf("Cleanup header chunks transaction: %s", sig) })) diff --git a/e2e/interchaintestv8/solana_upgrade_test.go b/e2e/interchaintestv8/solana_upgrade_test.go new file mode 100644 index 000000000..2b0f258b6 --- /dev/null +++ b/e2e/interchaintestv8/solana_upgrade_test.go @@ -0,0 +1,402 @@ +package main + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + + solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" + ics26_router "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/ics26router" + + "github.com/srdtrk/solidity-ibc-eureka/e2e/v8/solana" +) + +const ( + keypairDir = "solana-keypairs/localnet" + deployerPath = keypairDir + "/deployer_wallet.json" + programSoFile = "programs/solana/target/deploy/ics26_router.so" +) + +// IbcEurekaSolanaUpgradeTestSuite tests program upgradability via AccessManager +type IbcEurekaSolanaUpgradeTestSuite struct { + IbcEurekaSolanaTestSuite + + UpgraderWallet *solanago.Wallet +} + +func TestWithIbcEurekaSolanaUpgradeTestSuite(t *testing.T) { + s := &IbcEurekaSolanaUpgradeTestSuite{} + suite.Run(t, s) +} + +// Test_ProgramUpgrade_Via_AccessManager demonstrates the complete upgrade flow with role-based access control. +// +// BACKGROUND: +// Solana's BPF Loader Upgradeable uses a two-account system: +// - Program Account: Executable with fixed address (what users call) +// - ProgramData Account: Contains bytecode and upgrade authority metadata +// +// The upgrade authority controls who can upgrade the program. By setting it to an AccessManager-controlled +// PDA, we enable role-based upgrades where only accounts with UPGRADER_ROLE can upgrade. +// +// TEST FLOW: +// 1. Grant UPGRADER_ROLE to a test wallet +// 2. Derive required PDAs (program data account, upgrade authority PDA) +// 3. Transfer program upgrade authority from deployer to AccessManager's PDA (one-time setup) +// 4. Write new program bytecode to a buffer account +// 5. Transfer buffer authority to match program upgrade authority (security requirement) +// 6. Call AccessManager.upgrade_program() with UPGRADER_ROLE +// - AccessManager checks role membership +// - AccessManager calls BPFLoaderUpgradeable.upgrade via invoke_signed with PDA signature +// - BPF Loader verifies authorities match and replaces bytecode +// +// 7. Verify unauthorized accounts cannot upgrade (negative test) +// +// SECURITY MODEL: +// - Role-based access: Only UPGRADER_ROLE can trigger upgrades (AccessManager enforcement) +// - Authority matching: Buffer authority must equal program upgrade authority (BPF Loader enforcement) +// - CPI protection: Upgrade cannot be called via CPI (instructions sysvar check) +// - PDA verification: Upgrade authority PDA seeds are validated (Anchor constraints) +func (s *IbcEurekaSolanaUpgradeTestSuite) Test_ProgramUpgrade_Via_AccessManager() { + ctx := context.Background() + + s.UseMockWasmClient = true + s.SetupSuite(ctx) + + s.Require().True(s.Run("Setup: Create upgrader wallet", func() { + var err error + s.UpgraderWallet, err = s.SolanaChain.CreateAndFundWallet() + s.Require().NoError(err, "failed to create and fund upgrader wallet") + })) + + s.Require().True(s.Run("Setup: Grant UPGRADER_ROLE to upgrader wallet", func() { + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + const UPGRADER_ROLE = uint64(8) + + grantUpgraderRoleInstruction, err := access_manager.NewGrantRoleInstruction( + UPGRADER_ROLE, + s.UpgraderWallet.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err, "failed to build grant UPGRADER_ROLE instruction") + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaRelayer.PublicKey(), + grantUpgraderRoleInstruction, + ) + s.Require().NoError(err, "failed to create grant role transaction") + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) + s.Require().NoError(err, "failed to grant UPGRADER_ROLE") + })) + + targetProgramID := ics26_router.ProgramID + + var programDataAccount solanago.PublicKey + var upgradeAuthorityPDA solanago.PublicKey + + s.Require().True(s.Run("Derive upgrade authority and program data accounts", func() { + var err error + + programDataAccount, err = solana.GetProgramDataAddress(targetProgramID) + s.Require().NoError(err, "failed to derive program data address") + + upgradeAuthorityPDA, _ = solana.AccessManager.UpgradeAuthorityPDA( + access_manager.ProgramID, + targetProgramID.Bytes(), + ) + })) + + s.Require().True(s.Run("Transfer program upgrade authority to AccessManager", func() { + err := solana.SetUpgradeAuthority( + ctx, + targetProgramID, + upgradeAuthorityPDA, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to transfer program upgrade authority to AccessManager") + })) + + var bufferAccount solanago.PublicKey + + s.Require().True(s.Run("Write new program binary to buffer and transfer authority", func() { + var err error + + // For this test, we use the same binary to focus on the upgrade mechanism + // and access control, which is the primary goal of this test suite. + bufferAccount, err = solana.WriteProgramBuffer( + ctx, + programSoFile, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to write program buffer") + s.Require().NotEqual(solanago.PublicKey{}, bufferAccount, "buffer account should not be empty") + + // Transfer buffer authority to match program upgrade authority (security requirement) + err = solana.SetBufferAuthority( + ctx, + bufferAccount, + upgradeAuthorityPDA, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to transfer buffer authority to upgrade authority PDA") + })) + + s.Require().True(s.Run("Upgrade program via AccessManager with UPGRADER_ROLE", func() { + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + + upgradeProgramInstruction, err := access_manager.NewUpgradeProgramInstruction( + targetProgramID, + accessControlAccount, + targetProgramID, + programDataAccount, + bufferAccount, + upgradeAuthorityPDA, + s.UpgraderWallet.PublicKey(), + s.UpgraderWallet.PublicKey(), + solanago.SysVarInstructionsPubkey, + solanago.BPFLoaderUpgradeableProgramID, + solanago.SysVarRentPubkey, + solanago.SysVarClockPubkey, + ) + s.Require().NoError(err, "failed to build upgrade program instruction") + + computeBudgetIx := solana.NewComputeBudgetInstruction(400_000) + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.UpgraderWallet.PublicKey(), + computeBudgetIx, + upgradeProgramInstruction, + ) + s.Require().NoError(err, "failed to create upgrade transaction") + + sig, err := s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.UpgraderWallet) + s.Require().NoError(err, "program upgrade should succeed with UPGRADER_ROLE") + s.Require().NotEqual(solanago.Signature{}, sig, "upgrade signature should not be empty") + })) + + s.Require().True(s.Run("Unauthorized account cannot upgrade program", func() { + unauthorizedWallet, err := s.SolanaChain.CreateAndFundWallet() + s.Require().NoError(err, "failed to create unauthorized wallet") + + const keypairDir = "solana-keypairs/localnet" + const deployerPath = keypairDir + "/deployer_wallet.json" + const programSoFile = "programs/solana/target/deploy/ics26_router.so" + + unauthorizedBuffer, err := solana.WriteProgramBuffer( + ctx, + programSoFile, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to write buffer for unauthorized test") + + err = solana.SetBufferAuthority( + ctx, + unauthorizedBuffer, + upgradeAuthorityPDA, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to transfer buffer authority for unauthorized test") + + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + + upgradeProgramInstruction, err := access_manager.NewUpgradeProgramInstruction( + targetProgramID, + accessControlAccount, + targetProgramID, + programDataAccount, + unauthorizedBuffer, + upgradeAuthorityPDA, + unauthorizedWallet.PublicKey(), + unauthorizedWallet.PublicKey(), + solanago.SysVarInstructionsPubkey, + solanago.BPFLoaderUpgradeableProgramID, + solanago.SysVarRentPubkey, + solanago.SysVarClockPubkey, + ) + s.Require().NoError(err, "failed to build upgrade instruction for unauthorized test") + + computeBudgetIx := solana.NewComputeBudgetInstruction(400_000) + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + unauthorizedWallet.PublicKey(), + computeBudgetIx, + upgradeProgramInstruction, + ) + s.Require().NoError(err, "failed to create unauthorized upgrade transaction") + + // Use SignAndBroadcastTxWithOpts for immediate failure without retry (this is a negative test) + _, err = s.SolanaChain.SignAndBroadcastTxWithOpts(ctx, tx, rpc.ConfirmationStatusConfirmed, unauthorizedWallet) + s.Require().Error(err, "upgrade should fail without UPGRADER_ROLE") + s.Require().Contains(err.Error(), "Custom", "should be Unauthorized error") + })) + + s.Require().True(s.Run("Cannot bypass AccessManager and upgrade directly", func() { + // This test verifies that after transferring upgrade authority to the AccessManager PDA, + // the old authority (deployer) cannot bypass AccessManager by calling BPF Loader directly. + + const keypairDir = "solana-keypairs/localnet" + const deployerPath = keypairDir + "/deployer_wallet.json" + const programSoFile = "programs/solana/target/deploy/ics26_router.so" + + // Create a buffer with deployer as authority + bypassBuffer, err := solana.WriteProgramBuffer( + ctx, + programSoFile, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err, "failed to write buffer for bypass test") + + // Attempt to upgrade directly using BPF Loader (bypassing AccessManager) + // This should fail because the program's upgrade authority is now the AccessManager PDA, + // not the deployer wallet + err = solana.UpgradeProgramDirect( + ctx, + targetProgramID, + bypassBuffer, + deployerPath, + s.SolanaChain.RPCURL, + ) + // The direct upgrade should fail because the program's upgrade authority is now the AccessManager PDA + s.Require().Error(err, "direct upgrade should fail - authority is now AccessManager PDA") + s.Require().Contains(err.Error(), "does not match authority", "should fail with authority mismatch") + })) +} + +// Test_RevokeUpgraderRole demonstrates revoking upgrade permissions +func (s *IbcEurekaSolanaUpgradeTestSuite) Test_RevokeUpgraderRole() { + ctx := context.Background() + + // Enable mock WASM client to avoid relayer unimplemented panic + s.UseMockWasmClient = true + + s.SetupSuite(ctx) + + var upgraderWallet *solanago.Wallet + + s.Require().True(s.Run("Setup: Create and grant UPGRADER_ROLE", func() { + var err error + upgraderWallet, err = s.SolanaChain.CreateAndFundWallet() + s.Require().NoError(err) + + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + const UPGRADER_ROLE = uint64(8) + + grantInstruction, err := access_manager.NewGrantRoleInstruction( + UPGRADER_ROLE, + upgraderWallet.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaRelayer.PublicKey(), + grantInstruction, + ) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) + s.Require().NoError(err) + })) + + s.Require().True(s.Run("Revoke UPGRADER_ROLE", func() { + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + const UPGRADER_ROLE = uint64(8) + + revokeInstruction, err := access_manager.NewRevokeRoleInstruction( + UPGRADER_ROLE, + upgraderWallet.PublicKey(), + accessControlAccount, + s.SolanaRelayer.PublicKey(), // Admin revokes + solanago.SysVarInstructionsPubkey, + ) + s.Require().NoError(err) + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + s.SolanaRelayer.PublicKey(), + revokeInstruction, + ) + s.Require().NoError(err) + + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, s.SolanaRelayer) + s.Require().NoError(err, "failed to revoke UPGRADER_ROLE") + })) + + s.Require().True(s.Run("Verify revoked account cannot upgrade", func() { + const keypairDir = "solana-keypairs/localnet" + const deployerPath = keypairDir + "/deployer_wallet.json" + const programSoFile = "programs/solana/target/deploy/ics26_router.so" + + accessControlAccount, _ := solana.AccessManager.AccessManagerPDA(access_manager.ProgramID) + targetProgramID := ics26_router.ProgramID + programDataAccount, err := solana.GetProgramDataAddress(targetProgramID) + s.Require().NoError(err) + + upgradeAuthorityPDA, _ := solana.AccessManager.UpgradeAuthorityPDA( + access_manager.ProgramID, + targetProgramID.Bytes(), + ) + + // Write buffer + buffer, err := solana.WriteProgramBuffer( + ctx, + programSoFile, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err) + + // Transfer buffer authority to upgrade authority PDA + err = solana.SetBufferAuthority( + ctx, + buffer, + upgradeAuthorityPDA, + deployerPath, + s.SolanaChain.RPCURL, + ) + s.Require().NoError(err) + + upgradeInstruction, err := access_manager.NewUpgradeProgramInstruction( + targetProgramID, + accessControlAccount, + targetProgramID, + programDataAccount, + buffer, + upgradeAuthorityPDA, + upgraderWallet.PublicKey(), + upgraderWallet.PublicKey(), // Revoked account + solanago.SysVarInstructionsPubkey, + solanago.BPFLoaderUpgradeableProgramID, + solanago.SysVarRentPubkey, + solanago.SysVarClockPubkey, + ) + s.Require().NoError(err) + + computeBudgetIx := solana.NewComputeBudgetInstruction(400_000) + + tx, err := s.SolanaChain.NewTransactionFromInstructions( + upgraderWallet.PublicKey(), + computeBudgetIx, + upgradeInstruction, + ) + s.Require().NoError(err) + + // Should fail after role revocation + _, err = s.SolanaChain.SignAndBroadcastTxWithRetry(ctx, tx, rpc.CommitmentConfirmed, upgraderWallet) + s.Require().Error(err, "upgrade should fail after role revocation") + })) +} diff --git a/flake.nix b/flake.nix index 1400dab3d..b3f6adc4d 100644 --- a/flake.nix +++ b/flake.nix @@ -27,8 +27,50 @@ rust = pkgs.rust-bin.stable.latest.default.override { extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ]; }; + + # Override Anchor to v0.32.1 to fix a bug where `anchor keys sync --provider.cluster` + # was not respecting the cluster flag and updated all cluster sections instead of + # just the specified one. This was fixed in v0.32.0 (PR #3761). + # We use v0.32.1 for the latest bug fixes and improvements. + anchor = pkgs.rustPlatform.buildRustPackage rec { + pname = "anchor"; + version = "0.32.1"; + + src = pkgs.fetchFromGitHub { + owner = "solana-foundation"; + repo = "anchor"; + tag = "v${version}"; + hash = "sha256-oyCe8STDciRtdhOWgJrT+k50HhUWL2LSG8m4Ewnu2dc="; + fetchSubmodules = true; + }; + + cargoHash = "sha256-XrVvhJ1lFLBA+DwWgTV34jufrcjszpbCgXpF+TUoEvo="; + + nativeBuildInputs = with pkgs; [ perl pkg-config ]; + + buildInputs = with pkgs; [ openssl ] + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.apple-sdk_15 ]; + + checkFlags = [ + # the following test cases try to access network, skip them + "--skip=tests::test_check_and_get_full_commit_when_full_commit" + "--skip=tests::test_check_and_get_full_commit_when_partial_commit" + "--skip=tests::test_get_anchor_version_from_commit" + ]; + + meta = with pkgs.lib; { + description = "Solana Sealevel Framework"; + homepage = "https://github.com/solana-foundation/anchor"; + changelog = "https://github.com/solana-foundation/anchor/blob/${src.rev}/CHANGELOG.md"; + license = licenses.asl20; + maintainers = with maintainers; [ ]; + mainProgram = "anchor"; + }; + }; + solana-agave = pkgs.callPackage ./nix/agave.nix { - inherit (pkgs) rust-bin anchor; + inherit (pkgs) rust-bin; + inherit anchor; }; anchor-go = pkgs.callPackage ./nix/anchor-go.nix {}; in @@ -106,6 +148,8 @@ echo " anchor-nix build - Build with Solana toolchain + generate IDL with nightly" echo " anchor-nix test - Build and run anchor client tests" echo " anchor-nix unit-test [options] - Build program then run cargo test" + echo " anchor-nix keys [subcommand] - Manage program keypairs (sync, list, etc.)" + echo " anchor-nix deploy [options] - Deploy programs to specified cluster" echo "" # WORKAROUND: Fix Darwin SDK conflicts (Oct 2025) diff --git a/justfile b/justfile index db542274b..0f392bcaf 100644 --- a/justfile +++ b/justfile @@ -1,9 +1,18 @@ set dotenv-load +# Detect which anchor command is available +anchor_cmd := `command -v anchor-nix >/dev/null 2>&1 && echo "anchor-nix" || echo "anchor"` + +# Helper function to run solana-ibc CLI tool +solana_ibc := ''' + (cd tools/solana-ibc && go run . "$@") +''' + # Default task lists all available tasks default: just --list + # Build the contracts using `forge build` [group('build')] build-contracts: clean-foundry @@ -19,6 +28,11 @@ build-relayer: build-operator: cargo build --bin operator --release --locked +# Build the solana-ibc CLI tool using `go build` +[group('build')] +build-solana-ibc: + cd tools/solana-ibc && go build -o ../../bin/solana-ibc . + # Build riscv elf files using `~/.sp1/bin/cargo-prove` [group('build')] build-sp1-programs: @@ -28,16 +42,660 @@ build-sp1-programs: cd programs/sp1-programs && ~/.sp1/bin/cargo-prove prove build -p sp1-ics07-tendermint-uc-and-membership --locked cd programs/sp1-programs && ~/.sp1/bin/cargo-prove prove build -p sp1-ics07-tendermint-misbehaviour --locked -# Build the Solana Anchor program -[group('build')] -build-solana: - @echo "Building Solana Anchor program..." - if command -v anchor-nix >/dev/null 2>&1; then \ - echo "🦀 Using anchor-nix"; \ - (cd programs/solana && anchor-nix build); \ - else \ - echo "🦀 Using anchor"; \ - (cd programs/solana && anchor build); \ +# Sync Solana program keypairs and update declare_id! macros +# Usage: just sync-solana-keys [cluster] +# Example: just sync-solana-keys devnet +[group('solana')] +sync-solana-keys cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + echo "Syncing Solana program keys for cluster: {{cluster}}" + + # Validate cluster directory exists + if [ ! -d "solana-keypairs/{{cluster}}" ]; then + echo "❌ Cluster directory not found: solana-keypairs/{{cluster}}" + echo " Available clusters: $(just list-clusters)" + exit 1 + fi + + # Check for keypairs (skip for localnet as it's tracked in git) + if [ "{{cluster}}" != "localnet" ] && [ ! -f "solana-keypairs/{{cluster}}/ics26_router-keypair.json" ]; then + echo "❌ No keypairs found for cluster: {{cluster}}" + echo " Generate them first with: just generate-solana-keypairs {{cluster}}" + exit 1 + fi + + # Add [programs.{{cluster}}] section to Anchor.toml if missing + ANCHOR_TOML="programs/solana/Anchor.toml" + if ! grep -q "^\[programs\.{{cluster}}\]" "$ANCHOR_TOML"; then + echo "📝 Adding [programs.{{cluster}}] section to Anchor.toml..." + + # Build section content without trailing newline + SECTION="[programs.{{cluster}}]" + for keypair in solana-keypairs/{{cluster}}/*-keypair.json; do + [ -f "$keypair" ] || continue + PROGRAM_NAME=$(basename "$keypair" -keypair.json) + PROGRAM_ID=$(solana-keygen pubkey "$keypair") + SECTION+=$'\n'"$PROGRAM_NAME = \"$PROGRAM_ID\"" + done + + # Insert after [programs.localnet] section with proper spacing + awk -v section="$SECTION" ' + /^\[programs\.localnet\]/ { in_localnet=1; print; next } + in_localnet && /^[[:space:]]*$/ { next } + in_localnet && /^\[/ { + print "" + print section + print "" + in_localnet=0 + } + { print } + END { + if (in_localnet) { + print "" + print section + print "" + } + } + ' "$ANCHOR_TOML" > "$ANCHOR_TOML.tmp" && mv "$ANCHOR_TOML.tmp" "$ANCHOR_TOML" + fi + + # Copy keypairs to target/deploy + mkdir -p programs/solana/target/deploy + cp -f solana-keypairs/{{cluster}}/*-keypair.json programs/solana/target/deploy/ 2>/dev/null || true + + # Sync declare_id! macros + echo "🦀 Using {{anchor_cmd}}" + (cd programs/solana && {{anchor_cmd}} keys sync --provider.cluster {{cluster}}) + + echo "✅ Keys synced for cluster: {{cluster}}" + +# Build Solana Anchor programs +# Usage: just build-solana [program] +# Example: just build-solana (builds all programs) +# Example: just build-solana ics26-router (builds only ics26-router) +[group('solana')] +build-solana program="": + #!/usr/bin/env bash + set -euo pipefail + + if [ -z "{{program}}" ]; then + echo "Building all programs..." + echo "🦀 Using {{anchor_cmd}}" + (cd programs/solana && {{anchor_cmd}} build) + echo "✅ Build complete" + else + echo "Building program: {{program}}" + PROGRAM_DIR="programs/solana/programs/{{program}}" + + if [ ! -d "$PROGRAM_DIR" ]; then + echo "❌ Program directory not found: $PROGRAM_DIR" + echo " Available programs:" + ls -1 programs/solana/programs/ | grep -v "^\." || true + exit 1 + fi + + echo "🦀 Using {{anchor_cmd}}" + + # Build specific program and generate its IDL + (cd programs/solana && {{anchor_cmd}} build -- -p "{{program}}") + + echo "✅ Build complete for {{program}}" + fi + +# Deploy Solana Anchor programs to a specific cluster (default: localnet) +# Usage: just deploy-solana [cluster] [max-len-multiplier] +# Example: just deploy-solana devnet +# Example: just deploy-solana localnet 3 +# Use max-len-multiplier to reserve more space for future upgrades. +[group('solana')] +deploy-solana cluster="localnet" max_len_multiplier="2": + #!/usr/bin/env bash + set -euo pipefail + + echo "Deploying programs to {{cluster}}..." + echo "🚀 Using {{anchor_cmd}}" + echo "📏 Max length multiplier: {{max_len_multiplier}}x" + + # Get deployer wallet for localnet + if [ "{{cluster}}" = "localnet" ]; then + DEPLOYER_WALLET="$(pwd)/solana-keypairs/{{cluster}}/deployer_wallet.json" + WALLET_ARG="--provider.wallet $DEPLOYER_WALLET" + else + WALLET_ARG="" + fi + + # Deploy each program individually with its own max-len + cd programs/solana + for program_so in target/deploy/*.so; do + if [ -f "$program_so" ]; then + PROGRAM_NAME=$(basename "$program_so" .so) + PROGRAM_SIZE=$(stat -f%z "$program_so" 2>/dev/null || stat -c%s "$program_so" 2>/dev/null) + MAX_LEN=$((PROGRAM_SIZE * {{max_len_multiplier}})) + + echo "📦 Deploying $PROGRAM_NAME (size: $PROGRAM_SIZE bytes, max-len: $MAX_LEN bytes)" + {{anchor_cmd}} deploy --provider.cluster {{cluster}} $WALLET_ARG -p "$PROGRAM_NAME" -- --max-len $MAX_LEN + fi + done + cd ../.. + + echo "✅ Deployment complete for cluster: {{cluster}}" + +# Full deployment: deploy programs, initialize access manager, set upgrade authorities, and grant upgrader role +# Usage: just deploy-solana-full [cluster] [max-len-multiplier] +# Example: just deploy-solana-full localnet +# Example: just deploy-solana-full devnet 3 +[group('solana')] +deploy-solana-full cluster="localnet" max_len_multiplier="2": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + echo "🚀 Starting full Solana deployment for cluster: {{cluster}}" + echo "" + + # Step 1: Deploy all programs + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "📦 Step 1/4: Deploying Solana programs" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + just deploy-solana {{cluster}} {{max_len_multiplier}} + echo "" + + # Step 2: Initialize AccessManager + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "🔑 Step 2/4: Initializing AccessManager" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + just initialize-access-manager "" {{cluster}} + echo "" + + # Step 3: Set upgrade authority to AccessManager for all programs (except access_manager itself) + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "🔐 Step 3/4: Setting upgrade authorities to AccessManager" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + PROGRAMS=( + "ics26_router" + "ics27_gmp" + "ics07_tendermint" + "dummy_ibc_app" + "mock_ibc_app" + "gmp_counter_app" + "mock_light_client" + ) + + for program in "${PROGRAMS[@]}"; do + echo "Setting upgrade authority for $program..." + just set-upgrade-authority "$program" {{cluster}} || { + echo "⚠️ Warning: Failed to set upgrade authority for $program" + } + done + echo "" + + # Step 4: Grant UPGRADER_ROLE (8) to deployer + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "👤 Step 4/4: Granting UPGRADER_ROLE (8) to deployer" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + just grant-solana-role 8 "" {{cluster}} + echo "" + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "✅ Full deployment complete for cluster: {{cluster}}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Summary:" + echo " ✓ All programs deployed" + echo " ✓ AccessManager initialized" + echo " ✓ Upgrade authorities set to AccessManager PDAs" + echo " ✓ UPGRADER_ROLE granted to deployer" + echo "" + echo "Next steps:" + echo " • Use 'just upgrade-solana-program {{cluster}}' to upgrade programs" + echo " • Use 'just grant-solana-role {{cluster}}' to grant additional roles" + +# Initialize AccessManager with an admin +# Usage: just initialize-access-manager [admin-pubkey] [cluster] +# Example: just initialize-access-manager +# Example: just initialize-access-manager 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +[group('solana')] +initialize-access-manager admin_pubkey="" cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + # Get cluster URL from Anchor.toml + CLUSTER_URL=$(just get-cluster-url {{cluster}}) + + # Get payer keypair (deployer for localnet, or specified payer) + PAYER_KEYPAIR="solana-keypairs/{{cluster}}/deployer_wallet.json" + + # Default admin to deployer if not specified + if [ -z "{{admin_pubkey}}" ]; then + ADMIN_PUBKEY=$(solana-keygen pubkey "$PAYER_KEYPAIR") + else + ADMIN_PUBKEY="{{admin_pubkey}}" + fi + + if [ ! -f "$PAYER_KEYPAIR" ]; then + echo "❌ Payer keypair not found: $PAYER_KEYPAIR" + exit 1 + fi + + # Get access manager program ID + ACCESS_MANAGER_ID=$(solana-keygen pubkey "solana-keypairs/{{cluster}}/access_manager-keypair.json") + + echo "Initializing AccessManager" + echo "Cluster: {{cluster}} ($CLUSTER_URL)" + echo "Admin: $ADMIN_PUBKEY" + echo "Access Manager: $ACCESS_MANAGER_ID" + echo "" + + # Convert keypair to absolute path + ABS_PAYER_KEYPAIR=$(cd "$(dirname "$PAYER_KEYPAIR")" && pwd)/$(basename "$PAYER_KEYPAIR") + + solana_ibc() { + {{solana_ibc}} + } + solana_ibc access-manager initialize "$CLUSTER_URL" "$ABS_PAYER_KEYPAIR" "$ADMIN_PUBKEY" "$ACCESS_MANAGER_ID" + +# Grant a role to an account +# Usage: just grant-solana-role [account-pubkey] [cluster] +# Example: just grant-solana-role 8 +# Example: just grant-solana-role 8 Abc123... +[group('solana')] +grant-solana-role role_id account="" cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + # Get cluster URL from Anchor.toml + CLUSTER_URL=$(just get-cluster-url {{cluster}}) + + # Get admin keypair (deployer for localnet, or specified admin) + ADMIN_KEYPAIR="solana-keypairs/{{cluster}}/deployer_wallet.json" + + if [ ! -f "$ADMIN_KEYPAIR" ]; then + echo "❌ Admin keypair not found: $ADMIN_KEYPAIR" + exit 1 + fi + + # Default account to deployer if not specified + if [ -z "{{account}}" ]; then + ACCOUNT_PUBKEY=$(solana-keygen pubkey "$ADMIN_KEYPAIR") + else + ACCOUNT_PUBKEY="{{account}}" + fi + + # Get access manager program ID + ACCESS_MANAGER_ID=$(solana-keygen pubkey "solana-keypairs/{{cluster}}/access_manager-keypair.json") + + echo "Granting role {{role_id}} to $ACCOUNT_PUBKEY" + echo "Cluster: {{cluster}} ($CLUSTER_URL)" + echo "Access Manager: $ACCESS_MANAGER_ID" + echo "" + + # Convert keypair to absolute path + ABS_ADMIN_KEYPAIR=$(cd "$(dirname "$ADMIN_KEYPAIR")" && pwd)/$(basename "$ADMIN_KEYPAIR") + + solana_ibc() { + {{solana_ibc}} + } + solana_ibc access-manager grant "$CLUSTER_URL" "$ABS_ADMIN_KEYPAIR" "{{role_id}}" "$ACCOUNT_PUBKEY" "$ACCESS_MANAGER_ID" + +# Set program upgrade authority to AccessManager PDA +# Usage: just set-upgrade-authority [cluster] [current-authority-keypair] +# Example: just set-upgrade-authority ics26_router +# Example: just set-upgrade-authority ics26_router localnet ~/.config/solana/id.json +[group('solana')] +set-upgrade-authority program_name cluster="localnet" current_authority="": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + # Get cluster URL from Anchor.toml + CLUSTER_URL=$(just get-cluster-url {{cluster}}) + + # Get program ID from keypair + PROGRAM_KEYPAIR="solana-keypairs/{{cluster}}/{{program_name}}-keypair.json" + if [ ! -f "$PROGRAM_KEYPAIR" ]; then + echo "❌ Program keypair not found: $PROGRAM_KEYPAIR" + exit 1 + fi + PROGRAM_ID=$(solana-keygen pubkey "$PROGRAM_KEYPAIR") + + # Determine current authority keypair (default to deployer) + if [ -n "{{current_authority}}" ]; then + CURRENT_AUTHORITY="{{current_authority}}" + else + CURRENT_AUTHORITY="solana-keypairs/{{cluster}}/deployer_wallet.json" + if [ ! -f "$CURRENT_AUTHORITY" ]; then + echo "❌ Default authority keypair not found: $CURRENT_AUTHORITY" + echo " Please specify current authority explicitly" + exit 1 + fi + fi + + ACCESS_MANAGER_ID=$(solana-keygen pubkey "solana-keypairs/{{cluster}}/access_manager-keypair.json") + + # Derive upgrade authority PDA + solana_ibc() { + {{solana_ibc}} + } + UPGRADE_AUTH_PDA=$(solana_ibc upgrade derive-pda "$ACCESS_MANAGER_ID" "$PROGRAM_ID") + + echo "Setting upgrade authority for program {{program_name}}" + echo "Program ID: $PROGRAM_ID" + echo "Cluster: {{cluster}} ($CLUSTER_URL)" + echo "Current authority: $CURRENT_AUTHORITY" + echo "New authority (PDA): $UPGRADE_AUTH_PDA" + echo "" + + solana program set-upgrade-authority "$PROGRAM_ID" \ + --upgrade-authority "$CURRENT_AUTHORITY" \ + --new-upgrade-authority "$UPGRADE_AUTH_PDA" \ + --skip-new-upgrade-authority-signer-check \ + --url "$CLUSTER_URL" + + echo "" + echo "✅ Upgrade authority set to AccessManager PDA" + echo " Program: {{program_name}}" + echo " Program ID: $PROGRAM_ID" + echo " Upgrade Authority: $UPGRADE_AUTH_PDA" + + echo "✅ Upgrade authority set successfully!" + +# Revoke a role from an account +# Usage: just revoke-solana-role [account-pubkey] [cluster] +# Example: just revoke-solana-role 8 +# Example: just revoke-solana-role 8 Abc123... +[group('solana')] +revoke-solana-role role_id account="" cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + # Get cluster URL from Anchor.toml + CLUSTER_URL=$(just get-cluster-url {{cluster}}) + + # Get admin keypair + ADMIN_KEYPAIR="solana-keypairs/{{cluster}}/deployer_wallet.json" + + if [ ! -f "$ADMIN_KEYPAIR" ]; then + echo "❌ Admin keypair not found: $ADMIN_KEYPAIR" + exit 1 + fi + + # Default account to deployer if not specified + if [ -z "{{account}}" ]; then + ACCOUNT_PUBKEY=$(solana-keygen pubkey "$ADMIN_KEYPAIR") + else + ACCOUNT_PUBKEY="{{account}}" + fi + + # Get access manager program ID + ACCESS_MANAGER_ID=$(solana-keygen pubkey "solana-keypairs/{{cluster}}/access_manager-keypair.json") + + echo "Revoking role {{role_id}} from $ACCOUNT_PUBKEY" + echo "Cluster: {{cluster}} ($CLUSTER_URL)" + echo "Access Manager: $ACCESS_MANAGER_ID" + echo "" + + # Convert keypair to absolute path + ABS_ADMIN_KEYPAIR=$(cd "$(dirname "$ADMIN_KEYPAIR")" && pwd)/$(basename "$ADMIN_KEYPAIR") + + solana_ibc() { + {{solana_ibc}} + } + solana_ibc access-manager revoke "$CLUSTER_URL" "$ABS_ADMIN_KEYPAIR" "{{role_id}}" "$ACCOUNT_PUBKEY" "$ACCESS_MANAGER_ID" + +# Prepare program upgrade buffer (steps 1-3 of upgrade process) +# Usage: just prepare-solana-upgrade [cluster] +# Example: just prepare-solana-upgrade ics26_router GzT...xyz +# Note: Requires deployer wallet funded with SOL +[group('solana')] +prepare-solana-upgrade program upgrade_authority_pda cluster="localnet": build-solana + #!/usr/bin/env bash + set -euo pipefail + + PROGRAM_SO="programs/solana/target/deploy/{{program}}.so" + DEPLOYER_KEYPAIR="solana-keypairs/{{cluster}}/deployer_wallet.json" + CLUSTER_URL="{{cluster}}" + + # Validate inputs + if [ ! -f "$PROGRAM_SO" ]; then + echo "❌ Program binary not found: $PROGRAM_SO" + echo " Run 'just build-solana' first" + exit 1 + fi + + if [ ! -f "$DEPLOYER_KEYPAIR" ]; then + echo "❌ Deployer keypair not found: $DEPLOYER_KEYPAIR" + exit 1 + fi + + echo "📦 Preparing upgrade for program: {{program}}" + echo " Cluster: {{cluster}}" + echo " Upgrade Authority PDA: {{upgrade_authority_pda}}" + echo "" + + # Step 2: Write program to buffer + echo "Step 1/2: Writing program to buffer..." + BUFFER_OUTPUT=$(solana program write-buffer "$PROGRAM_SO" \ + --url "$CLUSTER_URL" \ + --keypair "$DEPLOYER_KEYPAIR" \ + --use-rpc 2>&1) + + # Extract buffer address from output + BUFFER_ADDRESS=$(echo "$BUFFER_OUTPUT" | grep -oP 'Buffer: \K[A-Za-z0-9]+' || true) + + if [ -z "$BUFFER_ADDRESS" ]; then + echo "❌ Failed to create buffer" + echo "$BUFFER_OUTPUT" + exit 1 + fi + + echo "✅ Buffer created: $BUFFER_ADDRESS" + echo "" + + # Step 3: Set buffer authority + echo "Step 2/2: Setting buffer authority to upgrade authority PDA..." + solana program set-buffer-authority "$BUFFER_ADDRESS" \ + --new-buffer-authority "{{upgrade_authority_pda}}" \ + --buffer-authority "$DEPLOYER_KEYPAIR" \ + --keypair "$DEPLOYER_KEYPAIR" \ + --url "$CLUSTER_URL" + + echo "" + echo "✅ Upgrade buffer prepared successfully!" + echo "" + echo "📋 Next steps:" + echo " Buffer Address: $BUFFER_ADDRESS" + echo " Upgrade Authority: {{upgrade_authority_pda}}" + echo "" + echo " To complete the upgrade, call AccessManager.upgrade_program with:" + echo " - buffer: $BUFFER_ADDRESS" + echo " - target_program: " + echo " - authority: " + echo "" + echo " See e2e/interchaintestv8/solana_upgrade_test.go for implementation example" + +# Execute complete program upgrade (prepare buffer + execute upgrade instruction) +# Usage: just upgrade-solana-program [cluster] [upgrader-keypair] +# Example: just upgrade-solana-program ics26_router +# Example: just upgrade-solana-program ics26_router devnet solana-keypairs/devnet/upgrader.json +# Note: Requires UPGRADER_ROLE granted to the upgrader keypair (defaults to deployer) +[group('solana')] +upgrade-solana-program program cluster="localnet" upgrader_keypair="": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + PROGRAM_SO="programs/solana/target/deploy/{{program}}.so" + DEPLOYER_KEYPAIR="solana-keypairs/{{cluster}}/deployer_wallet.json" + + # Default upgrader to deployer if not specified + if [ -n "{{upgrader_keypair}}" ]; then + UPGRADER_KEYPAIR="{{upgrader_keypair}}" + else + UPGRADER_KEYPAIR="$DEPLOYER_KEYPAIR" + fi + + # Get cluster URL from Anchor.toml + CLUSTER_URL=$(just get-cluster-url {{cluster}}) + + # Get program ID from keypair + PROGRAM_KEYPAIR="solana-keypairs/{{cluster}}/{{program}}-keypair.json" + if [ ! -f "$PROGRAM_KEYPAIR" ]; then + echo "❌ Program keypair not found: $PROGRAM_KEYPAIR" + exit 1 + fi + PROGRAM_ID=$(solana-keygen pubkey "$PROGRAM_KEYPAIR") + + # Get access-manager program ID + ACCESS_MANAGER_ID=$(solana-keygen pubkey "solana-keypairs/{{cluster}}/access_manager-keypair.json") + + # Derive upgrade authority PDA + solana_ibc() { + {{solana_ibc}} + } + UPGRADE_AUTH_PDA=$(solana_ibc upgrade derive-pda "$ACCESS_MANAGER_ID" "$PROGRAM_ID" 2>/dev/null || echo "") + + if [ -z "$UPGRADE_AUTH_PDA" ]; then + echo "❌ Failed to derive upgrade authority PDA" + exit 1 + fi + + echo "🔧 Starting program upgrade for: {{program}}" + echo " Program ID: $PROGRAM_ID" + echo " Cluster: {{cluster}} ($CLUSTER_URL)" + echo " Access Manager: $ACCESS_MANAGER_ID" + echo " Upgrade Authority PDA: $UPGRADE_AUTH_PDA" + echo "" + + # Step 1-3: Prepare buffer + echo "Step 1-3: Preparing upgrade buffer..." + + if ! BUFFER_OUTPUT=$(solana program write-buffer "$PROGRAM_SO" \ + --url "$CLUSTER_URL" \ + --keypair "$DEPLOYER_KEYPAIR" \ + --use-rpc 2>&1); then + echo "❌ Failed to create buffer" + echo "$BUFFER_OUTPUT" + exit 1 + fi + + BUFFER_ADDRESS=$(echo "$BUFFER_OUTPUT" | grep -oP 'Buffer: \K[A-Za-z0-9]+' || true) + + if [ -z "$BUFFER_ADDRESS" ]; then + echo "❌ Failed to extract buffer address from output" + echo "$BUFFER_OUTPUT" + exit 1 + fi + + echo "✅ Buffer created: $BUFFER_ADDRESS" + + # Set buffer authority to match upgrade authority PDA + echo "Setting buffer authority to: $UPGRADE_AUTH_PDA" + solana program set-buffer-authority "$BUFFER_ADDRESS" \ + --new-buffer-authority "$UPGRADE_AUTH_PDA" \ + --buffer-authority "$DEPLOYER_KEYPAIR" \ + --keypair "$DEPLOYER_KEYPAIR" \ + --url "$CLUSTER_URL" + + echo "✅ Buffer authority set to upgrade authority PDA" + + # Derive program data address + PROGRAM_DATA_ADDR=$(solana program show "$PROGRAM_ID" --url "$CLUSTER_URL" | grep "ProgramData Address" | awk '{print $3}') + + if [ -z "$PROGRAM_DATA_ADDR" ]; then + echo "❌ Failed to get program data address" + exit 1 + fi + + echo "Program Data Address: $PROGRAM_DATA_ADDR" + echo "" + echo "Step 4: Executing upgrade instruction..." + echo "(This requires UPGRADER_ROLE on the upgrader keypair)" + echo "" + + # Convert upgrader keypair to absolute path + ABS_UPGRADER_KEYPAIR=$(cd "$(dirname "$UPGRADER_KEYPAIR")" && pwd)/$(basename "$UPGRADER_KEYPAIR") + + # Call the upgrade tool + solana_ibc upgrade program \ + "$CLUSTER_URL" \ + "$ABS_UPGRADER_KEYPAIR" \ + "$PROGRAM_ID" \ + "$BUFFER_ADDRESS" \ + "$ACCESS_MANAGER_ID" \ + "$PROGRAM_DATA_ADDR" + + echo "" + echo "✅ Program upgrade complete!" + +# Generate Solana keypairs for a specific cluster +# Usage: just generate-solana-keypairs +# Example: just generate-solana-keypairs devnet +[group('solana')] +generate-solana-keypairs cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + set -euo pipefail + + # Cannot regenerate localnet keypairs (tracked in git for E2E tests) + if [ "{{cluster}}" = "localnet" ]; then + echo "❌ Cannot generate keypairs for localnet - they are tracked in git" + echo " Localnet keypairs are used for E2E tests and should not be regenerated" + exit 1 + fi + + echo "Generating keypairs for cluster: {{cluster}}" + mkdir -p solana-keypairs/{{cluster}} + solana-keygen new --no-bip39-passphrase --force --outfile solana-keypairs/{{cluster}}/ics26_router-keypair.json + solana-keygen new --no-bip39-passphrase --force --outfile solana-keypairs/{{cluster}}/ics07_tendermint-keypair.json + solana-keygen new --no-bip39-passphrase --force --outfile solana-keypairs/{{cluster}}/ics27_gmp-keypair.json + solana-keygen new --no-bip39-passphrase --force --outfile solana-keypairs/{{cluster}}/access_manager-keypair.json + echo "" + echo "✅ Keypairs generated in solana-keypairs/{{cluster}}/" + echo "⚠️ IMPORTANT: Backup these keypairs securely! They are NOT tracked in git." + echo "" + echo "📋 Program IDs for {{cluster}}:" + for keypair in solana-keypairs/{{cluster}}/*-keypair.json; do + printf " %-35s %s\n" "$(basename $keypair):" "$(solana-keygen pubkey $keypair)" + done + echo "" + echo "Next step: just build-solana" + +# Get cluster URL from Anchor.toml +[group('solana')] +get-cluster-url cluster="localnet": (_validate-cluster cluster) + #!/usr/bin/env bash + awk -F' = ' -v cluster="{{cluster}}" ' + /^\[clusters\]/ { in_clusters=1; next } + in_clusters && /^\[/ { exit } + in_clusters && $1 == cluster { gsub(/"/, "", $2); print $2; exit } + ' programs/solana/Anchor.toml + +# List available clusters from Anchor.toml +[group('solana')] +list-clusters: + #!/usr/bin/env bash + awk ' + /^\[clusters\]/ { in_clusters=1; next } + in_clusters && /^\[/ { exit } + in_clusters && /^[a-z]/ { + split($0, parts, " = ") + print parts[1] + } + ' programs/solana/Anchor.toml | tr '\n' ',' | sed 's/,$//' + +# Validate cluster exists in Anchor.toml (internal helper recipe) +[private] +_validate-cluster cluster: + #!/usr/bin/env bash + if ! awk -F' = ' -v cluster="{{cluster}}" ' + /^\[clusters\]/ { in_clusters=1; next } + in_clusters && /^\[/ { exit } + in_clusters && $1 == cluster { found=1; exit } + END { exit !found } + ' programs/solana/Anchor.toml; then + AVAILABLE=$(just list-clusters) + echo "❌ Unknown cluster: {{cluster}}" >&2 + echo " Available clusters: $AVAILABLE" >&2 + exit 1 fi # Build and optimize the eth wasm light client using a local docker image. Requires `docker` and `gzip` @@ -113,7 +771,7 @@ lint-rust: lint-solana: @echo "Linting the Solana code..." cd programs/solana && cargo fmt --all -- --check - cd programs/solana && cargo clippy --all-targets --all-features -- -D warnings + cd programs/solana && cargo +nightly clippy --all-targets --all-features -- -D warnings # Generate the (non-bytecode) ABI files for the contracts @@ -139,16 +797,20 @@ generate-abi-bytecode: build-contracts # Generate the types for interacting with SVM contracts using 'anchor-go' [group('generate')] -generate-solana-types: build-solana generate-pda +generate-solana-types: generate-pda @echo "Generating SVM types..." + # Core IBC apps rm -rf packages/go-anchor/ics07tendermint anchor-go --idl ./programs/solana/target/idl/ics07_tendermint.json --output packages/go-anchor/ics07tendermint --no-go-mod rm -rf packages/go-anchor/ics26router anchor-go --idl ./programs/solana/target/idl/ics26_router.json --output packages/go-anchor/ics26router --no-go-mod + rm -rf packages/go-anchor/accessmanager + anchor-go --idl ./programs/solana/target/idl/access_manager.json --output packages/go-anchor/accessmanager --no-go-mod rm -rf packages/go-anchor/ics27gmp anchor-go --idl ./programs/solana/target/idl/ics27_gmp.json --output packages/go-anchor/ics27gmp --no-go-mod - rm -rf packages/go-anchor/dummyibcapp - anchor-go --idl ./programs/solana/target/idl/dummy_ibc_app.json --output packages/go-anchor/dummyibcapp --no-go-mod + # Dummy apps for testing + rm -rf e2e/interchaintestv8/solana/go-anchor/dummyibcapp + anchor-go --idl ./programs/solana/target/idl/dummy_ibc_app.json --output e2e/interchaintestv8/solana/go-anchor/dummyibcapp --no-go-mod rm -rf e2e/interchaintestv8/solana/go-anchor/mocklightclient anchor-go --idl ./programs/solana/target/idl/mock_light_client.json --output e2e/interchaintestv8/solana/go-anchor/mocklightclient --no-go-mod rm -rf e2e/interchaintestv8/solana/go-anchor/gmpcounter @@ -320,27 +982,27 @@ test-e2e-solana-gmp testname: @echo "Running {{testname}} test..." just test-e2e TestWithIbcEurekaSolanaGMPTestSuite/{{testname}} +# Run the e2e tests in the IbcEurekaSolanaUpgradeTestSuite. For example, `just test-e2e-solana-upgrade Test_ProgramUpgrade_Via_AccessManager` +[group('test')] +test-e2e-solana-upgrade testname: + @echo "Running {{testname}} test..." + just test-e2e TestWithIbcEurekaSolanaUpgradeTestSuite/{{testname}} + # Run the Solana Anchor e2e tests [group('test')] test-anchor-solana *ARGS: - @echo "Running Solana Client Anchor tests (anchor-nix preferred) ..." - if command -v anchor-nix >/dev/null 2>&1; then \ - echo "🦀 Using anchor-nix"; \ - (cd programs/solana && anchor-nix test {{ARGS}}); \ - else \ - echo "🦀 Using anchor"; \ - (cd programs/solana && anchor test {{ARGS}}); \ - fi + @echo "Running Solana Client Anchor tests..." + @echo "🦀 Using {{anchor_cmd}}" + (cd programs/solana && {{anchor_cmd}} test {{ARGS}}) # Run Solana unit tests (mollusk + litesvm) [group('test')] test-solana *ARGS: @echo "Building and running Solana unit tests..." - if command -v anchor-nix >/dev/null 2>&1; then \ - echo "🦀 Using anchor-nix"; \ + @echo "🦀 Using {{anchor_cmd}}" + @if [ "{{anchor_cmd}}" = "anchor-nix" ]; then \ (cd programs/solana && anchor-nix unit-test {{ARGS}}); \ else \ - echo "🦀 Using anchor"; \ (cd programs/solana && anchor build) && \ echo "✅ Build successful, running cargo tests" && \ (cd programs/solana && cargo test {{ARGS}}); \ diff --git a/nix/agave.nix b/nix/agave.nix index 73d2ee527..4439a2158 100644 --- a/nix/agave.nix +++ b/nix/agave.nix @@ -276,9 +276,65 @@ let setup_solana - if ! "$REAL_ANCHOR" build --no-idl -- --no-rustup-override --skip-tools-install "''${extra_args[@]}"; then - echo "❌ Program build failed" - return 1 + # Parse arguments + local skip_idl=false + local cargo_args=() + local specific_package="" + + for arg in "''${extra_args[@]}"; do + if [[ "$arg" == "--no-idl" ]]; then + skip_idl=true + elif [[ "$arg" != "--" ]]; then + cargo_args+=("$arg") + fi + done + + # Check for -p flag to determine specific package + for ((i=0; i<''${#cargo_args[@]}; i++)); do + if [[ "''${cargo_args[$i]}" == "-p" && $((i+1)) -lt ''${#cargo_args[@]} ]]; then + specific_package="''${cargo_args[$((i+1))]}" + echo "🎯 Building package: $specific_package" + break + fi + done + + # Build the program(s) + if [ -n "$specific_package" ]; then + # Build specific package directly with cargo-build-sbf to avoid building all programs + echo " Building only $specific_package (skipping other programs)..." + + # Find the program directory + local program_dir="" + for dir in programs/*/; do + if [ "$(basename "$dir")" = "$specific_package" ]; then + program_dir="$dir" + break + fi + done + + if [ -z "$program_dir" ]; then + echo "❌ Program directory not found for: $specific_package" + return 1 + fi + + # Build using cargo-build-sbf directly + # Use the same flags as anchor to skip tool installation + if ! cargo build-sbf --manifest-path "''${program_dir}Cargo.toml" --no-rustup-override --skip-tools-install; then + echo "❌ Program build failed" + return 1 + fi + else + # Build all programs using anchor + if ! "$REAL_ANCHOR" build --no-idl -- --no-rustup-override --skip-tools-install; then + echo "❌ Program build failed" + return 1 + fi + fi + + # Skip IDL generation if requested + if [ "$skip_idl" = true ]; then + echo "⏭️ Skipping IDL generation (--no-idl flag)" + return 0 fi if cargo_toml=$(has_idl_build_feature); then @@ -297,6 +353,11 @@ let for program_dir in programs/*/; do program_name=$(basename "$program_dir") + # Skip if building specific package and this isn't it + if [ -n "$specific_package" ] && [ "$program_name" != "$specific_package" ]; then + continue + fi + has_idl_build=false if [ -f "$program_dir/Cargo.toml" ]; then if grep -q "idl-build" "$program_dir/Cargo.toml" 2>/dev/null || true; then @@ -467,6 +528,16 @@ let run_unit_test "$@" ;; + keys) + # Pass through to real anchor for keys command (sync, list, etc.) + "$REAL_ANCHOR" "$@" + ;; + + deploy) + # Pass through to real anchor for deploy command + "$REAL_ANCHOR" "$@" + ;; + *) cat < anyhow::Result { let trust_level = ibc_client .trust_level @@ -158,6 +159,7 @@ pub fn convert_client_state_to_sol( revision_number: latest_height.revision_number, revision_height: latest_height.revision_height, }, + access_manager, }) } diff --git a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs index 54e039213..d7f995f2a 100644 --- a/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs +++ b/packages/relayer/modules/cosmos-to-solana/src/tx_builder.rs @@ -42,7 +42,7 @@ use solana_ibc_types::{ router_instructions, Client, ClientSequence, Commitment, IBCApp, IBCAppState, PayloadChunk, ProofChunk, RouterState, }, - MsgAckPacket, MsgRecvPacket, MsgTimeoutPacket, MsgUploadChunk, + AccessManager, MsgAckPacket, MsgRecvPacket, MsgTimeoutPacket, MsgUploadChunk, }; use tendermint_rpc::{Client as _, HttpClient}; @@ -51,6 +51,7 @@ const MAX_CHUNK_SIZE: usize = 700; /// Parameters for assembling timeout packet accounts struct TimeoutAccountsParams { + access_manager: Pubkey, router_state: Pubkey, ibc_app: Pubkey, packet_commitment: Pubkey, @@ -214,6 +215,33 @@ impl TxBuilder { }) } + /// Resolves the access manager program ID from the router state. + /// + /// Future optimization: Consider caching this value. + fn resolve_access_manager_program_id(&self) -> Result { + let (router_state_pda, _) = RouterState::pda(self.solana_ics26_program_id); + + let account = self + .target_solana_client + .get_account_with_commitment(&router_state_pda, CommitmentConfig::confirmed()) + .map_err(|e| anyhow::anyhow!("Failed to fetch RouterState account: {e}"))? + .value + .ok_or_else(|| anyhow::anyhow!("Router state account not found"))?; + + if account.data.len() < ANCHOR_DISCRIMINATOR_SIZE { + return Err(anyhow::anyhow!( + "Account data too short for RouterState account" + )); + } + + // Deserialize RouterState account using borsh (skip discriminator) + let mut data = &account.data[ANCHOR_DISCRIMINATOR_SIZE..]; + let router_state = solana_ibc_types::RouterState::deserialize(&mut data) + .map_err(|e| anyhow::anyhow!("Failed to deserialize RouterState account: {e}"))?; + + Ok(router_state.access_manager) + } + async fn chain_id(&self) -> Result { Ok(self .src_tm_client @@ -319,9 +347,14 @@ impl TxBuilder { // Derive the app state account for the resolved IBC app let (ibc_app_state, _) = IBCAppState::pda(dest_port, ibc_app_program_id); + // Derive the access manager PDA + let access_manager_program_id = self.resolve_access_manager_program_id()?; + let (access_manager, _) = AccessManager::pda(access_manager_program_id); + // Build base accounts list for recv_packet (matches router program's RecvPacket account structure) let mut accounts = vec![ AccountMeta::new_readonly(router_state, false), + AccountMeta::new_readonly(access_manager, false), AccountMeta::new_readonly(ibc_app, false), AccountMeta::new(client_sequence, false), AccountMeta::new(packet_receipt, false), @@ -459,8 +492,13 @@ impl TxBuilder { let (consensus_state, _) = ConsensusState::pda(client_state, msg.proof.height, self.solana_ics07_program_id); + // Derive access manager PDA + let access_manager_program_id = self.resolve_access_manager_program_id()?; + let (access_manager, _) = solana_ibc_types::AccessManager::pda(access_manager_program_id); + let mut accounts = vec![ AccountMeta::new_readonly(router_state, false), + AccountMeta::new_readonly(access_manager, false), AccountMeta::new_readonly(ibc_app_pda, false), AccountMeta::new(packet_commitment, false), // Will be closed after ack AccountMeta::new_readonly(ibc_app_program, false), @@ -560,7 +598,12 @@ impl TxBuilder { let (consensus_state, _) = ConsensusState::pda(client_state, msg.proof.height, self.solana_ics07_program_id); + // Derive access manager PDA + let access_manager_program_id = self.resolve_access_manager_program_id()?; + let (access_manager, _) = solana_ibc_types::AccessManager::pda(access_manager_program_id); + Ok(Self::assemble_timeout_accounts(TimeoutAccountsParams { + access_manager, router_state, ibc_app, packet_commitment, @@ -580,6 +623,7 @@ impl TxBuilder { fn assemble_timeout_accounts(params: TimeoutAccountsParams) -> Vec { let mut accounts = vec![ AccountMeta::new_readonly(params.router_state, false), + AccountMeta::new_readonly(params.access_manager, false), AccountMeta::new_readonly(params.ibc_app, false), AccountMeta::new(params.packet_commitment, false), AccountMeta::new_readonly(params.ibc_app_program_id, false), @@ -857,12 +901,18 @@ impl TxBuilder { self.solana_ics07_program_id, ); + // Derive access manager PDA + let access_manager_program_id = self.resolve_access_manager_program_id()?; + let (access_manager, _) = AccessManager::pda(access_manager_program_id); + let mut accounts = vec![ AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(access_manager, false), AccountMeta::new_readonly(trusted_consensus_state, false), AccountMeta::new(new_consensus_state, false), AccountMeta::new(self.fee_payer, true), // submitter AccountMeta::new_readonly(solana_sdk::system_program::id(), false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::id(), false), // instructions_sysvar ]; for chunk_index in 0..total_chunks { @@ -1474,7 +1524,8 @@ impl TxBuilder { consensus_state: tm_consensus_state, } = tm_create_client_params(&self.src_tm_client).await?; - let client_state = convert_client_state_to_sol(tm_client_state)?; + let access_manager_program_id = self.resolve_access_manager_program_id()?; + let client_state = convert_client_state_to_sol(tm_client_state, access_manager_program_id)?; let consensus_state = convert_consensus_state(&tm_consensus_state)?; let instruction = self.build_create_client_instruction( diff --git a/programs/solana/Anchor.toml b/programs/solana/Anchor.toml index 57d1d2796..c9655bcde 100644 --- a/programs/solana/Anchor.toml +++ b/programs/solana/Anchor.toml @@ -1,13 +1,25 @@ [toolchain] +anchor_version = "0.32.1" [features] resolution = true skip-lint = false +[clusters] +localnet = "http://localhost:8899" +# devnet = "https://api.devnet.solana.com" +# testnet = "https://api.testnet.solana.com" +# mainnet = "https://api.mainnet-beta.solana.com" + [programs.localnet] -ics07_tendermint = "8wQAC7oWLTxExhR49jYAzXZB39mu7WVVvkWJGgAMMjpV" -ics26_router = "HsCyuYgKgoN9wUPiJyNZvvWg2N1uyZhDjvJfKJFu3jvU" -ics27_gmp = "FHsRDbCxXxv1wWsAFccaPz2BSphGEEcnVNHXCBTP92Am" +access_manager = "4fMih2CidrXPeRx77kj3QcuBZwREYtxEbXjURUgadoe1" +dummy_ibc_app = "5E73beFMq9QZvbwPN5i84psh2WcyJ9PgqF4avBaRDgCC" +gmp_counter_app = "GdEUjpVtKvHKStM3Hph6PnLSUMsJXvcVqugubhtQ5QUD" +ics07_tendermint = "HqPcGpVHxNNFfVatjhG78dFVMwjyZixoKPdZSt3d3TdD" +ics26_router = "FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx" +ics27_gmp = "3W3h4WSE8J9vFzVN8TGFGc9Uchbry3M4MBz4icdSWcFi" +mock_ibc_app = "4Fo5RuY7bEPZNz1FjkM9cUkUVc2BVhdYBjDA8P6Tmox1" +mock_light_client = "CSLS3A9jS7JAD8aUe3LRXMYZ1U8Lvxn9usGygVrA2arZ" [registry] url = "https://api.apr.dev" @@ -18,3 +30,5 @@ wallet = "~/.config/solana/id.json" [test] startup_wait = 5000 +shutdown_wait = 2000 +upgradeable = false diff --git a/programs/solana/Cargo.lock b/programs/solana/Cargo.lock index 81719ef5a..353dd209c 100644 --- a/programs/solana/Cargo.lock +++ b/programs/solana/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "access-manager" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "mollusk-svm", + "solana-ibc-types", + "solana-sdk", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -128,9 +138,9 @@ dependencies = [ [[package]] name = "anchor-attribute-access-control" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f70fd141a4d18adf11253026b32504f885447048c7494faf5fa83b01af9c0cf" +checksum = "7a883ca44ef14b2113615fc6d3a85fefc68b5002034e88db37f7f1f802f88aa9" dependencies = [ "anchor-syn", "proc-macro2", @@ -140,9 +150,9 @@ dependencies = [ [[package]] name = "anchor-attribute-account" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715a261c57c7679581e06f07a74fa2af874ac30f86bd8ea07cca4a7e5388a064" +checksum = "61c4d97763b29030412b4b80715076377edc9cc63bc3c9e667297778384b9fd2" dependencies = [ "anchor-syn", "bs58", @@ -153,9 +163,9 @@ dependencies = [ [[package]] name = "anchor-attribute-constant" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730d6df8ae120321c5c25e0779e61789e4b70dc8297102248902022f286102e4" +checksum = "aae3328bbf9bbd517a51621b1ba6cbec06cbbc25e8cfc7403bddf69bcf088206" dependencies = [ "anchor-syn", "quote", @@ -164,9 +174,9 @@ dependencies = [ [[package]] name = "anchor-attribute-error" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e6e449cc3a37b2880b74dcafb8e5a17b954c0e58e376432d7adc646fb333ef" +checksum = "cf2398a6d9e16df1ee9d7d37d970a8246756de898c8dd16ef6bdbe4da20cf39a" dependencies = [ "anchor-syn", "quote", @@ -175,9 +185,9 @@ dependencies = [ [[package]] name = "anchor-attribute-event" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7710e4c54adf485affcd9be9adec5ef8846d9c71d7f31e16ba86ff9fc1dd49f" +checksum = "f12758f4ec2f0e98d4d56916c6fe95cb23d74b8723dd902c762c5ef46ebe7b65" dependencies = [ "anchor-syn", "proc-macro2", @@ -187,9 +197,9 @@ dependencies = [ [[package]] name = "anchor-attribute-program" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ecfd49b2aeadeb32f35262230db402abed76ce87e27562b34f61318b2ec83c" +checksum = "8c7193b5af2649813584aae6e3569c46fd59616a96af2083c556b13136c3830f" dependencies = [ "anchor-lang-idl", "anchor-syn", @@ -204,9 +214,9 @@ dependencies = [ [[package]] name = "anchor-derive-accounts" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be89d160793a88495af462a7010b3978e48e30a630c91de47ce2c1d3cb7a6149" +checksum = "d332d1a13c0fca1a446de140b656e66110a5e8406977dcb6a41e5d6f323760b0" dependencies = [ "anchor-syn", "quote", @@ -215,9 +225,9 @@ dependencies = [ [[package]] name = "anchor-derive-serde" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc6ee78acb7bfe0c2dd2abc677aaa4789c0281a0c0ef01dbf6fe85e0fd9e6e4" +checksum = "8656e4af182edaeae665fa2d2d7ee81148518b5bd0be9a67f2a381bb17da7d46" dependencies = [ "anchor-syn", "borsh-derive-internal", @@ -228,9 +238,9 @@ dependencies = [ [[package]] name = "anchor-derive-space" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134a01c0703f6fd355a0e472c033f6f3e41fac1ef6e370b20c50f4c8d022cea7" +checksum = "dcff2a083560cd79817db07d89a4de39a2c4b2eaa00c1742cf0df49b25ff2bed" dependencies = [ "proc-macro2", "quote", @@ -239,9 +249,9 @@ dependencies = [ [[package]] name = "anchor-lang" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6bab117055905e930f762c196e08f861f8dfe7241b92cee46677a3b15561a0a" +checksum = "e67d85d5376578f12d840c29ff323190f6eecd65b00a0b5f2b2f232751d049cc" dependencies = [ "anchor-attribute-access-control", "anchor-attribute-account", @@ -257,7 +267,26 @@ dependencies = [ "bincode", "borsh 0.10.4", "bytemuck", - "solana-program", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-define-syscall", + "solana-feature-gate-interface", + "solana-instruction", + "solana-instructions-sysvar", + "solana-invoke", + "solana-loader-v3-interface 3.0.0", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", + "solana-sysvar-id", "thiserror 1.0.69", ] @@ -288,9 +317,9 @@ dependencies = [ [[package]] name = "anchor-spl" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c08cb5d762c0694f74bd02c9a5b04ea53cefc496e2c27b3234acffca5cd076b" +checksum = "3397ab3fc5b198bbfe55d827ff58bd69f2a8d3f9f71c3732c23c2093fec4d3ef" dependencies = [ "anchor-lang", "spl-associated-token-account", @@ -303,9 +332,9 @@ dependencies = [ [[package]] name = "anchor-syn" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc7a6d90cc643df0ed2744862cdf180587d1e5d28936538c18fc8908489ed67" +checksum = "b93b69aa7d099b59378433f6d7e20e1008fc10c69e48b220270e5b3f2ec4c8be" dependencies = [ "anyhow", "bs58", @@ -732,18 +761,18 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -1973,6 +2002,7 @@ dependencies = [ name = "ics07-tendermint" version = "0.1.0" dependencies = [ + "access-manager", "anchor-lang", "bincode", "hex", @@ -2029,15 +2059,21 @@ dependencies = [ name = "ics26-router" version = "0.1.0" dependencies = [ + "access-manager", "anchor-lang", "anchor-spl", "bincode", + "dummy-ibc-app", "hex", "ics25-handler", + "mock-ibc-app", + "mock-light-client", "mollusk-svm", "mollusk-svm-bencher", "sha2 0.10.9", + "solana-ibc-macros", "solana-ibc-types", + "solana-program", "solana-sdk", ] @@ -2045,6 +2081,7 @@ dependencies = [ name = "ics27-gmp" version = "0.1.0" dependencies = [ + "access-manager", "anchor-lang", "anchor-spl", "bincode", @@ -2055,6 +2092,7 @@ dependencies = [ "solana-ibc-macros", "solana-ibc-proto", "solana-ibc-types", + "solana-program", "solana-sdk", ] @@ -3987,6 +4025,7 @@ dependencies = [ "prost", "solana-ibc-constants", "solana-ibc-proto", + "solana-program", ] [[package]] @@ -4034,6 +4073,19 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-invoke" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f5693c6de226b3626658377168b0184e94e8292ff16e3d31d4766e65627565" +dependencies = [ + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-entrypoint", + "solana-stable-layout", +] + [[package]] name = "solana-keccak-hasher" version = "2.2.1" @@ -4085,6 +4137,7 @@ dependencies = [ "solana-instruction", "solana-pubkey", "solana-sdk-ids", + "solana-system-interface", ] [[package]] @@ -5038,9 +5091,9 @@ dependencies = [ [[package]] name = "spl-associated-token-account" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fee7d65013667032d499adc3c895e286197a35a0d3a4643c80e7fd3e9969e3" +checksum = "ae179d4a26b3c7a20c839898e6aed84cb4477adf108a366c95532f058aea041b" dependencies = [ "borsh 1.5.7", "num-derive", @@ -5049,7 +5102,7 @@ dependencies = [ "spl-associated-token-account-client", "spl-token", "spl-token-2022", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] @@ -5100,12 +5153,22 @@ dependencies = [ [[package]] name = "spl-elgamal-registry" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce0f668975d2b0536e8a8fd60e56a05c467f06021dae037f1d0cfed0de2e231d" +checksum = "65edfeed09cd4231e595616aa96022214f9c9d2be02dea62c2b30d5695a6833a" dependencies = [ "bytemuck", - "solana-program", + "solana-account-info", + "solana-cpi", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-system-interface", + "solana-sysvar", "solana-zk-sdk", "spl-pod", "spl-token-confidential-transfer-proof-extraction", @@ -5147,22 +5210,24 @@ dependencies = [ [[package]] name = "spl-program-error" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" +checksum = "9cdebc8b42553070b75aa5106f071fef2eb798c64a7ec63375da4b1f058688c6" dependencies = [ "num-derive", "num-traits", - "solana-program", + "solana-decode-error", + "solana-msg", + "solana-program-error", "spl-program-error-derive", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-program-error-derive" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +checksum = "2a2539e259c66910d78593475540e8072f0b10f0f61d7607bbf7593899ed52d0" dependencies = [ "proc-macro2", "quote", @@ -5172,9 +5237,9 @@ dependencies = [ [[package]] name = "spl-tlv-account-resolution" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" +checksum = "1408e961215688715d5a1063cbdcf982de225c45f99c82b4f7d7e1dd22b998d7" dependencies = [ "bytemuck", "num-derive", @@ -5189,37 +5254,66 @@ dependencies = [ "spl-pod", "spl-program-error", "spl-type-length-value", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-token" -version = "7.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" +checksum = "053067c6a82c705004f91dae058b11b4780407e9ccd6799dc9e7d0fab5f242da" dependencies = [ "arrayref", "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-program", - "thiserror 1.0.69", + "solana-account-info", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", + "solana-sysvar", + "thiserror 2.0.12", ] [[package]] name = "spl-token-2022" -version = "6.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" +checksum = "31f0dfbb079eebaee55e793e92ca5f433744f4b71ee04880bfd6beefba5973e5" dependencies = [ "arrayref", "bytemuck", "num-derive", "num-traits", "num_enum", - "solana-program", + "solana-account-info", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-native-token", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-memory", + "solana-program-option", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "solana-sdk-ids", "solana-security-txt", + "solana-system-interface", + "solana-sysvar", "solana-zk-sdk", "spl-elgamal-registry", "spl-memo", @@ -5232,14 +5326,14 @@ dependencies = [ "spl-token-metadata-interface", "spl-transfer-hook-interface", "spl-type-length-value", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-token-confidential-transfer-ciphertext-arithmetic" -version = "0.2.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170378693c5516090f6d37ae9bad2b9b6125069be68d9acd4865bbe9fc8499fd" +checksum = "cddd52bfc0f1c677b41493dafa3f2dbbb4b47cf0990f08905429e19dc8289b35" dependencies = [ "base64 0.22.1", "bytemuck", @@ -5249,13 +5343,19 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-extraction" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff2d6a445a147c9d6dd77b8301b1e116c8299601794b558eafa409b342faf96" +checksum = "fe2629860ff04c17bafa9ba4bed8850a404ecac81074113e1f840dbd0ebb7bd6" dependencies = [ "bytemuck", + "solana-account-info", "solana-curve25519", - "solana-program", + "solana-instruction", + "solana-instructions-sysvar", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "solana-sdk-ids", "solana-zk-sdk", "spl-pod", "thiserror 2.0.12", @@ -5263,20 +5363,20 @@ dependencies = [ [[package]] name = "spl-token-confidential-transfer-proof-generation" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" +checksum = "fa27b9174bea869a7ebf31e0be6890bce90b1a4288bc2bbf24bd413f80ae3fde" dependencies = [ "curve25519-dalek 4.1.3", "solana-zk-sdk", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-token-group-interface" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" +checksum = "5597b4cd76f85ce7cd206045b7dc22da8c25516573d42d267c8d1fd128db5129" dependencies = [ "bytemuck", "num-derive", @@ -5288,14 +5388,14 @@ dependencies = [ "solana-pubkey", "spl-discriminator", "spl-pod", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-token-metadata-interface" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" +checksum = "304d6e06f0de0c13a621464b1fd5d4b1bebf60d15ca71a44d3839958e0da16ee" dependencies = [ "borsh 1.5.7", "num-derive", @@ -5309,14 +5409,14 @@ dependencies = [ "spl-discriminator", "spl-pod", "spl-type-length-value", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-transfer-hook-interface" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" +checksum = "a7e905b849b6aba63bde8c4badac944ebb6c8e6e14817029cbe1bc16829133bd" dependencies = [ "arrayref", "bytemuck", @@ -5334,14 +5434,14 @@ dependencies = [ "spl-program-error", "spl-tlv-account-resolution", "spl-type-length-value", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] name = "spl-type-length-value" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" +checksum = "d417eb548214fa822d93f84444024b4e57c13ed6719d4dcc68eec24fb481e9f5" dependencies = [ "bytemuck", "num-derive", @@ -5352,7 +5452,7 @@ dependencies = [ "solana-program-error", "spl-discriminator", "spl-pod", - "thiserror 1.0.69", + "thiserror 2.0.12", ] [[package]] diff --git a/programs/solana/Cargo.toml b/programs/solana/Cargo.toml index 42e5b2745..8c38b2ae9 100644 --- a/programs/solana/Cargo.toml +++ b/programs/solana/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "programs/access-manager", "programs/ics07-tendermint", "programs/ics26-router", "programs/ics27-gmp", @@ -66,8 +67,8 @@ keywords = [ ] [workspace.dependencies] -anchor-lang = { version = "0.31.1", default-features = false } -anchor-spl = { version = "0.31.1" } +anchor-lang = { version = "0.32.1", default-features = false } +anchor-spl = { version = "0.32.1" } base64 = "0.22" tendermint-light-client-update-client = { path = "../../packages/tendermint-light-client/update-client", default-features = false } @@ -81,6 +82,7 @@ solana-ibc-proto = { path = "./packages/solana-ibc-proto", default-features = fa ics25-handler = { path = "./packages/ics25-handler", default-features = false } solana-ibc-constants = { path = "./packages/solana-ibc-constants", default-features = false } solana-ibc-macros = { path = "./packages/solana-ibc-macros" } +access-manager = { path = "./programs/access-manager", default-features = false } ics26-router = { path = "./programs/ics26-router", default-features = false } gmp-counter-app = { path = "./programs/gmp-counter-app", default-features = false } diff --git a/programs/solana/README.md b/programs/solana/README.md new file mode 100644 index 000000000..5dbac1506 --- /dev/null +++ b/programs/solana/README.md @@ -0,0 +1,506 @@ +# Solana IBC Programs + +This directory contains the Solana implementation of IBC Eureka protocol, built using the Anchor framework. The programs enable trust-minimized interoperability between Solana and Cosmos SDK chains. + +### Generate Keypairs for a Cluster + +```bash +just generate-solana-keypairs +``` + +Where `` is one of: `localnet`, `devnet`, `testnet`, `mainnet` + +This creates keypairs in `solana-keypairs//`: + +- `access_manager-keypair.json` +- `ics07_tendermint-keypair.json` +- `ics26_router-keypair.json` +- `ics27_gmp-keypair.json` +- `dummy_ibc_app-keypair.json` +- `mock_light_client-keypair.json` +- `deployer_wallet.json` + +### Manual Keypair Generation + +```bash +# Generate a program keypair +solana-keygen new -o solana-keypairs//-keypair.json + +# Display the public key (program ID) +solana-keygen pubkey solana-keypairs//-keypair.json +``` + +**Security Notes:** + +- Keypairs for `devnet`, `testnet`, and `mainnet` are **automatically gitignored** +- Only `localnet` keypairs are committed to the repository +- **Never commit production keypairs to version control** +- Store production keypairs securely + +## Deployment + +### Quick Deployment + +Deploy to devnet with a single command: + +```bash +just deploy-solana devnet +``` + +This automatically: + +1. Builds programs with cluster-specific program IDs +2. Deploys all programs to the target cluster +3. Updates `Anchor.toml` with deployed program IDs + +### Step-by-Step Deployment + +#### 1. Fund Your Deployer Wallet + +```bash +# Devnet (free airdrops) +solana airdrop 5 solana-keypairs/devnet/deployer_wallet.json --url devnet + +# Testnet +solana airdrop 2 solana-keypairs/testnet/deployer_wallet.json --url testnet + +# Mainnet (fund with real SOL) +solana transfer --from --url mainnet +``` + +#### 2. Build Programs + +```bash +just build-solana +``` + +This compiles all programs and generates their IDLs. + +#### 3. Deploy Programs + +```bash +cd programs/solana +anchor deploy --provider.cluster +``` + +Or deploy individual programs: + +```bash +anchor deploy -p access-manager --provider.cluster devnet +``` + +### Local Deployment (Testing) + +For local development and testing: + +```bash +# Terminal 1: Start local validator +solana-test-validator + +# Terminal 2: Configure Solana CLI +solana config set --url localhost + +# Terminal 3: Deploy +just deploy-solana localnet +``` + +### Deployment Options + +#### Deploy Specific Programs + +```bash +anchor deploy -p access-manager --provider.cluster devnet +anchor deploy -p ics26-router --provider.cluster devnet +``` + +#### Upgrade Existing Programs + +```bash +# Build new version +just build-solana + +# Deploy upgrade +anchor upgrade target/deploy/ics26_router.so \ + --program-id \ + --provider.cluster devnet +``` + +**Note:** After initial deployment, program upgrades should use the AccessManager's upgrade mechanism (see [Program Upgradability](#program-upgradability)). + +### Verifying Deployment + +```bash +# Check program account +solana program show --url devnet + +# Verify program executable +solana account --url devnet + +# Check deployer balance +solana balance solana-keypairs/devnet/deployer_wallet.json --url devnet +``` + +## Access Control & Roles + +The AccessManager provides role-based access control across all IBC programs. + +### Role Definitions + +| Role ID | Name | Purpose | +| ---------- | -------------------- | --------------------------------------- | +| `0` | `ADMIN_ROLE` | Root administrator with all permissions | +| `1` | `RELAYER_ROLE` | Submit IBC packets and client updates | +| `2` | `PAUSER_ROLE` | Pause operations during emergencies | +| `3` | `UNPAUSER_ROLE` | Resume operations after emergency | +| `6` | `ID_CUSTOMIZER_ROLE` | Customize client and connection IDs | +| `8` | `UPGRADER_ROLE` | Upgrade program bytecode | +| `u64::MAX` | `PUBLIC_ROLE` | Anyone (unrestricted access) | + +**Note:** Some role IDs (4, 5, 7) are reserved for future use and Ethereum compatibility but not currently implemented on Solana. + +### Role Management + +#### Initialize AccessManager + +Before granting roles, initialize the AccessManager with an initial admin: + +```bash +just initialize-access-manager +``` + +Example: + +```bash +just initialize-access-manager localnet 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +``` + +This creates the AccessManager PDA and grants ADMIN_ROLE to the specified account. + +#### Grant Role + +Grant a role to an account: + +```bash +just grant-solana-role +``` + +Example (grant UPGRADER_ROLE): + +```bash +just grant-solana-role localnet 8 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +``` + +#### Revoke Role + +Revoke a role from an account: + +```bash +just revoke-solana-role +``` + +Example: + +```bash +just revoke-solana-role localnet 8 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +``` + +### Role Verification + +Programs verify roles before executing sensitive operations: + +```rust +use access_manager::helpers::require_role; + +require_role( + &access_manager_account, + roles::RELAYER_ROLE, + &authority_account, + &instructions_sysvar, + &program_id, +)?; +``` + +## Program Upgradability + +Solana programs deployed via BPF Loader Upgradeable can be upgraded by the upgrade authority. We use AccessManager to manage upgrade permissions via role-based access control. + +### Initial Setup + +After deploying programs, configure upgrade authority management: + +**Step 1: Initialize AccessManager (if not already done)** + +```bash +just initialize-access-manager +``` + +Example: + +```bash +just initialize-access-manager localnet 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +``` + +**Step 2: Grant UPGRADER_ROLE to authorized account** + +```bash +just grant-solana-role 8 +``` + +Example: + +```bash +just grant-solana-role localnet 8 8ntLtUdGwBaXfFPCrNis9MWsKMdEUYyonwuw7NQwhs5z +``` + +**Step 3: Transfer upgrade authority to AccessManager PDA** + +```bash +just set-upgrade-authority [current-authority-keypair] +``` + +Example: + +```bash +just set-upgrade-authority FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx localnet ~/.config/solana/id.json +``` + +**Notes:** + +- The upgrade authority PDA is automatically derived with seeds: `["upgrade_authority", program_id]` +- The third parameter (current authority keypair) is optional - if omitted, the command will tell you which keypair you need +- Use `--skip-new-upgrade-authority-signer-check` is automatically applied since PDAs cannot sign + +### Performing Upgrades + +Once upgrade authority is transferred to AccessManager, upgrades require UPGRADER_ROLE. + +#### Complete Upgrade (Recommended) + +Execute the entire upgrade process (build, buffer, and upgrade instruction) in one command: + +```bash +just upgrade-solana-program +``` + +Example: + +```bash +just upgrade-solana-program ics26_router 4fMih2CidrXPeRx77kj3QcuBZwREYtxEbXjURUgadoe1 devnet solana-keypairs/devnet/upgrader.json +``` + +This command automatically: + +1. Builds the new program version +2. Writes bytecode to a buffer account +3. Sets buffer authority to the upgrade authority PDA +4. Executes the AccessManager.upgrade_program instruction +5. Waits for confirmation + +**Requirements:** + +- Upgrader keypair must have UPGRADER_ROLE +- Deployer wallet must have SOL for buffer creation +- Program must already be deployed with AccessManager as upgrade authority + +#### Prepare Buffer Only + +If you only want to prepare the buffer without executing the upgrade: + +```bash +just prepare-solana-upgrade +``` + +Example: + +```bash +# Derive the upgrade authority PDA first (seeds: ["upgrade_authority", program_id]) +# For ics26-router on devnet: +UPGRADE_AUTH_PDA="" + +just prepare-solana-upgrade ics26_router devnet $UPGRADE_AUTH_PDA +``` + +This command: + +1. Builds the new program version +2. Writes bytecode to a buffer +3. Sets buffer authority to the upgrade authority PDA +4. Outputs the buffer address for manual upgrade execution + +#### Option 2: Manual steps + +**Step 1: Build new program version** + +```bash +just build-solana +``` + +**Step 2: Write new bytecode to a buffer** + +```bash +solana program write-buffer programs/solana/target/deploy/.so \ + --url \ + --keypair solana-keypairs//deployer_wallet.json \ + --use-rpc +``` + +This outputs a buffer address like: `Buffer: ` + +**Step 3: Set buffer authority to match program upgrade authority** + +```bash +solana program set-buffer-authority \ + --new-buffer-authority \ + --buffer-authority solana-keypairs//deployer_wallet.json \ + --keypair solana-keypairs//deployer_wallet.json \ + --url +``` + +#### Final Step: Call AccessManager.upgrade_program + +After preparing the buffer (using either option above), execute the upgrade instruction. + +**Prerequisites:** + +Build the `solana-ibc` CLI tool if not already built: + +```bash +go build -o bin/solana-ibc ./tools/solana-ibc +``` + +**Using `solana-ibc` CLI:** + +```bash +# First, derive the upgrade authority PDA +UPGRADE_AUTH_PDA=$(bin/solana-ibc upgrade derive-pda ) + +# Get the program data address +PROGRAM_DATA_ADDR=$(solana program show --url | grep "ProgramData Address" | awk '{print $3}') + +# Execute the upgrade +bin/solana-ibc upgrade program \ + \ + \ + \ + \ + \ + $PROGRAM_DATA_ADDR +``` + +Example: + +```bash +# For ics26-router on localnet +UPGRADE_AUTH_PDA=$(bin/solana-ibc upgrade derive-pda \ + 4fMih2CidrXPeRx77kj3QcuBZwREYtxEbXjURUgadoe1 \ + FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx) + +PROGRAM_DATA_ADDR=$(solana program show FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx \ + --url http://localhost:8899 | grep "ProgramData Address" | awk '{print $3}') + +bin/solana-ibc upgrade program \ + http://localhost:8899 \ + solana-keypairs/localnet/deployer_wallet.json \ + FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx \ + \ + 4fMih2CidrXPeRx77kj3QcuBZwREYtxEbXjURUgadoe1 \ + $PROGRAM_DATA_ADDR +``` + +**Note:** The `just upgrade-solana-program` command (documented above) automates all these steps, including buffer creation, PDA derivation, and upgrade execution. For programmatic integration, see `e2e/interchaintestv8/solana_upgrade_test.go` for a complete Go implementation example. + +### Upgrade Security + +**Access Control:** + +- Only accounts with `UPGRADER_ROLE` can trigger upgrades +- CPI calls to `upgrade_program` are blocked +- Instructions sysvar verification prevents fake sysvar attacks + +**Authority Verification:** + +- Buffer authority must match program upgrade authority (BPF Loader requirement) +- Upgrade authority PDA seeds are validated via Anchor constraints +- Program account must be writable and executable + +**Audit Trail:** + +- All upgrades emit `ProgramUpgradedEvent` with: + - Program ID + - Upgrader public key + - Timestamp + +### Revoking Upgrade Permissions + +Remove upgrade capability from an account using the justfile command: + +```bash +just revoke-solana-role 8 +``` + +Or using the `solana-ibc` CLI directly: + +```bash +bin/solana-ibc access-manager revoke \ + \ + \ + 8 \ + \ + +``` + +## Development + +### Building Programs + +```bash +# Build all programs (generates all IDLs) +just build-solana + +# Build specific program only (generates only its IDL) +just build-solana ics26-router + +# Using anchor directly (from programs/solana directory) +cd programs/solana +anchor build +anchor build -p ics26-router +``` + +### Running Tests + +```bash +# All Solana tests (unit + integration) +just test-solana + +# Specific program tests (requires the app being built) +cargo test --manifest-path programs/solana/programs/ics26-router/Cargo.toml + +# E2E tests +just test-e2e-solana Test_Deploy +just test-e2e-solana-gmp Test_GMPCounterFromCosmos +just test-e2e-solana-upgrade Test_ProgramUpgrade_Via_AccessManager +``` + +### Code Quality + +```bash +# Format code +just fmt-solana + +# Run linter +just lint-solana +``` + +### IDL Generation + +IDL (Interface Definition Language) files are automatically generated during build: + +```bash +# Generate IDLs for all programs +just build-solana + +# Generate IDL for specific program only +just build-solana ics26-router +``` + +IDL files are written to `target/idl/` and contain the program's interface definition for client libraries and tools. diff --git a/programs/solana/package.json b/programs/solana/package.json deleted file mode 100644 index a773d503b..000000000 --- a/programs/solana/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "license": "ISC", - "scripts": { - "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", - "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" - }, - "dependencies": { - "@coral-xyz/anchor": "^0.31.1" - }, - "devDependencies": { - "@types/bn.js": "^5.1.0", - "@types/jest": "^29.0.3", - "jest": "^29.0.3", - "prettier": "^2.6.2", - "ts-jest": "^29.0.2", - "typescript": "^5.7.3" - } -} diff --git a/programs/solana/packages/solana-ibc-macros/src/lib.rs b/programs/solana/packages/solana-ibc-macros/src/lib.rs index 800726c3b..708628cc7 100644 --- a/programs/solana/packages/solana-ibc-macros/src/lib.rs +++ b/programs/solana/packages/solana-ibc-macros/src/lib.rs @@ -1,7 +1,7 @@ //! Procedural macros for IBC applications on Solana //! -//! This crate provides the `#[ibc_app]` macro which validates that IBC applications -//! implement all required callback functions with correct signatures. +//! This crate provides: +//! - `#[ibc_app]` - Validates IBC app callback implementations use proc_macro::TokenStream; use quote::quote; diff --git a/programs/solana/packages/solana-ibc-types/Cargo.toml b/programs/solana/packages/solana-ibc-types/Cargo.toml index 73d607fc9..9576de24e 100644 --- a/programs/solana/packages/solana-ibc-types/Cargo.toml +++ b/programs/solana/packages/solana-ibc-types/Cargo.toml @@ -11,6 +11,7 @@ categories = ["cryptography", "blockchain"] [dependencies] anchor-lang.workspace = true +solana-program.workspace = true prost.workspace = true solana-ibc-constants.workspace = true ibc-eureka-constrained-types.workspace = true diff --git a/programs/solana/packages/solana-ibc-types/src/access_manager.rs b/programs/solana/packages/solana-ibc-types/src/access_manager.rs new file mode 100644 index 000000000..bd479a18a --- /dev/null +++ b/programs/solana/packages/solana-ibc-types/src/access_manager.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::*; + +pub mod roles { + // Some roles are commented out, because we don't need them in Solana, + // but for consistency with Ethereum, let's keep their ids reserved. + + pub const ADMIN_ROLE: u64 = 0; + pub const PUBLIC_ROLE: u64 = u64::MAX; + pub const RELAYER_ROLE: u64 = 1; + pub const PAUSER_ROLE: u64 = 2; + pub const UNPAUSER_ROLE: u64 = 3; + // pub const DELEGATE_SENDER_ROLE: u64 = 4; + // pub const RATE_LIMITER_ROLE: u64 = 5; + pub const ID_CUSTOMIZER_ROLE: u64 = 6; + // pub const ERC20_CUSTOMIZER_ROLE: u64 = 7; + pub const UPGRADER_ROLE: u64 = 8; +} + +/// Backwards-compatible helper struct for getting access manager PDA +/// All actual types have been moved to the access-manager program +pub struct AccessManager; + +impl AccessManager { + pub const SEED: &'static [u8] = b"access_manager"; + + /// Get access manager PDA (backwards compatible helper) + pub fn pda(program_id: Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address(&[Self::SEED], &program_id) + } +} diff --git a/programs/solana/packages/solana-ibc-types/src/cpi.rs b/programs/solana/packages/solana-ibc-types/src/cpi.rs index 69dc2fb2a..26cd31bf0 100644 --- a/programs/solana/packages/solana-ibc-types/src/cpi.rs +++ b/programs/solana/packages/solana-ibc-types/src/cpi.rs @@ -48,3 +48,31 @@ pub fn validate_cpi_caller( Ok(()) } + +/// Validates that this instruction is called directly (NOT via CPI) +/// +/// Checks: +/// 1. `instruction_sysvar` is the real sysvar (prevents [Wormhole-style attack]) +/// 2. Current instruction's `program_id` IS self (rejects CPI calls) +/// +/// Use this for admin instructions that should only be called directly by users. +pub fn reject_cpi( + instruction_sysvar: &AccountInfo<'_>, + self_program_id: &Pubkey, +) -> core::result::Result<(), CpiValidationError> { + // CRITICAL: Validate that the instruction_sysvar account is actually the instructions sysvar + if instruction_sysvar.key() != anchor_lang::solana_program::sysvar::instructions::ID { + return Err(CpiValidationError::InvalidSysvar); + } + + // Get the current instruction (0 = current, relative offset) + let current_ix = get_instruction_relative(0, instruction_sysvar) + .map_err(|_| CpiValidationError::InvalidSysvar)?; + + // Reject CPI calls (when current instruction is NOT our own program) + if current_ix.program_id != *self_program_id { + return Err(CpiValidationError::UnauthorizedCaller); + } + + Ok(()) +} diff --git a/programs/solana/packages/solana-ibc-types/src/events.rs b/programs/solana/packages/solana-ibc-types/src/events.rs index 6bab056b5..a83a147ac 100644 --- a/programs/solana/packages/solana-ibc-types/src/events.rs +++ b/programs/solana/packages/solana-ibc-types/src/events.rs @@ -70,3 +70,11 @@ pub struct IBCAppAdded { #[event] #[derive(Debug, Clone)] pub struct NoopEvent {} + +/// Event emitted when access manager is updated +#[event] +#[derive(Debug, Clone)] +pub struct AccessManagerUpdated { + pub old_access_manager: Pubkey, + pub new_access_manager: Pubkey, +} diff --git a/programs/solana/packages/solana-ibc-types/src/ics07.rs b/programs/solana/packages/solana-ibc-types/src/ics07.rs index 916cd4583..369cfd612 100644 --- a/programs/solana/packages/solana-ibc-types/src/ics07.rs +++ b/programs/solana/packages/solana-ibc-types/src/ics07.rs @@ -49,6 +49,7 @@ pub struct ClientState { pub max_clock_drift: u64, pub frozen_height: IbcHeight, pub latest_height: IbcHeight, + pub access_manager: Pubkey, } impl ClientState { diff --git a/programs/solana/packages/solana-ibc-types/src/ics27.rs b/programs/solana/packages/solana-ibc-types/src/ics27.rs index bd004f263..639a17953 100644 --- a/programs/solana/packages/solana-ibc-types/src/ics27.rs +++ b/programs/solana/packages/solana-ibc-types/src/ics27.rs @@ -4,7 +4,7 @@ //! to ensure consistent PDA derivation across the system. use anchor_lang::prelude::*; -use anchor_lang::solana_program::hash::hash; +use solana_program::hash::hash; // Re-export from solana-ibc-proto pub use solana_ibc_proto::{ diff --git a/programs/solana/packages/solana-ibc-types/src/lib.rs b/programs/solana/packages/solana-ibc-types/src/lib.rs index 5b23fe3cb..225e57ab2 100644 --- a/programs/solana/packages/solana-ibc-types/src/lib.rs +++ b/programs/solana/packages/solana-ibc-types/src/lib.rs @@ -4,6 +4,7 @@ //! implementing IBC on Solana, including router messages (ICS26), //! light client types (ICS07), and Solana-specific PDA utilities. +pub mod access_manager; pub mod app_msgs; pub mod cpi; pub mod events; @@ -33,11 +34,12 @@ pub use ics27::{ }; pub use events::{ - AckPacketEvent, ClientAddedEvent, ClientUpdatedEvent, IBCAppAdded, NoopEvent, SendPacketEvent, - TimeoutPacketEvent, WriteAcknowledgementEvent, + AccessManagerUpdated, AckPacketEvent, ClientAddedEvent, ClientUpdatedEvent, IBCAppAdded, + NoopEvent, SendPacketEvent, TimeoutPacketEvent, WriteAcknowledgementEvent, }; pub use ibc_app_interface::ibc_app_instructions; -pub use cpi::{validate_cpi_caller, CpiValidationError}; +pub use access_manager::{roles, AccessManager}; +pub use cpi::{reject_cpi, validate_cpi_caller, CpiValidationError}; pub use utils::compute_discriminator; diff --git a/programs/solana/packages/solana-ibc-types/src/router.rs b/programs/solana/packages/solana-ibc-types/src/router.rs index 25dc74eea..ce435456d 100644 --- a/programs/solana/packages/solana-ibc-types/src/router.rs +++ b/programs/solana/packages/solana-ibc-types/src/router.rs @@ -67,8 +67,6 @@ pub struct ClientAccount { pub client_program_id: Pubkey, /// Counterparty chain information pub counterparty_info: CounterpartyInfo, - /// Authority that registered this client - pub authority: Pubkey, /// Whether the client is active pub active: bool, /// Reserved space for future fields @@ -214,7 +212,18 @@ impl IBCApp { } } -pub struct RouterState; +/// Router state account - matches the on-chain account structure in ICS26 router +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct RouterState { + /// Schema version for upgrades + pub version: AccountVersion, + /// Whether the router is paused (emergency stop) + pub paused: bool, + /// Access manager program ID for role-based access control + pub access_manager: Pubkey, + /// Reserved space for future fields + pub _reserved: [u8; 256], +} impl RouterState { pub const SEED: &'static [u8] = b"router_state"; diff --git a/programs/solana/packages/solana-ibc-types/src/utils.rs b/programs/solana/packages/solana-ibc-types/src/utils.rs index 61120496e..0f63c09f5 100644 --- a/programs/solana/packages/solana-ibc-types/src/utils.rs +++ b/programs/solana/packages/solana-ibc-types/src/utils.rs @@ -1,6 +1,6 @@ //! Utility functions for IBC on Solana -use anchor_lang::solana_program::hash::hash; +use solana_program::hash::hash; /// Compute Anchor instruction discriminator /// diff --git a/programs/solana/programs/access-manager/Cargo.toml b/programs/solana/programs/access-manager/Cargo.toml new file mode 100644 index 000000000..fad663992 --- /dev/null +++ b/programs/solana/programs/access-manager/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "access-manager" +version.workspace = true +description = "Role-based access manager for IBC on Solana" +edition.workspace = true +license.workspace = true +repository.workspace = true +readme = "../../../README.md" +keywords = ["cosmos", "ibc", "solana", "access-manager", "anchor"] +categories = ["cryptography", "blockchain"] + +[lib] +crate-type = ["cdylib", "lib"] +name = "access_manager" + +[features] +default = [] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +idl-build = ["anchor-lang/idl-build"] + +[dependencies] +anchor-lang.workspace = true +solana-ibc-types.workspace = true + +[dev-dependencies] +mollusk-svm.workspace = true +solana-sdk.workspace = true + +[lints] +workspace = true diff --git a/programs/solana/programs/access-manager/Xargo.toml b/programs/solana/programs/access-manager/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/programs/solana/programs/access-manager/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/programs/solana/programs/access-manager/src/errors.rs b/programs/solana/programs/access-manager/src/errors.rs new file mode 100644 index 000000000..216fa7aa0 --- /dev/null +++ b/programs/solana/programs/access-manager/src/errors.rs @@ -0,0 +1,31 @@ +use anchor_lang::prelude::*; +use solana_ibc_types::CpiValidationError; + +#[error_code] +pub enum AccessManagerError { + #[msg("Unauthorized: caller does not have required role")] + Unauthorized, + #[msg("Invalid role ID")] + InvalidRoleId, + #[msg("Cannot remove the last admin")] + CannotRemoveLastAdmin, + #[msg("Invalid sysenv: cross-program invocation from unexpected program")] + InvalidSysenv, + #[msg("Account must be a signer")] + SignerRequired, + #[msg("CPI calls not allowed")] + CpiNotAllowed, + #[msg("Invalid upgrade authority")] + InvalidUpgradeAuthority, +} + +impl From for AccessManagerError { + fn from(error: CpiValidationError) -> Self { + match error { + CpiValidationError::InvalidSysvar => Self::InvalidSysenv, + CpiValidationError::UnauthorizedCaller | CpiValidationError::DirectCallNotAllowed => { + Self::CpiNotAllowed + } + } + } +} diff --git a/programs/solana/programs/access-manager/src/events.rs b/programs/solana/programs/access-manager/src/events.rs new file mode 100644 index 000000000..2e98f49bf --- /dev/null +++ b/programs/solana/programs/access-manager/src/events.rs @@ -0,0 +1,34 @@ +use anchor_lang::prelude::*; + +#[event] +#[derive(Debug, Clone)] +pub struct RoleGrantedEvent { + pub role_id: u64, + pub account: Pubkey, + pub granted_by: Pubkey, +} + +#[event] +#[derive(Debug, Clone)] +pub struct RoleRevokedEvent { + pub role_id: u64, + pub account: Pubkey, + pub revoked_by: Pubkey, +} + +#[event] +#[derive(Debug, Clone)] +pub struct ProgramUpgradedEvent { + pub program: Pubkey, + pub authority: Pubkey, + pub timestamp: i64, +} + +#[event] +#[derive(Debug, Clone)] +pub struct ProgramExtendedEvent { + pub program: Pubkey, + pub authority: Pubkey, + pub additional_bytes: u32, + pub timestamp: i64, +} diff --git a/programs/solana/programs/access-manager/src/helpers.rs b/programs/solana/programs/access-manager/src/helpers.rs new file mode 100644 index 000000000..867790b3b --- /dev/null +++ b/programs/solana/programs/access-manager/src/helpers.rs @@ -0,0 +1,70 @@ +use crate::errors::AccessManagerError; +use crate::state::AccessManager; +use anchor_lang::prelude::*; + +/// Helper function to verify direct call authorization with role-based access control +/// +/// This function provides defense-in-depth by performing THREE security checks: +/// 1. Rejects CPI calls (instruction must be called directly) +/// 2. Verifies the account is a transaction signer (`is_signer` == true) +/// 3. Verifies the account has the required role +/// +/// This is the recommended pattern for all admin/privileged instructions. +/// +/// # Arguments +/// * `access_manager_account` - The access manager account info +/// * `role_id` - The role ID to check (e.g., `RELAYER_ROLE`, `PAUSER_ROLE`) +/// * `signer_account` - The account that must be a signer AND have the role +/// * `instructions_sysvar` - The instructions sysvar for CPI validation +/// * `program_id` - The current program ID +/// +/// # Returns +/// * `Ok(())` if all checks pass +/// * `Err(CpiNotAllowed)` if called via CPI +/// * `Err(SignerRequired)` if account is not a signer +/// * `Err(Unauthorized)` if account doesn't have the required role +/// +/// # Security +/// Prevents CPI-based signer spoofing attacks by ensuring: +/// - No CPI chain exists (direct call only) +/// - Account actually signed the transaction +/// - Account has proper authorization +/// +/// # Example +/// ```ignore +/// access_manager::require_role( +/// &ctx.accounts.access_manager, +/// solana_ibc_types::roles::PAUSER_ROLE, +/// &ctx.accounts.authority, +/// &ctx.accounts.instructions_sysvar, +/// &crate::ID, +/// )?; +/// ``` +pub fn require_role( + access_manager_account: &AccountInfo, + role_id: u64, + signer_account: &AccountInfo, + instructions_sysvar: &AccountInfo, + program_id: &Pubkey, +) -> Result<()> { + // Layer 1: Reject CPI calls - instruction must be called directly + // This prevents malicious programs from bypassing signer checks by spoofing signers in a CPI call. + // Only direct user transactions can pass this check, ensuring the signer is authentic. + solana_ibc_types::reject_cpi(instructions_sysvar, program_id) + .map_err(|_| error!(AccessManagerError::CpiNotAllowed))?; + + // Layer 2: Verify the account is actually a signer + require!(signer_account.is_signer, AccessManagerError::SignerRequired); + + // Layer 3: Verify the signer has the required role + let access_manager_data = access_manager_account.try_borrow_data()?; + let access_manager: AccessManager = + anchor_lang::AccountDeserialize::try_deserialize(&mut &access_manager_data[..])?; + + require!( + access_manager.has_role(role_id, &signer_account.key()), + AccessManagerError::Unauthorized + ); + + Ok(()) +} diff --git a/programs/solana/programs/access-manager/src/instructions.rs b/programs/solana/programs/access-manager/src/instructions.rs new file mode 100644 index 000000000..bf4029d0c --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions.rs @@ -0,0 +1,11 @@ +pub mod grant_role; +pub mod initialize; +pub mod renounce_role; +pub mod revoke_role; +pub mod upgrade_program; + +pub use grant_role::*; +pub use initialize::*; +pub use renounce_role::*; +pub use revoke_role::*; +pub use upgrade_program::*; diff --git a/programs/solana/programs/access-manager/src/instructions/grant_role.rs b/programs/solana/programs/access-manager/src/instructions/grant_role.rs new file mode 100644 index 000000000..b81b90293 --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions/grant_role.rs @@ -0,0 +1,251 @@ +use crate::errors::AccessManagerError; +use crate::events::RoleGrantedEvent; +use crate::state::AccessManager; +use anchor_lang::prelude::*; +use solana_ibc_types::{reject_cpi, roles}; + +#[derive(Accounts)] +pub struct GrantRole<'info> { + #[account( + mut, + seeds = [AccessManager::SEED], + bump + )] + pub access_manager: Account<'info, AccessManager>, + + pub admin: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn grant_role(ctx: Context, role_id: u64, account: Pubkey) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(AccessManagerError::from)?; + + // Only admins can grant roles + require!( + ctx.accounts + .access_manager + .has_role(roles::ADMIN_ROLE, &ctx.accounts.admin.key()), + AccessManagerError::Unauthorized + ); + + // Cannot grant PUBLIC_ROLE + require!( + role_id != roles::PUBLIC_ROLE, + AccessManagerError::InvalidRoleId + ); + + ctx.accounts.access_manager.grant_role(role_id, account)?; + + emit!(RoleGrantedEvent { + role_id, + account, + granted_by: ctx.accounts.admin.key(), + }); + + msg!( + "Role {} granted to {} by {}", + role_id, + account, + ctx.accounts.admin.key() + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::AccessManagerError; + use crate::test_utils::*; + use mollusk_svm::result::Check; + use solana_sdk::instruction::AccountMeta; + + #[test] + fn test_grant_role_success() { + let admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::GrantRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let access_manager = get_access_manager_from_result(&result, &access_manager_pda); + assert!(access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + } + + #[test] + fn test_grant_role_not_admin() { + let admin = Pubkey::new_unique(); + let non_admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::GrantRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(non_admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (non_admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_grant_role_invalid_role() { + let admin = Pubkey::new_unique(); + let account = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::GrantRole { + role_id: roles::PUBLIC_ROLE, // Cannot grant PUBLIC_ROLE + account, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::InvalidRoleId as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_grant_role_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::GrantRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + fake_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_grant_role_cpi_rejection() { + let admin = Pubkey::new_unique(); + let member = Pubkey::new_unique(); + let role_id = 100; + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::GrantRole { + role_id, + account: member, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + cpi_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_access_manager_cpi_rejection_error()], + ); + } +} diff --git a/programs/solana/programs/access-manager/src/instructions/initialize.rs b/programs/solana/programs/access-manager/src/instructions/initialize.rs new file mode 100644 index 000000000..c1632d1bb --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions/initialize.rs @@ -0,0 +1,276 @@ +use crate::errors::AccessManagerError; +use crate::state::AccessManager; +use anchor_lang::prelude::*; +use solana_ibc_types::{reject_cpi, roles}; + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account( + init, + payer = payer, + space = 8 + AccessManager::INIT_SPACE, + seeds = [AccessManager::SEED], + bump + )] + pub access_manager: Account<'info, AccessManager>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn initialize(ctx: Context, admin: Pubkey) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(AccessManagerError::from)?; + + let access_manager = &mut ctx.accounts.access_manager; + access_manager.roles = vec![]; + + // Grant ADMIN_ROLE to the initial admin + access_manager.grant_role(roles::ADMIN_ROLE, admin)?; + + msg!("Global access control initialized with admin: {}", admin); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use anchor_lang::InstructionData; + use mollusk_svm::result::Check; + use mollusk_svm::Mollusk; + use solana_sdk::account::Account; + use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::pubkey::Pubkey; + use solana_sdk::{native_loader, system_program}; + + #[test] + fn test_initialize_happy_path() { + let admin = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + + let (access_manager_pda, _) = + Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID); + + let instruction_data = crate::instruction::Initialize { admin }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + let payer_lamports = 10_000_000_000; + let accounts = vec![ + ( + access_manager_pda, + Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + payer, + Account { + lamports: payer_lamports, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + system_program::ID, + Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ), + ( + solana_sdk::sysvar::instructions::ID, + crate::test_utils::create_instructions_sysvar_account(), + ), + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_access_manager_program_path()); + + let checks = vec![ + Check::success(), + Check::account(&access_manager_pda) + .owner(&crate::ID) + .build(), + ]; + + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let access_manager_account = result + .resulting_accounts + .iter() + .find(|(pubkey, _)| pubkey == &access_manager_pda) + .map(|(_, account)| account) + .expect("Access control account not found"); + + let deserialized_access_manager: AccessManager = + anchor_lang::AccountDeserialize::try_deserialize(&mut &access_manager_account.data[..]) + .expect("Failed to deserialize access control"); + + // Verify admin has ADMIN_ROLE + assert!(deserialized_access_manager.has_role(roles::ADMIN_ROLE, &admin)); + assert_eq!(deserialized_access_manager.roles.len(), 1); + } + + #[test] + fn test_initialize_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); + + let (access_manager_pda, _) = + Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID); + + let instruction_data = crate::instruction::Initialize { admin }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let payer_lamports = 10_000_000_000; + let accounts = vec![ + ( + access_manager_pda, + Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + payer, + Account { + lamports: payer_lamports, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + system_program::ID, + Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ), + fake_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_access_manager_program_path()); + + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_initialize_cpi_rejection() { + let payer = Pubkey::new_unique(); + let admin = Pubkey::new_unique(); + + let (access_manager_pda, _) = + Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID); + + let instruction_data = crate::instruction::Initialize { admin }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let payer_lamports = 10_000_000_000; + let accounts = vec![ + ( + access_manager_pda, + Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + payer, + Account { + lamports: payer_lamports, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + system_program::ID, + Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ), + cpi_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_access_manager_program_path()); + + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_access_manager_cpi_rejection_error()], + ); + } +} diff --git a/programs/solana/programs/access-manager/src/instructions/renounce_role.rs b/programs/solana/programs/access-manager/src/instructions/renounce_role.rs new file mode 100644 index 000000000..d4cc01cfd --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions/renounce_role.rs @@ -0,0 +1,240 @@ +use crate::errors::AccessManagerError; +use crate::events::RoleRevokedEvent; +use crate::state::AccessManager; +use anchor_lang::prelude::*; +use solana_ibc_types::{reject_cpi, roles}; + +#[derive(Accounts)] +pub struct RenounceRole<'info> { + #[account( + mut, + seeds = [AccessManager::SEED], + bump + )] + pub access_manager: Account<'info, AccessManager>, + + pub caller: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn renounce_role(ctx: Context, role_id: u64) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(AccessManagerError::from)?; + + // Cannot renounce PUBLIC_ROLE + require!( + role_id != roles::PUBLIC_ROLE, + AccessManagerError::InvalidRoleId + ); + + let caller_key = ctx.accounts.caller.key(); + + // Verify caller has the role they're trying to renounce + require!( + ctx.accounts.access_manager.has_role(role_id, &caller_key), + AccessManagerError::Unauthorized + ); + + // Revoke the role from caller (will fail if trying to remove last admin) + ctx.accounts + .access_manager + .revoke_role(role_id, &caller_key)?; + + emit!(RoleRevokedEvent { + role_id, + account: caller_key, + revoked_by: caller_key, // Self-revocation + }); + + msg!("Role {} renounced by {}", role_id, caller_key); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::AccessManagerError; + use crate::test_utils::*; + use mollusk_svm::result::Check; + use solana_sdk::instruction::AccountMeta; + + #[test] + fn test_renounce_role_success() { + let relayer = Pubkey::new_unique(); + let admin = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, relayer); + + let instruction = build_instruction( + crate::instruction::RenounceRole { + role_id: roles::RELAYER_ROLE, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (relayer, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let access_manager = get_access_manager_from_result(&result, &access_manager_pda); + assert!(!access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + } + + #[test] + fn test_renounce_role_without_having_role() { + let relayer = Pubkey::new_unique(); + let admin = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::RenounceRole { + role_id: roles::RELAYER_ROLE, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (relayer, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_renounce_role_cannot_remove_last_admin() { + let admin = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + + let instruction = build_instruction( + crate::instruction::RenounceRole { + role_id: roles::ADMIN_ROLE, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::CannotRemoveLastAdmin as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_renounce_role_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, relayer); + + let instruction = build_instruction( + crate::instruction::RenounceRole { + role_id: roles::RELAYER_ROLE, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (relayer, create_signer_account()), + fake_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_renounce_role_cpi_rejection() { + let relayer = Pubkey::new_unique(); + let role_id = 100; + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(relayer, role_id, relayer); + + let instruction = build_instruction( + crate::instruction::RenounceRole { role_id }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (relayer, create_signer_account()), + cpi_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_access_manager_cpi_rejection_error()], + ); + } +} diff --git a/programs/solana/programs/access-manager/src/instructions/revoke_role.rs b/programs/solana/programs/access-manager/src/instructions/revoke_role.rs new file mode 100644 index 000000000..e328616c1 --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions/revoke_role.rs @@ -0,0 +1,255 @@ +use crate::errors::AccessManagerError; +use crate::events::RoleRevokedEvent; +use crate::state::AccessManager; +use anchor_lang::prelude::*; +use solana_ibc_types::{reject_cpi, roles}; + +#[derive(Accounts)] +pub struct RevokeRole<'info> { + #[account( + mut, + seeds = [AccessManager::SEED], + bump + )] + pub access_manager: Account<'info, AccessManager>, + + pub admin: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn revoke_role(ctx: Context, role_id: u64, account: Pubkey) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(AccessManagerError::from)?; + + // Only admins can revoke roles + require!( + ctx.accounts + .access_manager + .has_role(roles::ADMIN_ROLE, &ctx.accounts.admin.key()), + AccessManagerError::Unauthorized + ); + + // Cannot revoke PUBLIC_ROLE + require!( + role_id != roles::PUBLIC_ROLE, + AccessManagerError::InvalidRoleId + ); + + // Revoke the role (will fail if trying to remove last admin) + ctx.accounts.access_manager.revoke_role(role_id, &account)?; + + emit!(RoleRevokedEvent { + role_id, + account, + revoked_by: ctx.accounts.admin.key(), + }); + + msg!( + "Role {} revoked from {} by {}", + role_id, + account, + ctx.accounts.admin.key() + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::AccessManagerError; + use crate::test_utils::*; + use mollusk_svm::result::Check; + use solana_sdk::instruction::AccountMeta; + + #[test] + fn test_revoke_role_success() { + let admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, relayer); + + let instruction = build_instruction( + crate::instruction::RevokeRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let access_manager = get_access_manager_from_result(&result, &access_manager_pda); + assert!(!access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + } + + #[test] + fn test_revoke_role_not_admin() { + let admin = Pubkey::new_unique(); + let non_admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, relayer); + + let instruction = build_instruction( + crate::instruction::RevokeRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(non_admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (non_admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_revoke_role_invalid_role() { + let admin = Pubkey::new_unique(); + let account = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, account); + + let instruction = build_instruction( + crate::instruction::RevokeRole { + role_id: roles::PUBLIC_ROLE, // Cannot revoke PUBLIC_ROLE + account, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + ( + solana_sdk::sysvar::instructions::ID, + create_instructions_sysvar_account(), + ), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::InvalidRoleId as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_revoke_role_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::RELAYER_ROLE, relayer); + + let instruction = build_instruction( + crate::instruction::RevokeRole { + role_id: roles::RELAYER_ROLE, + account: relayer, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + fake_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_revoke_role_cpi_rejection() { + let admin = Pubkey::new_unique(); + let member = Pubkey::new_unique(); + let role_id = 100; + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::RevokeRole { + role_id, + account: member, + }, + vec![ + AccountMeta::new(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + cpi_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_access_manager_cpi_rejection_error()], + ); + } +} diff --git a/programs/solana/programs/access-manager/src/instructions/upgrade_program.rs b/programs/solana/programs/access-manager/src/instructions/upgrade_program.rs new file mode 100644 index 000000000..9a826c6c9 --- /dev/null +++ b/programs/solana/programs/access-manager/src/instructions/upgrade_program.rs @@ -0,0 +1,506 @@ +use crate::errors::AccessManagerError; +use crate::events::ProgramUpgradedEvent; +use crate::state::AccessManager; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::bpf_loader_upgradeable; +use solana_ibc_types::roles; + +#[derive(Accounts)] +#[instruction(target_program: Pubkey)] +pub struct UpgradeProgram<'info> { + #[account( + seeds = [AccessManager::SEED], + bump + )] + pub access_manager: Account<'info, AccessManager>, + + /// CHECK: Validated as executable program account + /// Must be writable because BPF Loader Upgradeable requires both program and programdata + /// accounts to be writable during upgrade. The program account contains metadata and a + /// pointer to the programdata account, which may be updated during the upgrade process. + #[account( + mut, + executable, + constraint = program.key() == target_program @ AccessManagerError::InvalidUpgradeAuthority + )] + pub program: AccountInfo<'info>, + + /// CHECK: Validated via BPF Loader constraints + #[account(mut)] + pub program_data: AccountInfo<'info>, + + /// CHECK: Validated via BPF Loader as buffer account + #[account(mut)] + pub buffer: AccountInfo<'info>, + + /// CHECK: Validated via seeds constraint + #[account( + mut, + seeds = [AccessManager::UPGRADE_AUTHORITY_SEED, target_program.as_ref()], + bump + )] + pub upgrade_authority: AccountInfo<'info>, + + /// CHECK: Can be any account to receive refunded rent + #[account(mut)] + pub spill: AccountInfo<'info>, + + pub authority: Signer<'info>, + + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, + + /// CHECK: Must be BPF Loader Upgradeable program ID + #[account(address = bpf_loader_upgradeable::ID)] + pub bpf_loader_upgradeable: AccountInfo<'info>, + + pub rent: Sysvar<'info, Rent>, + + pub clock: Sysvar<'info, Clock>, +} + +pub fn upgrade_program(ctx: Context, target_program: Pubkey) -> Result<()> { + crate::helpers::require_role( + &ctx.accounts.access_manager.to_account_info(), + roles::UPGRADER_ROLE, + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + + let (upgrade_authority_pda, bump) = + AccessManager::upgrade_authority_pda(&target_program, &crate::ID); + + let upgrade_ix = bpf_loader_upgradeable::upgrade( + &ctx.accounts.program.key(), + &ctx.accounts.buffer.key(), + &upgrade_authority_pda, + &ctx.accounts.spill.key(), + ); + + anchor_lang::solana_program::program::invoke_signed( + &upgrade_ix, + &[ + ctx.accounts.program_data.to_account_info(), + ctx.accounts.program.to_account_info(), + ctx.accounts.buffer.to_account_info(), + ctx.accounts.spill.to_account_info(), + ctx.accounts.rent.to_account_info(), + ctx.accounts.clock.to_account_info(), + ctx.accounts.upgrade_authority.to_account_info(), + ], + &[&[ + AccessManager::UPGRADE_AUTHORITY_SEED, + target_program.as_ref(), + &[bump], + ]], + )?; + + let clock = Clock::get()?; + emit!(ProgramUpgradedEvent { + program: target_program, + authority: ctx.accounts.authority.key(), + timestamp: clock.unix_timestamp, + }); + + msg!( + "Program {} upgraded by {}", + target_program, + ctx.accounts.authority.key() + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use mollusk_svm::result::Check; + use solana_sdk::{account::Account, instruction::AccountMeta}; + + fn setup_upgrade_test( + admin: Pubkey, + upgrader: Pubkey, + target_program: Pubkey, + ) -> ( + Pubkey, + Account, + Pubkey, + Pubkey, + Pubkey, + Pubkey, + Vec, + ) { + let (access_manager_pda, mut access_manager_account) = + create_initialized_access_manager(admin); + + let mut access_manager_data = get_account_data::(&access_manager_account); + access_manager_data + .grant_role(roles::UPGRADER_ROLE, upgrader) + .unwrap(); + access_manager_account.data = serialize_account(&access_manager_data); + + let (upgrade_authority_pda, _) = + AccessManager::upgrade_authority_pda(&target_program, &crate::ID); + + let program_data_address = Pubkey::new_unique(); + let buffer = Pubkey::new_unique(); + let spill = Pubkey::new_unique(); + + let account_metas = build_upgrade_account_metas( + access_manager_pda, + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + upgrader, + ); + + ( + access_manager_pda, + access_manager_account, + upgrade_authority_pda, + program_data_address, + buffer, + spill, + account_metas, + ) + } + + fn create_program_accounts( + target_program: Pubkey, + program_data_address: Pubkey, + buffer: Pubkey, + upgrade_authority_pda: Pubkey, + spill: Pubkey, + upgrader: Pubkey, + ) -> Vec<(Pubkey, Account)> { + vec![ + ( + target_program, + Account { + lamports: 1_000_000, + data: vec![], + owner: bpf_loader_upgradeable::ID, + executable: true, + ..Default::default() + }, + ), + ( + program_data_address, + Account { + lamports: 1_000_000, + data: vec![0; 100], + owner: bpf_loader_upgradeable::ID, + ..Default::default() + }, + ), + ( + buffer, + Account { + lamports: 1_000_000, + data: vec![0; 100], + owner: bpf_loader_upgradeable::ID, + ..Default::default() + }, + ), + ( + upgrade_authority_pda, + Account { + lamports: 1_000_000, + owner: crate::ID, + ..Default::default() + }, + ), + ( + spill, + Account { + lamports: 1_000_000, + owner: solana_sdk::system_program::ID, + ..Default::default() + }, + ), + (upgrader, create_signer_account()), + ( + bpf_loader_upgradeable::ID, + Account { + lamports: 1_000_000, + executable: true, + owner: solana_sdk::native_loader::ID, + ..Default::default() + }, + ), + create_rent_sysvar_account(), + create_clock_sysvar_account(), + ] + } + + fn build_upgrade_account_metas( + access_manager_pda: Pubkey, + target_program: Pubkey, + program_data_address: Pubkey, + buffer: Pubkey, + upgrade_authority_pda: Pubkey, + spill: Pubkey, + authority: Pubkey, + ) -> Vec { + vec![ + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new(target_program, false), + AccountMeta::new(program_data_address, false), + AccountMeta::new(buffer, false), + AccountMeta::new(upgrade_authority_pda, false), + AccountMeta::new(spill, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + AccountMeta::new_readonly(bpf_loader_upgradeable::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::rent::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::clock::ID, false), + ] + } + + #[allow(clippy::too_many_arguments)] + fn build_upgrade_instruction_and_accounts( + access_manager_pda: Pubkey, + access_manager_account: Account, + target_program: Pubkey, + program_data_address: Pubkey, + buffer: Pubkey, + upgrade_authority_pda: Pubkey, + spill: Pubkey, + authority: Pubkey, + sysvar_account: (Pubkey, Account), + ) -> (solana_sdk::instruction::Instruction, Vec<(Pubkey, Account)>) { + let account_metas = build_upgrade_account_metas( + access_manager_pda, + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + authority, + ); + + let instruction = build_instruction( + crate::instruction::UpgradeProgram { target_program }, + account_metas, + ); + + let mut accounts = vec![(access_manager_pda, access_manager_account)]; + accounts.extend(create_program_accounts( + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + authority, + )); + accounts.push(sysvar_account); + + (instruction, accounts) + } + + // Note: This test cannot fully succeed in Mollusk because invoke_signed to bpf_loader_upgradeable + // references additional accounts not available in the unit test environment. + // The instruction logic is validated by the authorization tests. + // Full upgrade testing should be done in integration tests. + #[test] + #[ignore = "Requires full integration test setup with BPF Loader"] + fn test_upgrade_program_success() { + let admin = Pubkey::new_unique(); + let upgrader = Pubkey::new_unique(); + let target_program = Pubkey::new_unique(); + + let ( + access_manager_pda, + access_manager_account, + upgrade_authority_pda, + program_data_address, + buffer, + spill, + account_metas, + ) = setup_upgrade_test(admin, upgrader, target_program); + + let instruction = build_instruction( + crate::instruction::UpgradeProgram { target_program }, + account_metas, + ); + + let mut accounts = vec![(access_manager_pda, access_manager_account)]; + accounts.extend(create_program_accounts( + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + upgrader, + )); + accounts.push(create_instructions_sysvar_account_with_caller(crate::ID)); + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_upgrade_program_not_upgrader() { + let admin = Pubkey::new_unique(); + let non_upgrader = Pubkey::new_unique(); + let target_program = Pubkey::new_unique(); + + let (access_manager_pda, access_manager_account) = create_initialized_access_manager(admin); + let (upgrade_authority_pda, _) = + AccessManager::upgrade_authority_pda(&target_program, &crate::ID); + + let program_data_address = Pubkey::new_unique(); + let buffer = Pubkey::new_unique(); + let spill = Pubkey::new_unique(); + + let (instruction, accounts) = build_upgrade_instruction_and_accounts( + access_manager_pda, + access_manager_account, + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + non_upgrader, + create_instructions_sysvar_account_with_caller(crate::ID), + ); + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_upgrade_program_cpi_rejection() { + let admin = Pubkey::new_unique(); + let upgrader = Pubkey::new_unique(); + let target_program = Pubkey::new_unique(); + + let ( + access_manager_pda, + access_manager_account, + upgrade_authority_pda, + program_data_address, + buffer, + spill, + account_metas, + ) = setup_upgrade_test(admin, upgrader, target_program); + + let instruction = build_instruction( + crate::instruction::UpgradeProgram { target_program }, + account_metas, + ); + + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let mut accounts = vec![(access_manager_pda, access_manager_account)]; + accounts.extend(create_program_accounts( + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + upgrader, + )); + accounts.push(cpi_sysvar_account); + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_access_manager_cpi_rejection_error()], + ); + } + + #[test] + fn test_upgrade_program_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let upgrader = Pubkey::new_unique(); + let target_program = Pubkey::new_unique(); + + let ( + access_manager_pda, + access_manager_account, + upgrade_authority_pda, + program_data_address, + buffer, + spill, + account_metas, + ) = setup_upgrade_test(admin, upgrader, target_program); + + let instruction = build_instruction( + crate::instruction::UpgradeProgram { target_program }, + account_metas, + ); + + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let mut accounts = vec![(access_manager_pda, access_manager_account)]; + accounts.extend(create_program_accounts( + target_program, + program_data_address, + buffer, + upgrade_authority_pda, + spill, + upgrader, + )); + accounts.push(fake_sysvar_account); + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_upgrade_program_wrong_pda() { + let admin = Pubkey::new_unique(); + let upgrader = Pubkey::new_unique(); + let target_program = Pubkey::new_unique(); + + let (access_manager_pda, mut access_manager_account) = + create_initialized_access_manager(admin); + + let mut access_manager_data = get_account_data::(&access_manager_account); + access_manager_data + .grant_role(roles::UPGRADER_ROLE, upgrader) + .unwrap(); + access_manager_account.data = serialize_account(&access_manager_data); + + let wrong_upgrade_authority = Pubkey::new_unique(); + let program_data_address = Pubkey::new_unique(); + let buffer = Pubkey::new_unique(); + let spill = Pubkey::new_unique(); + + let (instruction, accounts) = build_upgrade_instruction_and_accounts( + access_manager_pda, + access_manager_account, + target_program, + program_data_address, + buffer, + wrong_upgrade_authority, + spill, + upgrader, + create_instructions_sysvar_account_with_caller(crate::ID), + ); + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ErrorCode::ConstraintSeeds as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } +} diff --git a/programs/solana/programs/access-manager/src/lib.rs b/programs/solana/programs/access-manager/src/lib.rs new file mode 100644 index 000000000..265d3b3b9 --- /dev/null +++ b/programs/solana/programs/access-manager/src/lib.rs @@ -0,0 +1,52 @@ +use anchor_lang::prelude::*; + +pub mod errors; +pub mod events; +pub mod helpers; +pub mod instructions; +pub mod state; +#[cfg(test)] +pub mod test_utils; +pub mod types; + +pub use errors::AccessManagerError; +pub use helpers::require_role; +use instructions::*; +pub use types::RoleData; + +declare_id!("4fMih2CidrXPeRx77kj3QcuBZwREYtxEbXjURUgadoe1"); + +pub fn get_access_manager_program_path() -> &'static str { + use std::sync::OnceLock; + static PATH: OnceLock = OnceLock::new(); + + PATH.get_or_init(|| { + std::env::var("access_manager_PROGRAM_PATH") + .unwrap_or_else(|_| "../../target/deploy/access_manager".to_string()) + }) +} + +#[program] +pub mod access_manager { + use super::*; + + pub fn initialize(ctx: Context, admin: Pubkey) -> Result<()> { + instructions::initialize(ctx, admin) + } + + pub fn grant_role(ctx: Context, role_id: u64, account: Pubkey) -> Result<()> { + instructions::grant_role(ctx, role_id, account) + } + + pub fn revoke_role(ctx: Context, role_id: u64, account: Pubkey) -> Result<()> { + instructions::revoke_role(ctx, role_id, account) + } + + pub fn renounce_role(ctx: Context, role_id: u64) -> Result<()> { + instructions::renounce_role(ctx, role_id) + } + + pub fn upgrade_program(ctx: Context, target_program: Pubkey) -> Result<()> { + instructions::upgrade_program(ctx, target_program) + } +} diff --git a/programs/solana/programs/access-manager/src/state.rs b/programs/solana/programs/access-manager/src/state.rs new file mode 100644 index 000000000..539f7eded --- /dev/null +++ b/programs/solana/programs/access-manager/src/state.rs @@ -0,0 +1,216 @@ +use crate::types::RoleData; +use anchor_lang::prelude::*; +use solana_ibc_types::roles; + +#[account] +#[derive(InitSpace, Debug)] +pub struct AccessManager { + #[max_len(16)] + pub roles: Vec, +} + +impl AccessManager { + pub const SEED: &'static [u8] = b"access_manager"; + pub const UPGRADE_AUTHORITY_SEED: &'static [u8] = b"upgrade_authority"; + + /// Get upgrade authority PDA for a target program + pub fn upgrade_authority_pda(target_program: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[Self::UPGRADE_AUTHORITY_SEED, target_program.as_ref()], + program_id, + ) + } + + pub fn has_role(&self, role_id: u64, account: &Pubkey) -> bool { + // PUBLIC_ROLE is accessible to everyone + if role_id == roles::PUBLIC_ROLE { + return true; + } + + // Check if account has the specific role + self.roles + .iter() + .find(|r| r.role_id == role_id) + .is_some_and(|r| r.members.contains(account)) + } + + pub fn grant_role(&mut self, role_id: u64, account: Pubkey) -> Result<()> { + if let Some(role) = self.roles.iter_mut().find(|r| r.role_id == role_id) { + if !role.members.contains(&account) { + role.members.push(account); + } + } else { + self.roles.push(RoleData { + role_id, + members: vec![account], + }); + } + Ok(()) + } + + pub fn revoke_role(&mut self, role_id: u64, account: &Pubkey) -> Result<()> { + // Prevent removing the last admin + if role_id == roles::ADMIN_ROLE && self.is_last_admin(account) { + return Err(crate::errors::AccessManagerError::CannotRemoveLastAdmin.into()); + } + + if let Some(role) = self.roles.iter_mut().find(|r| r.role_id == role_id) { + role.members.retain(|m| m != account); + } + Ok(()) + } + + /// Check if an account is the last admin + fn is_last_admin(&self, account: &Pubkey) -> bool { + self.roles + .iter() + .find(|r| r.role_id == roles::ADMIN_ROLE) + .is_some_and(|admin_role| { + admin_role.members.len() == 1 && admin_role.members.contains(account) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_access_manager() -> AccessManager { + AccessManager { roles: vec![] } + } + + fn create_access_manager_with_roles(roles: Vec) -> AccessManager { + AccessManager { roles } + } + + #[test] + fn test_admin_does_not_auto_pass_roles() { + let admin = Pubkey::new_unique(); + let mut access_manager = create_access_manager(); + + // Grant admin role + access_manager.grant_role(roles::ADMIN_ROLE, admin).unwrap(); + + // Admin should have ADMIN_ROLE + assert!(access_manager.has_role(roles::ADMIN_ROLE, &admin)); + + // Admin should NOT automatically have other roles + assert!(!access_manager.has_role(roles::RELAYER_ROLE, &admin)); + assert!(!access_manager.has_role(roles::PAUSER_ROLE, &admin)); + assert!(!access_manager.has_role(roles::UNPAUSER_ROLE, &admin)); + } + + #[test] + fn test_public_role_accessible_to_all() { + let anyone = Pubkey::new_unique(); + let admin = Pubkey::new_unique(); + let access_manager = create_access_manager(); + + assert!(access_manager.has_role(roles::PUBLIC_ROLE, &anyone)); + assert!(access_manager.has_role(roles::PUBLIC_ROLE, &admin)); + } + + #[test] + fn test_grant_role() { + let relayer = Pubkey::new_unique(); + let mut access_manager = create_access_manager(); + + assert!(!access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + + access_manager + .grant_role(roles::RELAYER_ROLE, relayer) + .unwrap(); + + assert!(access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + } + + #[test] + fn test_revoke_role() { + let relayer = Pubkey::new_unique(); + let mut access_manager = create_access_manager_with_roles(vec![RoleData { + role_id: roles::RELAYER_ROLE, + members: vec![relayer], + }]); + + assert!(access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + + access_manager + .revoke_role(roles::RELAYER_ROLE, &relayer) + .unwrap(); + + assert!(!access_manager.has_role(roles::RELAYER_ROLE, &relayer)); + } + + #[test] + fn test_cannot_remove_last_admin() { + let admin = Pubkey::new_unique(); + let mut access_manager = create_access_manager_with_roles(vec![RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![admin], + }]); + + assert!(access_manager.has_role(roles::ADMIN_ROLE, &admin)); + + // Should fail to revoke last admin + let result = access_manager.revoke_role(roles::ADMIN_ROLE, &admin); + assert!(result.is_err()); + + // Admin should still have the role + assert!(access_manager.has_role(roles::ADMIN_ROLE, &admin)); + } + + #[test] + fn test_can_remove_non_last_admin() { + let admin1 = Pubkey::new_unique(); + let admin2 = Pubkey::new_unique(); + let mut access_manager = create_access_manager_with_roles(vec![RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![admin1, admin2], + }]); + + // Should succeed to revoke one admin when multiple exist + access_manager + .revoke_role(roles::ADMIN_ROLE, &admin1) + .unwrap(); + + assert!(!access_manager.has_role(roles::ADMIN_ROLE, &admin1)); + assert!(access_manager.has_role(roles::ADMIN_ROLE, &admin2)); + } + + #[test] + fn test_grant_role_idempotent() { + let relayer = Pubkey::new_unique(); + let mut access_manager = create_access_manager(); + + access_manager + .grant_role(roles::RELAYER_ROLE, relayer) + .unwrap(); + access_manager + .grant_role(roles::RELAYER_ROLE, relayer) + .unwrap(); + + let role = access_manager + .roles + .iter() + .find(|r| r.role_id == roles::RELAYER_ROLE) + .unwrap(); + assert_eq!(role.members.len(), 1); + } + + #[test] + fn test_multiple_members_per_role() { + let relayer1 = Pubkey::new_unique(); + let relayer2 = Pubkey::new_unique(); + let mut access_manager = create_access_manager(); + + access_manager + .grant_role(roles::RELAYER_ROLE, relayer1) + .unwrap(); + access_manager + .grant_role(roles::RELAYER_ROLE, relayer2) + .unwrap(); + + assert!(access_manager.has_role(roles::RELAYER_ROLE, &relayer1)); + assert!(access_manager.has_role(roles::RELAYER_ROLE, &relayer2)); + } +} diff --git a/programs/solana/programs/access-manager/src/test_utils.rs b/programs/solana/programs/access-manager/src/test_utils.rs new file mode 100644 index 000000000..7cba88f0a --- /dev/null +++ b/programs/solana/programs/access-manager/src/test_utils.rs @@ -0,0 +1,374 @@ +use crate::state::AccessManager; +use crate::types::RoleData; +use anchor_lang::prelude::*; +use mollusk_svm::{result::InstructionResult, Mollusk}; +use solana_ibc_types::roles; +use solana_sdk::{ + account::Account, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + system_program, +}; + +pub fn serialize_access_manager(access_manager: &AccessManager) -> Vec { + let mut data = AccessManager::DISCRIMINATOR.to_vec(); + access_manager.serialize(&mut data).unwrap(); + data +} + +pub fn create_initialized_access_manager(admin: Pubkey) -> (Pubkey, Account) { + let (access_manager_pda, _) = Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID); + + let access_manager = AccessManager { + roles: vec![RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![admin], + }], + }; + + // Use INIT_SPACE to ensure account has enough space for max roles + let mut data = vec![0u8; 8 + AccessManager::INIT_SPACE]; + data[0..8].copy_from_slice(AccessManager::DISCRIMINATOR); + access_manager.serialize(&mut &mut data[8..]).unwrap(); + + ( + access_manager_pda, + Account { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub fn create_access_manager_with_role( + admin: Pubkey, + role_id: u64, + member: Pubkey, +) -> (Pubkey, Account) { + let (access_manager_pda, _) = Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID); + + let mut roles_vec = vec![RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![admin], + }]; + + // Add the requested role if it's not already ADMIN_ROLE or if member is different + if role_id != roles::ADMIN_ROLE || member != admin { + if let Some(existing_role) = roles_vec.iter_mut().find(|r| r.role_id == role_id) { + if !existing_role.members.contains(&member) { + existing_role.members.push(member); + } + } else { + roles_vec.push(RoleData { + role_id, + members: vec![member], + }); + } + } + + let access_manager = AccessManager { roles: roles_vec }; + + // Use INIT_SPACE to ensure account has enough space for max roles + let mut data = vec![0u8; 8 + AccessManager::INIT_SPACE]; + data[0..8].copy_from_slice(AccessManager::DISCRIMINATOR); + access_manager.serialize(&mut &mut data[8..]).unwrap(); + + ( + access_manager_pda, + Account { + lamports: 1_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub fn create_signer_account() -> Account { + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + } +} + +pub fn setup_mollusk() -> Mollusk { + Mollusk::new(&crate::ID, crate::get_access_manager_program_path()) +} + +/// Anchor error code offset +pub const ANCHOR_ERROR_OFFSET: u32 = 6000; + +/// Build instruction for access-manager program +pub fn build_instruction( + instruction_data: T, + accounts: Vec, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts, + data: instruction_data.data(), + } +} + +/// Get the `access_manager` PDA +pub fn get_access_manager_pda() -> Pubkey { + Pubkey::find_program_address(&[AccessManager::SEED], &crate::ID).0 +} + +/// Deserialize `access_manager` from instruction result +pub fn get_access_manager_from_result(result: &InstructionResult, pda: &Pubkey) -> AccessManager { + let account = result + .resulting_accounts + .iter() + .find(|(pubkey, _)| pubkey == pda) + .map(|(_, account)| account) + .expect("Access manager account not found"); + + anchor_lang::AccountDeserialize::try_deserialize(&mut &account.data[..]) + .expect("Failed to deserialize access manager") +} + +/// Create instructions sysvar account for direct call (not CPI) +pub fn create_instructions_sysvar_account() -> Account { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + // Create minimal mock instruction to simulate direct call + // Current instruction has this program as the program_id + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &crate::ID, // Direct call to our program + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Create fake instructions sysvar for testing Wormhole-style attack +/// Returns (`fake_pubkey`, account) where `fake_pubkey` is NOT the real instructions sysvar ID +pub fn create_fake_instructions_sysvar_account(caller_program_id: Pubkey) -> (Pubkey, Account) { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &caller_program_id, // Fake caller program + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + // Use a FAKE pubkey (not the real instructions sysvar ID) + let fake_sysvar_pubkey = Pubkey::new_unique(); + + ( + fake_sysvar_pubkey, + Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +/// Helper for testing Wormhole-style fake sysvar attacks +/// Automatically finds and replaces the instructions sysvar with a fake one +/// Returns (`modified_instruction`, `fake_sysvar_account_tuple`) +pub fn setup_fake_sysvar_attack( + mut instruction: Instruction, + program_id: Pubkey, +) -> (Instruction, (Pubkey, Account)) { + let (fake_sysvar_pubkey, fake_sysvar_account) = + create_fake_instructions_sysvar_account(program_id); + + // Find the instructions sysvar account and replace it with the fake one + let sysvar_account_index = instruction + .accounts + .iter() + .position(|acc| acc.pubkey == solana_sdk::sysvar::instructions::ID) + .expect("Instructions sysvar account not found in instruction"); + + instruction.accounts[sysvar_account_index] = + AccountMeta::new_readonly(fake_sysvar_pubkey, false); + + (instruction, (fake_sysvar_pubkey, fake_sysvar_account)) +} + +/// Expected error for Wormhole-style sysvar attacks (Anchor's address constraint violation) +pub fn expect_sysvar_attack_error() -> mollusk_svm::result::Check<'static> { + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ErrorCode::ConstraintAddress as u32, + )) +} + +/// Create instructions sysvar that simulates a CPI call from another program +/// Uses the REAL sysvar address but with a different `program_id` to simulate CPI context +pub fn create_cpi_instructions_sysvar_account(caller_program_id: Pubkey) -> Account { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &caller_program_id, // Different program calling via CPI + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Helper for testing CPI rejection +/// Replaces the instructions sysvar with one that simulates a CPI call +/// Returns (`modified_instruction`, `cpi_sysvar_account_tuple`) +pub fn setup_cpi_call_test( + instruction: Instruction, + caller_program_id: Pubkey, +) -> (Instruction, (Pubkey, Account)) { + let cpi_sysvar_account = create_cpi_instructions_sysvar_account(caller_program_id); + + // Use the REAL sysvar address (unlike Wormhole attack which uses fake) + ( + instruction, + (solana_sdk::sysvar::instructions::ID, cpi_sysvar_account), + ) +} + +/// Expected error for CPI rejection (`UnauthorizedCaller` from `reject_cpi`) +/// This is for instructions that DON'T map the error (like router/gmp callback instructions) +pub fn expect_cpi_rejection_error() -> mollusk_svm::result::Check<'static> { + use solana_ibc_types::CpiValidationError; + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ERROR_CODE_OFFSET + CpiValidationError::UnauthorizedCaller as u32, + )) +} + +/// Expected error for CPI rejection in access-manager instructions +/// These instructions map `CpiValidationError::UnauthorizedCaller` to `AccessManagerError::CpiNotAllowed` +pub fn expect_access_manager_cpi_rejection_error() -> mollusk_svm::result::Check<'static> { + use crate::errors::AccessManagerError; + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::CpiNotAllowed as u32, + )) +} + +/// Create instructions sysvar account for direct call (alias for `create_instructions_sysvar_account`) +pub fn create_instructions_sysvar_account_with_caller( + caller_program_id: Pubkey, +) -> (Pubkey, Account) { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &caller_program_id, + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + ( + solana_sdk::sysvar::instructions::ID, + Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +/// Deserialize account data into typed struct +pub fn get_account_data(account: &Account) -> T { + anchor_lang::AccountDeserialize::try_deserialize(&mut &account.data[..]) + .expect("Failed to deserialize account") +} + +/// Serialize account struct to data +pub fn serialize_account( + account: &T, +) -> Vec { + let mut data = T::DISCRIMINATOR.to_vec(); + account.serialize(&mut data).unwrap(); + data +} + +/// Create rent sysvar account +pub fn create_rent_sysvar_account() -> (Pubkey, Account) { + ( + solana_sdk::sysvar::rent::ID, + Account { + lamports: 1_000_000, + data: vec![0; 17], // Rent sysvar is 17 bytes + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +/// Create clock sysvar account +pub fn create_clock_sysvar_account() -> (Pubkey, Account) { + ( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1_000_000, + data: vec![0; 40], // Clock sysvar is 40 bytes + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + }, + ) +} diff --git a/programs/solana/programs/access-manager/src/types.rs b/programs/solana/programs/access-manager/src/types.rs new file mode 100644 index 000000000..b99063850 --- /dev/null +++ b/programs/solana/programs/access-manager/src/types.rs @@ -0,0 +1,8 @@ +use anchor_lang::prelude::*; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, InitSpace, Debug)] +pub struct RoleData { + pub role_id: u64, + #[max_len(16)] + pub members: Vec, +} diff --git a/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs b/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs index 0e765fb5e..49025f4ee 100644 --- a/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs +++ b/programs/solana/programs/gmp-counter-app/src/instructions/mod.rs @@ -1,5 +1,7 @@ pub mod counter_ops; pub mod initialize; +pub mod process_test_payload; pub use counter_ops::*; pub use initialize::*; +pub use process_test_payload::*; diff --git a/programs/solana/programs/gmp-counter-app/src/instructions/process_test_payload.rs b/programs/solana/programs/gmp-counter-app/src/instructions/process_test_payload.rs new file mode 100644 index 000000000..5a312905e --- /dev/null +++ b/programs/solana/programs/gmp-counter-app/src/instructions/process_test_payload.rs @@ -0,0 +1,34 @@ +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::program::set_return_data; + +#[derive(Accounts)] +pub struct ProcessTestPayload<'info> { + #[account( + seeds = [CounterAppState::SEED], + bump = app_state.bump + )] + pub app_state: Account<'info, CounterAppState>, + + #[account(mut)] + pub payer: Signer<'info>, + + pub system_program: Program<'info, System>, +} + +pub fn process_test_payload<'info>( + ctx: Context<'_, '_, '_, 'info, ProcessTestPayload<'info>>, + data: Vec, +) -> Result<()> { + let num_remaining_accounts = ctx.remaining_accounts.len(); + + msg!( + "GMP Counter App: Processed test payload - data size: {} bytes, remaining accounts: {}", + data.len(), + num_remaining_accounts + ); + + set_return_data(b"ok"); + + Ok(()) +} diff --git a/programs/solana/programs/gmp-counter-app/src/lib.rs b/programs/solana/programs/gmp-counter-app/src/lib.rs index 216ca53ac..edb55f58e 100644 --- a/programs/solana/programs/gmp-counter-app/src/lib.rs +++ b/programs/solana/programs/gmp-counter-app/src/lib.rs @@ -18,6 +18,7 @@ use instructions::*; /// - `increment`: Increment a user's counter (called by GMP) /// - `decrement`: Decrement a user's counter (called by GMP) /// - `get_counter`: Get a user's current counter value +/// - `process_test_payload`: Test instruction for GMP scenarios (large payloads, many accounts) /// #[program] pub mod gmp_counter_app { @@ -43,4 +44,13 @@ pub mod gmp_counter_app { pub fn get_counter(ctx: Context, user: Pubkey) -> Result<()> { instructions::get_counter(ctx, user) } + + /// Process test payload (for GMP testing: large payloads, many accounts) + /// Always returns "ok" as acknowledgement + pub fn process_test_payload<'info>( + ctx: Context<'_, '_, '_, 'info, ProcessTestPayload<'info>>, + data: Vec, + ) -> Result<()> { + instructions::process_test_payload(ctx, data) + } } diff --git a/programs/solana/programs/ics07-tendermint/Cargo.toml b/programs/solana/programs/ics07-tendermint/Cargo.toml index a4d6983dc..49f0ab691 100644 --- a/programs/solana/programs/ics07-tendermint/Cargo.toml +++ b/programs/solana/programs/ics07-tendermint/Cargo.toml @@ -27,6 +27,7 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } +access-manager = { workspace = true, features = ["cpi"] } ics25-handler.workspace = true solana-ibc-types.workspace = true diff --git a/programs/solana/programs/ics07-tendermint/README.md b/programs/solana/programs/ics07-tendermint/README.md index 333cdd8a7..b5c0262d1 100644 --- a/programs/solana/programs/ics07-tendermint/README.md +++ b/programs/solana/programs/ics07-tendermint/README.md @@ -33,15 +33,18 @@ The client implements a mandatory chunked upload mechanism for all header update ### Core IBC Instructions #### `initialize` + Initializes a new Tendermint light client instance for a specific chain. Multiple clients can be initialized to track different Tendermint-based chains simultaneously. **Parameters:** + - `chain_id`: The unique chain identifier (e.g., "cosmoshub-4", "osmosis-1", "noble-1") - `latest_height`: Initial trusted height - `client_state`: Initial client configuration (trust level, periods, etc.) - `consensus_state`: Initial trusted consensus state **Accounts:** + - `client_state` (init): PDA storing client configuration, derived from chain_id - `consensus_state_store` (init): PDA storing consensus state at height - `payer` (signer, mut): Account paying for initialization @@ -54,28 +57,34 @@ Initializes a new Tendermint light client instance for a specific chain. Multipl Since Tendermint headers always exceed Solana's transaction size limits, all header updates must use the chunked upload system described below. #### `create_metadata` + Creates metadata for a header upload. This instruction must be called once before uploading chunks. **Parameters:** + - `chain_id`: Target chain identifier - `target_height`: Height being updated to - `total_chunks`: Total number of chunks expected - `header_commitment`: Keccak hash of the complete header **Accounts:** + - `metadata` (init): PDA for header metadata - `client_state`: Validates chain exists - `submitter` (signer, mut): Relayer creating metadata and paying rent - `system_program`: System program **Notes:** + - Must be called exactly once per upload attempt - Creates a new metadata account for tracking the upload #### `upload_header_chunk` + Uploads a single chunk of a large header. Requires metadata to be created first via `create_metadata`. **Parameters:** + - `params`: UploadChunkParams containing: - `chain_id`: Target chain - `target_height`: Height being updated to @@ -84,6 +93,7 @@ Uploads a single chunk of a large header. Requires metadata to be created first - `chunk_hash`: Keccak hash of the chunk data for integrity verification **Accounts:** + - `chunk` (init_if_needed): PDA for this specific chunk - `metadata` (mut): PDA for header metadata (must already exist) - `client_state`: Validates chain exists @@ -97,9 +107,11 @@ Uploads a single chunk of a large header. Requires metadata to be created first **Parallel Upload**: After metadata is created, all chunks can be uploaded in parallel transactions for faster throughput. Each chunk upload is independent. #### `assemble_and_update_client` + Assembles uploaded chunks into a complete header and updates the client. **Accounts:** + - `client_state` (mut): Client being updated - `metadata` (mut, close): Header metadata (closed after success) - `trusted_consensus_state`: Consensus at trusted height @@ -110,6 +122,7 @@ Assembles uploaded chunks into a complete header and updates the client. - Remaining accounts: Chunk accounts in order (all closed after success) **Process:** + 1. Validates all chunks are present and match commitment 2. Reconstructs complete header from chunks 3. Verifies header against trusted state @@ -117,14 +130,17 @@ Assembles uploaded chunks into a complete header and updates the client. 5. Closes all temporary accounts, returning rent to submitter #### `cleanup_incomplete_upload` + Allows relayers to reclaim rent from failed or abandoned uploads. **Parameters:** + - `chain_id`: Chain identifier - `cleanup_height`: Height of upload to clean - `submitter`: Original submitter address **Accounts:** + - `client_state`: Validates chain exists - `metadata` (mut, close): Metadata to close - `submitter_account` (signer, mut): Must be original submitter @@ -135,34 +151,43 @@ Allows relayers to reclaim rent from failed or abandoned uploads. ### Verification Instructions #### `verify_membership` + Verifies a key-value pair exists in the counterparty chain's state. **Parameters:** + - `msg`: MembershipMsg with proof details **Accounts:** + - `client_state`: Client configuration - `consensus_state_at_height`: Consensus state at proof height #### `verify_non_membership` + Verifies a key does not exist in the counterparty chain's state. **Parameters:** + - `msg`: MembershipMsg with proof details **Accounts:** + - `client_state`: Client configuration - `consensus_state_at_height`: Consensus state at proof height ### Misbehaviour Handling #### `submit_misbehaviour` + Submits evidence of misbehaviour to freeze the client. **Parameters:** + - `msg`: MisbehaviourMsg with conflicting headers **Accounts:** + - `client_state` (mut): Client to potentially freeze - `trusted_consensus_state_1`: First trusted state - `trusted_consensus_state_2`: Second trusted state @@ -179,6 +204,7 @@ header_metadata: [b"header_metadata", submitter, chain_id, height_bytes] ``` This PDA structure ensures that: + - Each chain has its own isolated client state - Consensus states are chain-specific - Upload operations cannot interfere across different chains @@ -202,6 +228,7 @@ This PDA structure ensures that: ### Parallel Upload Optimization For maximum throughput with large headers: + 1. Call `create_metadata` to initialize metadata 2. Upload all chunks in parallel transactions 3. Wait for all confirmations @@ -228,6 +255,7 @@ This parallel approach can reduce upload time from `n * block_time` to `2 * bloc ## Testing The implementation includes comprehensive tests for: + - Happy path updates with real Tendermint fixtures - Chunked upload and assembly - Error conditions (missing chunks, wrong order, corruption) @@ -236,6 +264,7 @@ The implementation includes comprehensive tests for: - Multi-relayer scenarios Run tests: + ```bash cargo test --package ics07-tendermint ``` @@ -243,6 +272,7 @@ cargo test --package ics07-tendermint ## Gas/Compute Costs Approximate compute units per operation: + - `initialize`: ~50k CU - `upload_header_chunk`: ~30k CU per chunk - `assemble_and_update_client`: ~200k CU (includes verification) @@ -260,6 +290,7 @@ This implementation uses `brine-ed25519` for on-chain Ed25519 signature verifica **Why Ed25519Program Doesn't Work for IBC:** Solana's Ed25519Program is a precompile that verifies signatures included as Ed25519Program instructions in the current transaction. However, IBC light clients verify signatures from **external blockchain data** (Tendermint headers from Cosmos chains). These signatures: + - Come from Tendermint validators signing blocks on another chain - Are embedded in header data uploaded via `upload_header_chunk` - Cannot be reformulated as Ed25519Program instructions in the Solana transaction @@ -271,6 +302,7 @@ This implementation already uses multi-transaction chunking to upload large head The key insight: **chunking is for DATA TRANSPORT, not signature verification**. The signatures are embedded INSIDE the serialized header data and can only be verified AFTER the header is fully assembled and deserialized in `assemble_and_update_client`. Using Ed25519Program would require: + ``` Current approach (brine-ed25519): 1. Upload header chunks in PARALLEL (N transactions, ~2 block times) @@ -286,6 +318,7 @@ Hypothetical Ed25519Program approach: ``` This would create **double multi-transaction coordination** (chunks + signature verifications), with: + - **Significantly slower updates**: Current system uses parallel chunk upload (~2 blocks). Ed25519Program would add M sequential signature verification transactions, increasing latency by 4-8 seconds per update - Additional state storage for verification results (rent costs likely exceed CU savings) - More complex atomicity concerns (chunks AND signature verifications must all succeed) @@ -296,10 +329,12 @@ The existing chunking system actually **strengthens** the case for brine-ed25519 **Alternatives Considered:** 1. **Ed25519Program (native precompile)** - FREE compute units + - ❌ Incompatible with external signature verification - Only works for signatures that are part of the transaction instruction set 2. **brine-ed25519 (on-chain library)** - ~30k CU per signature ✅ **CHOSEN** + - ✅ Can verify any signature from external data - Uses optimized curve operations for efficiency - Typical update: ~200k CU total (10-20 signatures for 2/3 trust threshold) @@ -316,11 +351,11 @@ The existing chunking system actually **strengthens** the case for brine-ed25519 **Comparison to Other Implementations:** -| Implementation | Approach | Verification Cost | -|----------------|----------|-------------------| -| **Ethereum** | SP1 ZK Proofs | ~230k gas (~$0.50-5.00 USD) | -| **Solana** | On-chain verification (brine-ed25519) | ~200k CU (~$0.00001 USD) | -| **Cosmos** | Native IBC with on-chain verification | ~300k gas (~$0.003 USD) | +| Implementation | Approach | Verification Cost | +| -------------- | ------------------------------------- | --------------------------- | +| **Ethereum** | SP1 ZK Proofs | ~230k gas (~$0.50-5.00 USD) | +| **Solana** | On-chain verification (brine-ed25519) | ~200k CU (~$0.00001 USD) | +| **Cosmos** | Native IBC with on-chain verification | ~300k gas (~$0.003 USD) | The on-chain verification approach makes Solana one of the most cost-efficient platforms for IBC light client verification, despite not being able to use the free Ed25519Program precompile. @@ -351,4 +386,3 @@ For implementation details, see the `SolanaSignatureVerifier` in `packages/tende 3. Check client not frozen before relying on proofs 4. Monitor for client updates 5. **Multi-Chain Applications**: Can interact with multiple chains by referencing different chain_id PDAs - diff --git a/programs/solana/programs/ics07-tendermint/src/error.rs b/programs/solana/programs/ics07-tendermint/src/error.rs index e8dad3d6d..9c1c1cf01 100644 --- a/programs/solana/programs/ics07-tendermint/src/error.rs +++ b/programs/solana/programs/ics07-tendermint/src/error.rs @@ -66,6 +66,10 @@ pub enum ErrorCode { #[msg("Chunk data too large: exceeds maximum chunk size")] ChunkDataTooLarge, + // Access control errors + #[msg("Unauthorized: caller does not have required role")] + UnauthorizedRole, + // Other errors #[msg("Serialization error: failed to serialize/deserialize data")] SerializationError, diff --git a/programs/solana/programs/ics07-tendermint/src/instructions.rs b/programs/solana/programs/ics07-tendermint/src/instructions.rs index 38f7d04f6..6c8a7ac09 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions.rs @@ -3,6 +3,7 @@ pub mod assemble_and_update_client; pub mod cleanup_incomplete_misbehaviour; pub mod cleanup_incomplete_upload; pub mod initialize; +pub mod set_access_manager; pub mod upload_header_chunk; pub mod upload_misbehaviour_chunk; pub mod verify_membership; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs index 02e0faa8e..67d3c8779 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour.rs @@ -10,6 +10,17 @@ pub fn assemble_and_submit_misbehaviour( mut ctx: Context, client_id: String, ) -> Result<()> { + // Performs: CPI rejection + signer verification + role check + // Ethereum: SP1ICS07Tendermint.sol:205 - misbehaviour restricted to PROOF_SUBMITTER_ROLE + // Note: Solana uses RELAYER_ROLE instead (accepted difference) + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::RELAYER_ROLE, + &ctx.accounts.submitter, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + require!( !ctx.accounts.client_state.is_frozen(), ErrorCode::ClientAlreadyFrozen diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs index b46fb6f4d..089476820 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_submit_misbehaviour/tests.rs @@ -92,6 +92,7 @@ fn setup_test_accounts(config: TestSetupConfig) -> TestAccounts { revision_number: 0, revision_height: 200, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; @@ -119,6 +120,16 @@ fn setup_test_accounts(config: TestSetupConfig) -> TestAccounts { }, )); + // Add access manager account + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + accounts.push((access_manager_pda, access_manager_account)); + if with_valid_consensus_states { let consensus_state_1 = ConsensusState { timestamp: 1_000_000_000_000_000_000, // nanoseconds @@ -217,6 +228,12 @@ fn setup_test_accounts(config: TestSetupConfig) -> TestAccounts { }, )); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let mut chunk_pdas = vec![]; if with_chunks { const CHUNK_SIZE: usize = 700; @@ -275,11 +292,16 @@ fn create_assemble_instruction(test_accounts: &TestAccounts, client_id: &str) -> client_id: client_id.to_string(), }; + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let mut account_metas = vec![ AccountMeta::new(test_accounts.client_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(test_accounts.trusted_consensus_state_1_pda, false), AccountMeta::new_readonly(test_accounts.trusted_consensus_state_2_pda, false), AccountMeta::new(test_accounts.submitter, true), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), ]; for chunk_pda in &test_accounts.chunk_pdas { @@ -361,9 +383,9 @@ fn test_assemble_and_submit_misbehaviour_invalid_protobuf() { let instruction = create_assemble_instruction(&test_accounts, chain_id); let mollusk = Mollusk::new(&crate::ID, PROGRAM_BINARY_PATH); - let checks = vec![ - Check::err(anchor_lang::prelude::ProgramError::Custom(0x1778)), // InvalidHeader - ]; + let checks = vec![Check::err( + anchor_lang::error::Error::from(ErrorCode::InvalidHeader).into(), + )]; mollusk.process_and_validate_instruction(&instruction, &test_accounts.accounts, &checks); } diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs index 3871004ad..d4e804f8c 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client.rs @@ -14,6 +14,17 @@ pub fn assemble_and_update_client( chain_id: String, target_height: u64, ) -> Result { + // Performs: CPI rejection + signer verification + role check + // Ethereum: SP1ICS07Tendermint.sol:111 - updateClient restricted to PROOF_SUBMITTER_ROLE + // Note: Solana uses RELAYER_ROLE instead (accepted difference) + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::RELAYER_ROLE, + &ctx.accounts.submitter, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + require!( !ctx.accounts.client_state.is_frozen(), ErrorCode::ClientFrozen diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs index 1eb43f2c1..9e8107177 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/assemble_and_update_client/tests.rs @@ -107,6 +107,7 @@ fn get_chunk_pdas( } struct AssembleInstructionParams { + access_manager_pda: Pubkey, client_state_pda: Pubkey, trusted_consensus_state_pda: Pubkey, new_consensus_state_pda: Pubkey, @@ -119,10 +120,12 @@ struct AssembleInstructionParams { fn create_assemble_instruction(params: AssembleInstructionParams) -> Instruction { let mut account_metas = vec![ AccountMeta::new(params.client_state_pda, false), + AccountMeta::new_readonly(params.access_manager_pda, false), AccountMeta::new_readonly(params.trusted_consensus_state_pda, false), AccountMeta::new(params.new_consensus_state_pda, false), AccountMeta::new(params.submitter, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), ]; // Add chunk accounts @@ -154,6 +157,11 @@ fn test_successful_assembly_and_update() { let chain_id = &client_state.chain_id; let target_height = update_message.new_height; let submitter = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + // Setup access control + let (access_manager_pda, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account(relayer, vec![relayer]); // Split the real header into chunks let chunk_size = client_message_bytes.len() / 3 + 1; @@ -196,6 +204,7 @@ fn test_successful_assembly_and_update() { let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, trusted_height); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -217,10 +226,12 @@ fn test_successful_assembly_and_update() { // Setup accounts for instruction let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, client_state_account), (trusted_consensus_pda, trusted_consensus_account), (consensus_state_pda, Account::default()), (submitter, submitter_account), + (relayer, create_submitter_account(1_000_000_000)), (payer, create_submitter_account(1_000_000_000)), keyed_account_for_system_program(), ]; @@ -253,6 +264,12 @@ fn test_successful_assembly_and_update() { }, )); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // With real fixtures, this should either succeed or fail with a known error @@ -291,6 +308,11 @@ fn test_assembly_with_corrupted_chunk() { let chain_id = "test-chain"; let target_height = 100u64; let submitter = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + // Setup access control + let (_access_manager_pda, _access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account(relayer, vec![relayer]); let (_, chunks) = create_test_header_and_chunks(2); @@ -309,11 +331,16 @@ fn test_assembly_with_corrupted_chunk() { // Create metadata // Get chunk PDAs let chunk_pdas = get_chunk_pdas(&submitter, chain_id, target_height, 2); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + // Create instruction let payer = Pubkey::new_unique(); let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, 90); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -323,8 +350,16 @@ fn test_assembly_with_corrupted_chunk() { target_height, }); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + // Setup accounts with corrupted second chunk let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, create_client_state_account(chain_id, 90)), ( trusted_consensus_pda, @@ -344,6 +379,32 @@ fn test_assembly_with_corrupted_chunk() { corrupted_data[0] ^= 0xFF; // Flip bits to corrupt accounts.push((chunk_pdas[1], create_chunk_account(corrupted_data))); + // Add Clock sysvar for update client validation + let clock = solana_sdk::sysvar::clock::Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + accounts.push(( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1, + data: clock_data, + owner: solana_sdk::native_loader::ID, + executable: false, + rent_epoch: 0, + }, + )); + + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // When chunk data is corrupted, the header deserialization or validation will fail @@ -376,10 +437,15 @@ fn test_assembly_wrong_submitter() { &crate::ID, ); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let payer = Pubkey::new_unique(); let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, 90); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -389,7 +455,15 @@ fn test_assembly_wrong_submitter() { target_height, }); + // Setup access manager with wrong_submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + wrong_submitter, + vec![wrong_submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, create_client_state_account(chain_id, 90)), ( trusted_consensus_pda, @@ -406,6 +480,32 @@ fn test_assembly_wrong_submitter() { accounts.push((*chunk_pda, create_chunk_account(chunks[i].clone()))); } + // Add Clock sysvar for update client validation + let clock = solana_sdk::sysvar::clock::Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + accounts.push(( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1, + data: clock_data, + owner: solana_sdk::native_loader::ID, + executable: false, + rent_epoch: 0, + }, + )); + + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // When the submitter is wrong, the PDA validation fails because chunks were created @@ -441,10 +541,15 @@ fn test_assembly_chunks_in_wrong_order() { // Pass chunks in wrong order (2, 0, 1 instead of 0, 1, 2) let wrong_order_pdas = vec![chunk_pdas[2], chunk_pdas[0], chunk_pdas[1]]; + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let payer = Pubkey::new_unique(); let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, 90); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -454,7 +559,15 @@ fn test_assembly_chunks_in_wrong_order() { target_height, }); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, create_client_state_account(chain_id, 90)), ( trusted_consensus_pda, @@ -471,6 +584,32 @@ fn test_assembly_chunks_in_wrong_order() { accounts.push((chunk_pdas[0], create_chunk_account(chunks[0].clone()))); accounts.push((chunk_pdas[1], create_chunk_account(chunks[1].clone()))); + // Add Clock sysvar for update client validation + let clock = solana_sdk::sysvar::clock::Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + accounts.push(( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1, + data: clock_data, + owner: solana_sdk::native_loader::ID, + executable: false, + rent_epoch: 0, + }, + )); + + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // When chunks are in wrong order, PDA validation fails first @@ -488,6 +627,11 @@ fn test_rent_reclaim_after_assembly() { let chain_id = "test-chain"; let target_height = 100u64; let submitter = Pubkey::new_unique(); + let relayer = Pubkey::new_unique(); + + // Setup access control + let (_access_manager_pda, _access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account(relayer, vec![relayer]); let (_, chunks) = create_test_header_and_chunks(2); @@ -509,10 +653,15 @@ fn test_rent_reclaim_after_assembly() { // Submitter account let submitter_account = create_submitter_account(initial_balance); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let payer = Pubkey::new_unique(); let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, 90); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -522,7 +671,15 @@ fn test_rent_reclaim_after_assembly() { target_height, }); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, create_client_state_account(chain_id, 90)), ( trusted_consensus_pda, @@ -539,6 +696,32 @@ fn test_rent_reclaim_after_assembly() { accounts.push((*chunk_pda, create_chunk_account(chunks[i].clone()))); } + // Add Clock sysvar for update client validation + let clock = solana_sdk::sysvar::clock::Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + accounts.push(( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1, + data: clock_data, + owner: solana_sdk::native_loader::ID, + executable: false, + rent_epoch: 0, + }, + )); + + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // With real fixtures, this should either succeed or fail with a known error @@ -593,6 +776,10 @@ fn test_assemble_and_update_client_happy_path() { ); let chunk_pdas = get_chunk_pdas(&submitter, chain_id, target_height, num_chunks); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + // Create existing client state with proper data let mut client_state_account = create_client_state_account(chain_id, client_state.latest_height.revision_height); @@ -614,6 +801,7 @@ fn test_assemble_and_update_client_happy_path() { let payer = Pubkey::new_unique(); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -635,7 +823,15 @@ fn test_assemble_and_update_client_happy_path() { }; let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, client_state_account), (trusted_consensus_pda, trusted_consensus_account), (consensus_state_pda, Account::default()), @@ -654,6 +850,12 @@ fn test_assemble_and_update_client_happy_path() { ), ]; + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + for (i, chunk_pda) in chunk_pdas.iter().enumerate() { accounts.push((*chunk_pda, create_chunk_account(chunks[i].clone()))); } @@ -765,7 +967,12 @@ fn test_assemble_with_frozen_client() { &crate::ID, ); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -784,7 +991,15 @@ fn test_assemble_with_frozen_client() { .try_serialize(&mut trusted_consensus_data) .unwrap(); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, frozen_client), ( trusted_consensus_pda, @@ -811,6 +1026,12 @@ fn test_assemble_with_frozen_client() { let (clock_pubkey, clock_account) = create_clock_account(clock_timestamp); accounts.push((clock_pubkey, clock_account)); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + // Increase compute budget for processing real data let mut mollusk_with_budget = mollusk; mollusk_with_budget.compute_budget.compute_unit_limit = 2_000_000; @@ -903,7 +1124,12 @@ fn test_assemble_with_existing_consensus_state() { .try_serialize(&mut trusted_consensus_data) .unwrap(); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -913,7 +1139,15 @@ fn test_assemble_with_existing_consensus_state() { target_height, }); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ + (access_manager_pda, access_manager_account), (client_state_pda, client_account), ( trusted_consensus_pda, @@ -940,6 +1174,12 @@ fn test_assemble_with_existing_consensus_state() { let (clock_pubkey, clock_account) = create_clock_account(clock_timestamp); accounts.push((clock_pubkey, clock_account)); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + // Increase compute budget for this complex operation (header verification is expensive) let mut mollusk_with_budget = mollusk; mollusk_with_budget.compute_budget.compute_unit_limit = 20_000_000; @@ -1013,10 +1253,15 @@ fn test_assemble_with_invalid_header_after_assembly() { ); let chunk_pdas = get_chunk_pdas(&submitter, chain_id, target_height, 2); - let payer = Pubkey::new_unique(); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + let _payer = Pubkey::new_unique(); let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, 90); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -1026,21 +1271,54 @@ fn test_assemble_with_invalid_header_after_assembly() { target_height, }); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ (client_state_pda, create_client_state_account(chain_id, 90)), + (access_manager_pda, access_manager_account), ( trusted_consensus_pda, create_consensus_state_account([0; 32], [0; 32], 0), ), (consensus_state_pda, Account::default()), (submitter, create_submitter_account(10_000_000_000)), - (payer, create_submitter_account(1_000_000_000)), keyed_account_for_system_program(), ]; accounts.push((chunk_pdas[0], create_chunk_account(chunk1))); accounts.push((chunk_pdas[1], create_chunk_account(chunk2))); + // Add Clock sysvar for update client validation + let clock = solana_sdk::sysvar::clock::Clock { + slot: 0, + epoch_start_timestamp: 0, + epoch: 0, + leader_schedule_epoch: 0, + unix_timestamp: 0, + }; + let clock_data = bincode::serialize(&clock).expect("Failed to serialize Clock for test"); + accounts.push(( + solana_sdk::sysvar::clock::ID, + Account { + lamports: 1, + data: clock_data, + owner: solana_sdk::native_loader::ID, + executable: false, + rent_epoch: 0, + }, + )); + + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // Should fail during header validation after assembly @@ -1080,11 +1358,16 @@ fn test_assemble_updates_latest_height() { ); let chunk_pdas = get_chunk_pdas(&submitter, chain_id, target_height, 2); - let payer = Pubkey::new_unique(); + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + let _payer = Pubkey::new_unique(); let trusted_height = update_message.trusted_height; let trusted_consensus_pda = derive_consensus_state_pda(&client_state_pda, trusted_height); let instruction = create_assemble_instruction(AssembleInstructionParams { + access_manager_pda, client_state_pda, trusted_consensus_state_pda: trusted_consensus_pda, new_consensus_state_pda: consensus_state_pda, @@ -1110,12 +1393,19 @@ fn test_assemble_updates_latest_height() { consensus_state.timestamp, ); + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + let mut accounts = vec![ (client_state_pda, initial_client), + (access_manager_pda, access_manager_account), (trusted_consensus_pda, trusted_consensus_account), (consensus_state_pda, Account::default()), (submitter, create_submitter_account(10_000_000_000)), - (payer, create_submitter_account(1_000_000_000)), keyed_account_for_system_program(), ]; @@ -1145,6 +1435,12 @@ fn test_assemble_updates_latest_height() { }, )); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + let result = mollusk.process_instruction(&instruction, &accounts); // With real fixtures, verify the client state update @@ -1268,12 +1564,24 @@ fn test_assemble_and_update_with_invalid_signature() { chunk_accounts.push((chunk_pda, chunk_account)); } + // Access manager PDA + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + // Setup access manager with submitter as relayer + let (_, access_manager_account) = + crate::test_helpers::access_control::create_access_manager_account( + submitter, + vec![submitter], + ); + // Prepare accounts let mut accounts = vec![ ( client_state_pda, create_client_state_account(chain_id, trusted_height), ), + (access_manager_pda, access_manager_account), ( trusted_consensus_pda, create_consensus_state_account( @@ -1320,10 +1628,12 @@ fn test_assemble_and_update_with_invalid_signature() { // Create instruction let mut account_metas = vec![ AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(trusted_consensus_pda, false), AccountMeta::new(new_consensus_pda, false), AccountMeta::new(submitter, true), AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + AccountMeta::new_readonly(anchor_lang::solana_program::sysvar::instructions::ID, false), ]; // Add chunk accounts to instruction @@ -1362,6 +1672,12 @@ fn test_assemble_and_update_with_invalid_signature() { }, )); + // Add instructions sysvar for CPI validation + accounts.push(( + anchor_lang::solana_program::sysvar::instructions::ID, + crate::test_helpers::create_instructions_sysvar_account(), + )); + // Need higher compute budget for signature verification let mut mollusk_with_budget = setup_mollusk(); mollusk_with_budget.compute_budget.compute_unit_limit = 10_000_000; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_misbehaviour/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_misbehaviour/tests.rs index 2975e4c78..91df540fb 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_misbehaviour/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_misbehaviour/tests.rs @@ -74,6 +74,7 @@ fn setup_test_accounts_with_chunks( revision_number: 0, revision_height: 100, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; @@ -280,6 +281,7 @@ fn test_cleanup_with_missing_chunks() { revision_number: 0, revision_height: 100, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_upload/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_upload/tests.rs index 961c67c93..e9ac37f50 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_upload/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/cleanup_incomplete_upload/tests.rs @@ -79,6 +79,7 @@ fn setup_test_accounts_with_chunks( revision_number: 0, revision_height: 100, // Higher than cleanup_height }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; @@ -300,6 +301,7 @@ fn test_cleanup_with_missing_chunks() { revision_number: 0, revision_height: 100, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/initialize.rs b/programs/solana/programs/ics07-tendermint/src/instructions/initialize.rs index 626baca13..bfddc92a5 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/initialize.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/initialize.rs @@ -5,9 +5,20 @@ use anchor_lang::prelude::*; pub fn initialize( ctx: Context, + chain_id: String, + latest_height: u64, client_state: ClientState, consensus_state: ConsensusState, ) -> Result<()> { + // NOTE: chain_id is used in the #[instruction] attribute for account validation + // but we also validate it matches the client_state for safety + require!(client_state.chain_id == chain_id, ErrorCode::InvalidChainId); + + require!( + client_state.latest_height.revision_height == latest_height, + ErrorCode::InvalidHeight + ); + require!(!client_state.chain_id.is_empty(), ErrorCode::InvalidChainId); require!( @@ -36,6 +47,7 @@ pub fn initialize( let client_state_account = &mut ctx.accounts.client_state; let latest_height = client_state.latest_height; + client_state_account.set_inner(client_state); let consensus_state_store = &mut ctx.accounts.consensus_state_store; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/set_access_manager.rs b/programs/solana/programs/ics07-tendermint/src/instructions/set_access_manager.rs new file mode 100644 index 000000000..79cb3ce11 --- /dev/null +++ b/programs/solana/programs/ics07-tendermint/src/instructions/set_access_manager.rs @@ -0,0 +1,244 @@ +use anchor_lang::prelude::*; +use solana_ibc_types::events::AccessManagerUpdated; + +pub fn set_access_manager( + ctx: Context, + _chain_id: String, + new_access_manager: Pubkey, +) -> Result<()> { + let old_access_manager = ctx.accounts.client_state.access_manager; + + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ADMIN_ROLE, + &ctx.accounts.admin, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + + ctx.accounts.client_state.access_manager = new_access_manager; + + emit!(AccessManagerUpdated { + old_access_manager, + new_access_manager, + }); + + msg!( + "Access manager for client {} updated from {} to {}", + ctx.accounts.client_state.chain_id, + old_access_manager, + new_access_manager + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_helpers::{fixtures::load_primary_fixtures, PROGRAM_BINARY_PATH}; + use crate::types::ClientState; + use access_manager::AccessManagerError; + use anchor_lang::InstructionData; + use mollusk_svm::result::Check; + use mollusk_svm::Mollusk; + use solana_ibc_types::roles; + use solana_sdk::account::Account as SolanaAccount; + use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::pubkey::Pubkey; + + const ANCHOR_ERROR_OFFSET: u32 = 6000; + + fn create_signer_account() -> SolanaAccount { + SolanaAccount { + lamports: 1_000_000_000, + data: vec![], + owner: solana_sdk::system_program::ID, + executable: false, + rent_epoch: 0, + } + } + + fn create_client_state_account(chain_id: &str, access_manager: Pubkey) -> SolanaAccount { + use anchor_lang::AccountSerialize; + + let (client_state, _, _) = load_primary_fixtures(); + let mut client_state = client_state; + client_state.chain_id = chain_id.to_string(); + client_state.access_manager = access_manager; + + let mut data = vec![0u8; 8 + ClientState::INIT_SPACE]; + client_state.try_serialize(&mut &mut data[..]).unwrap(); + + SolanaAccount { + lamports: 10_000_000, + data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + } + } + + fn create_access_manager_account(admin: Pubkey, role: u64) -> SolanaAccount { + use access_manager::state::AccessManager; + use access_manager::types::RoleData; + use anchor_lang::AccountSerialize; + + let access_manager = AccessManager { + roles: vec![RoleData { + role_id: role, + members: vec![admin], + }], + }; + + let mut data = vec![0u8; 8 + 10000]; // Enough space + access_manager.try_serialize(&mut &mut data[..]).unwrap(); + + SolanaAccount { + lamports: 10_000_000, + data, + owner: access_manager::ID, + executable: false, + rent_epoch: 0, + } + } + + fn create_instructions_sysvar_account() -> (Pubkey, SolanaAccount) { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &crate::ID, + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + ( + solana_sdk::sysvar::instructions::ID, + SolanaAccount { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + }, + ) + } + + #[test] + fn test_set_access_manager_success() { + let chain_id = "test-chain"; + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (client_state_pda, _) = + Pubkey::find_program_address(&[ClientState::SEED, chain_id.as_bytes()], &crate::ID); + + let (access_manager_pda, _) = Pubkey::find_program_address( + &[access_manager::state::AccessManager::SEED], + &access_manager::ID, + ); + + let instruction_data = crate::instruction::SetAccessManager { + chain_id: chain_id.to_string(), + new_access_manager, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + let client_state_account = create_client_state_account(chain_id, access_manager::ID); + let access_manager_account = create_access_manager_account(admin, roles::ADMIN_ROLE); + let (instructions_sysvar_pubkey, instructions_sysvar_account) = + create_instructions_sysvar_account(); + + let accounts = vec![ + (client_state_pda, client_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + (instructions_sysvar_pubkey, instructions_sysvar_account), + ]; + + let mollusk = Mollusk::new(&crate::ID, PROGRAM_BINARY_PATH); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let client_state_account = result + .get_account(&client_state_pda) + .expect("Client state account not found"); + let client_state: ClientState = + ClientState::try_deserialize(&mut &client_state_account.data[..]) + .expect("Failed to deserialize client state"); + + assert_eq!(client_state.access_manager, new_access_manager); + } + + #[test] + fn test_set_access_manager_not_admin() { + let chain_id = "test-chain"; + let admin = Pubkey::new_unique(); + let non_admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (client_state_pda, _) = + Pubkey::find_program_address(&[ClientState::SEED, chain_id.as_bytes()], &crate::ID); + + let (access_manager_pda, _) = Pubkey::find_program_address( + &[access_manager::state::AccessManager::SEED], + &access_manager::ID, + ); + + let instruction_data = crate::instruction::SetAccessManager { + chain_id: chain_id.to_string(), + new_access_manager, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(client_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(non_admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + let client_state_account = create_client_state_account(chain_id, access_manager::ID); + let access_manager_account = create_access_manager_account(admin, roles::ADMIN_ROLE); + let (instructions_sysvar_pubkey, instructions_sysvar_account) = + create_instructions_sysvar_account(); + + let accounts = vec![ + (client_state_pda, client_state_account), + (access_manager_pda, access_manager_account), + (non_admin, create_signer_account()), + (instructions_sysvar_pubkey, instructions_sysvar_account), + ]; + + let mollusk = Mollusk::new(&crate::ID, PROGRAM_BINARY_PATH); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } +} diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/upload_header_chunk/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/upload_header_chunk/tests.rs index 8411a2d04..8c3afbbf9 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/upload_header_chunk/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/upload_header_chunk/tests.rs @@ -85,6 +85,7 @@ fn setup_test_accounts( revision_number: 0, revision_height: 100, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; @@ -408,6 +409,7 @@ fn test_upload_chunk_with_frozen_client_fails() { revision_number: 0, revision_height: 150, }, + access_manager: access_manager::ID, }; let mut data = vec![]; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/upload_misbehaviour_chunk/tests.rs b/programs/solana/programs/ics07-tendermint/src/instructions/upload_misbehaviour_chunk/tests.rs index 7d110af46..5cce6d436 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/upload_misbehaviour_chunk/tests.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/upload_misbehaviour_chunk/tests.rs @@ -81,6 +81,7 @@ fn setup_test_accounts( revision_number: 0, revision_height: 100, }, + access_manager: access_manager::ID, }; let mut client_data = vec![]; @@ -195,6 +196,7 @@ fn test_upload_chunk_with_frozen_client_fails() { revision_number: 0, revision_height: 150, }, + access_manager: access_manager::ID, }; let mut data = vec![]; diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/verify_membership.rs b/programs/solana/programs/ics07-tendermint/src/instructions/verify_membership.rs index 886f3408d..082790b63 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/verify_membership.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/verify_membership.rs @@ -21,7 +21,6 @@ pub fn verify_membership(ctx: Context, msg: MembershipMsg) -> tendermint_light_client_membership::membership(app_hash, &[(kv_pair, proof)]) .map_err(|_| error!(ErrorCode::MembershipVerificationFailed))?; - Ok(()) } @@ -29,6 +28,9 @@ pub fn verify_membership(ctx: Context, msg: MembershipMsg) -> mod tests { use super::*; use crate::state::ConsensusStateStore; + use crate::test_helpers::chunk_test_utils::{ + derive_client_state_pda, derive_consensus_state_pda, + }; use crate::test_helpers::fixtures::*; use crate::test_helpers::PROGRAM_BINARY_PATH; use crate::types::{ClientState, ConsensusState, IbcHeight}; @@ -55,10 +57,6 @@ mod tests { client_state: ClientState, consensus_state: ConsensusState, ) -> TestAccounts { - use crate::test_helpers::chunk_test_utils::{ - derive_client_state_pda, derive_consensus_state_pda, - }; - let client_state_pda = derive_client_state_pda(&chain_id); let consensus_state_pda = derive_consensus_state_pda(&client_state_pda, height); @@ -299,8 +297,6 @@ mod tests { #[test] fn test_verify_membership_nonexistent_height() { - use crate::test_helpers::chunk_test_utils::derive_client_state_pda; - let fixture = load_membership_verification_fixture("verify_membership_key_0"); let client_state = decode_client_state_from_hex(&fixture.client_state_hex); @@ -313,10 +309,7 @@ mod tests { client_state.try_serialize(&mut client_data).unwrap(); let nonexistent_consensus_pda = - crate::test_helpers::chunk_test_utils::derive_consensus_state_pda( - &client_state_pda, - nonexistent_height, - ); + derive_consensus_state_pda(&client_state_pda, nonexistent_height); let accounts = vec![ ( diff --git a/programs/solana/programs/ics07-tendermint/src/instructions/verify_non_membership.rs b/programs/solana/programs/ics07-tendermint/src/instructions/verify_non_membership.rs index d5e4eaee3..5566193eb 100644 --- a/programs/solana/programs/ics07-tendermint/src/instructions/verify_non_membership.rs +++ b/programs/solana/programs/ics07-tendermint/src/instructions/verify_non_membership.rs @@ -36,6 +36,9 @@ pub fn verify_non_membership( mod tests { use super::*; use crate::state::ConsensusStateStore; + use crate::test_helpers::chunk_test_utils::{ + derive_client_state_pda, derive_consensus_state_pda, + }; use crate::test_helpers::fixtures::*; use crate::test_helpers::PROGRAM_BINARY_PATH; use crate::types::{ClientState, ConsensusState, IbcHeight}; @@ -62,10 +65,6 @@ mod tests { client_state: ClientState, consensus_state: ConsensusState, ) -> TestAccounts { - use crate::test_helpers::chunk_test_utils::{ - derive_client_state_pda, derive_consensus_state_pda, - }; - let client_state_pda = derive_client_state_pda(&chain_id); let consensus_state_pda = derive_consensus_state_pda(&client_state_pda, height); @@ -306,8 +305,6 @@ mod tests { #[test] fn test_verify_non_membership_nonexistent_height() { - use crate::test_helpers::chunk_test_utils::derive_client_state_pda; - let fixture = load_membership_verification_fixture("verify_non-membership_key_1"); let client_state = decode_client_state_from_hex(&fixture.client_state_hex); @@ -320,10 +317,7 @@ mod tests { client_state.try_serialize(&mut client_data).unwrap(); let nonexistent_consensus_pda = - crate::test_helpers::chunk_test_utils::derive_consensus_state_pda( - &client_state_pda, - nonexistent_height, - ); + derive_consensus_state_pda(&client_state_pda, nonexistent_height); let accounts = vec![ ( diff --git a/programs/solana/programs/ics07-tendermint/src/lib.rs b/programs/solana/programs/ics07-tendermint/src/lib.rs index a8dbf88cb..cabcd64bd 100644 --- a/programs/solana/programs/ics07-tendermint/src/lib.rs +++ b/programs/solana/programs/ics07-tendermint/src/lib.rs @@ -20,7 +20,7 @@ pub use types::{ pub use ics25_handler::{MembershipMsg, NonMembershipMsg}; #[derive(Accounts)] -#[instruction(chain_id: String, latest_height: u64, client_state: ClientState)] +#[instruction(chain_id: String, latest_height: u64, client_state: ClientState, consensus_state: ConsensusState)] pub struct Initialize<'info> { #[account( init, @@ -44,6 +44,33 @@ pub struct Initialize<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +#[instruction(chain_id: String)] +pub struct SetAccessManager<'info> { + #[account( + mut, + seeds = [ClientState::SEED, chain_id.as_bytes()], + bump, + constraint = client_state.chain_id == chain_id.as_str() + )] + pub client_state: Account<'info, ClientState>, + + /// CHECK: Validated via seeds constraint using the stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = client_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + + pub admin: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + #[derive(Accounts)] #[instruction(msg: ics25_handler::MembershipMsg)] pub struct VerifyMembership<'info> { @@ -108,6 +135,15 @@ pub struct AssembleAndUpdateClient<'info> { )] pub client_state: Account<'info, ClientState>, + /// Global access control account (owned by access-manager program) + /// CHECK: Validated by seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = client_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + /// Trusted consensus state at the height embedded in the header /// CHECK: Must already exist. Unchecked because PDA seeds require runtime header data. pub trusted_consensus_state: UncheckedAccount<'info>, @@ -121,6 +157,11 @@ pub struct AssembleAndUpdateClient<'info> { pub submitter: Signer<'info>, pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, // Remaining accounts are the chunk accounts in order // They will be validated and closed in the instruction handler } @@ -182,12 +223,26 @@ pub struct AssembleAndSubmitMisbehaviour<'info> { )] pub client_state: Account<'info, ClientState>, + /// Global access control account (owned by access-manager program) + /// CHECK: Validated by seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = client_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + pub trusted_consensus_state_1: Account<'info, ConsensusStateStore>, pub trusted_consensus_state_2: Account<'info, ConsensusStateStore>, #[account(mut)] pub submitter: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, // Remaining accounts are the chunk accounts in order } @@ -221,12 +276,21 @@ pub mod ics07_tendermint { client_state: ClientState, consensus_state: ConsensusState, ) -> Result<()> { - // NOTE: chain_id is used in the #[instruction] attribute for account validation - // but the actual handler doesn't need it as it's embedded in client_state - assert_eq!(client_state.chain_id, chain_id); - assert_eq!(client_state.latest_height.revision_height, latest_height); + instructions::initialize::initialize( + ctx, + chain_id, + latest_height, + client_state, + consensus_state, + ) + } - instructions::initialize::initialize(ctx, client_state, consensus_state) + pub fn set_access_manager( + ctx: Context, + chain_id: String, + new_access_manager: Pubkey, + ) -> Result<()> { + instructions::set_access_manager::set_access_manager(ctx, chain_id, new_access_manager) } pub fn verify_membership(ctx: Context, msg: MembershipMsg) -> Result<()> { @@ -253,8 +317,8 @@ pub mod ics07_tendermint { /// Assemble chunks and update the client /// Automatically cleans up all chunks after successful update - pub fn assemble_and_update_client( - ctx: Context, + pub fn assemble_and_update_client<'info>( + ctx: Context<'_, '_, '_, 'info, AssembleAndUpdateClient<'info>>, chain_id: String, target_height: u64, ) -> Result { diff --git a/programs/solana/programs/ics07-tendermint/src/test_helpers.rs b/programs/solana/programs/ics07-tendermint/src/test_helpers.rs index 0df1d8319..6e6ca699a 100644 --- a/programs/solana/programs/ics07-tendermint/src/test_helpers.rs +++ b/programs/solana/programs/ics07-tendermint/src/test_helpers.rs @@ -88,6 +88,7 @@ pub mod fixtures { revision_number: latest_height.revision_number, revision_height: latest_height.revision_height, }, + access_manager: access_manager::ID, } } @@ -347,6 +348,7 @@ pub mod fixtures { revision_number: latest_height.revision_number, revision_height: latest_height.revision_height, }, + access_manager: access_manager::ID, } } @@ -408,8 +410,8 @@ pub mod fixtures { pub mod chunk_test_utils { use crate::state::{HeaderChunk, CHUNK_DATA_SIZE}; use crate::types::{ClientState, ConsensusState, IbcHeight, UploadChunkParams}; - use anchor_lang::solana_program::keccak; use solana_sdk::account::Account; + use solana_sdk::keccak; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_program; @@ -462,6 +464,7 @@ pub mod chunk_test_utils { revision_number: 0, revision_height: latest_height, }, + access_manager: access_manager::ID, }; let mut data = vec![]; @@ -584,3 +587,88 @@ pub mod chunk_test_utils { (all_chunks, header_commitment) } } + +/// Access control test utilities +pub mod access_control { + use access_manager::RoleData; + use anchor_lang::prelude::Pubkey; + use anchor_lang::{AnchorSerialize, Discriminator}; + + /// Setup access manager account for tests + /// Returns (PDA, serialized account data) + pub fn setup_access_manager(admin: Pubkey, relayers: Vec) -> (Pubkey, Vec) { + let (access_manager_pda, _) = Pubkey::find_program_address( + &[access_manager::state::AccessManager::SEED], + &access_manager::ID, + ); + + let mut roles = vec![RoleData { + role_id: solana_ibc_types::roles::ADMIN_ROLE, + members: vec![admin], + }]; + + if !relayers.is_empty() { + roles.push(RoleData { + role_id: solana_ibc_types::roles::RELAYER_ROLE, + members: relayers, + }); + } + + let access_manager = access_manager::state::AccessManager { roles }; + + let mut data = access_manager::state::AccessManager::DISCRIMINATOR.to_vec(); + access_manager.serialize(&mut data).unwrap(); + + (access_manager_pda, data) + } + + /// Create access manager account for mollusk tests + pub fn create_access_manager_account( + admin: Pubkey, + relayers: Vec, + ) -> (Pubkey, solana_sdk::account::Account) { + let (pda, data) = setup_access_manager(admin, relayers); + + let account = solana_sdk::account::Account { + lamports: 10_000_000, + data, + owner: access_manager::ID, + executable: false, + rent_epoch: 0, + }; + + (pda, account) + } +} + +/// Create instructions sysvar account for direct call (not CPI) +pub fn create_instructions_sysvar_account() -> solana_sdk::account::Account { + use solana_sdk::pubkey::Pubkey; + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + // Create minimal mock instruction to simulate direct call + // Current instruction has this program as the program_id + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &crate::ID, // Direct call to our program + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + solana_sdk::account::Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + } +} diff --git a/programs/solana/programs/ics07-tendermint/src/types.rs b/programs/solana/programs/ics07-tendermint/src/types.rs index eb7d561ad..736e5a3ec 100644 --- a/programs/solana/programs/ics07-tendermint/src/types.rs +++ b/programs/solana/programs/ics07-tendermint/src/types.rs @@ -43,6 +43,8 @@ pub struct ClientState { pub max_clock_drift: u64, pub frozen_height: IbcHeight, pub latest_height: IbcHeight, + /// Access manager program ID for role-based access control + pub access_manager: Pubkey, } impl ClientState { @@ -204,6 +206,7 @@ mod compatibility_tests { revision_number: 1, revision_height: 1000, }, + access_manager: access_manager::ID, }; let serialized = client_state.try_to_vec().unwrap(); diff --git a/programs/solana/programs/ics26-router/Cargo.toml b/programs/solana/programs/ics26-router/Cargo.toml index 7e582a224..8a7caa3a5 100644 --- a/programs/solana/programs/ics26-router/Cargo.toml +++ b/programs/solana/programs/ics26-router/Cargo.toml @@ -27,8 +27,11 @@ idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } anchor-spl.workspace = true +solana-program.workspace = true solana-ibc-types.workspace = true +solana-ibc-macros.workspace = true ics25-handler.workspace = true +access-manager = { workspace = true, features = ["cpi"] } sha2 = "0.10" hex = "0.4" @@ -40,3 +43,6 @@ mollusk-svm.workspace = true mollusk-svm-bencher.workspace = true solana-sdk.workspace = true bincode.workspace = true +mock-ibc-app = { path = "../mock-ibc-app" } +mock-light-client = { path = "../mock-light-client" } +dummy-ibc-app = { path = "../dummy-ibc-app" } diff --git a/programs/solana/programs/ics26-router/src/errors.rs b/programs/solana/programs/ics26-router/src/errors.rs index 50c38fb84..a3ebedd8a 100644 --- a/programs/solana/programs/ics26-router/src/errors.rs +++ b/programs/solana/programs/ics26-router/src/errors.rs @@ -4,10 +4,8 @@ use anchor_lang::prelude::*; pub enum RouterError { #[msg("Unauthorized sender")] UnauthorizedSender, - #[msg("Port already bound to IBC app")] - PortAlreadyBound, - #[msg("Port not found")] - PortNotFound, + #[msg("Unauthorized role")] + UnauthorizedRole, #[msg("Invalid port identifier")] InvalidPortIdentifier, #[msg("Invalid timeout timestamp")] @@ -40,8 +38,6 @@ pub enum RouterError { InvalidClientId, #[msg("Invalid light client program")] InvalidLightClientProgram, - #[msg("Unsupported client type")] - UnsupportedClientType, #[msg("Invalid counterparty info")] InvalidCounterpartyInfo, #[msg("Client already exists")] diff --git a/programs/solana/programs/ics26-router/src/instructions.rs b/programs/solana/programs/ics26-router/src/instructions.rs index 50ea029f0..91bb0ab8d 100644 --- a/programs/solana/programs/ics26-router/src/instructions.rs +++ b/programs/solana/programs/ics26-router/src/instructions.rs @@ -5,6 +5,7 @@ pub mod client; pub mod initialize; pub mod recv_packet; pub mod send_packet; +pub mod set_access_manager; pub mod timeout_packet; pub mod upload_payload_chunk; pub mod upload_proof_chunk; @@ -16,6 +17,7 @@ pub use client::*; pub use initialize::*; pub use recv_packet::*; pub use send_packet::*; +pub use set_access_manager::*; pub use timeout_packet::*; pub use upload_payload_chunk::*; pub use upload_proof_chunk::*; diff --git a/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs b/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs index 173f05194..b74e8c96f 100644 --- a/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/ack_packet.rs @@ -19,6 +19,15 @@ pub struct AckPacket<'info> { )] pub router_state: Account<'info, RouterState>, + /// Global access control account (owned by access-manager program) + /// CHECK: Validated by seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager, + )] + pub access_manager: AccountInfo<'info>, + // Note: Port validation is done in the handler function to avoid Anchor macro issues pub ibc_app: Account<'info, IBCApp>, @@ -82,17 +91,21 @@ pub fn ack_packet<'info>( ctx: Context<'_, '_, '_, 'info, AckPacket<'info>>, msg: MsgAckPacket, ) -> Result<()> { + // Check that relayer has the required role + // Ethereum: ICS26Router.sol:214 - ackPacket restricted to RELAYER_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::RELAYER_ROLE, + &ctx.accounts.relayer, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + // TODO: Support multi-payload packets #602 - let router_state = &ctx.accounts.router_state; let packet_commitment_account = &ctx.accounts.packet_commitment; let client = &ctx.accounts.client; - require_keys_eq!( - ctx.accounts.relayer.key(), - router_state.authority, - RouterError::UnauthorizedSender - ); - require_eq!( &msg.packet.source_client, &client.client_id, @@ -232,7 +245,7 @@ mod tests { use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; - use solana_ibc_types::{Payload, PayloadMetadata, ProofMetadata}; + use solana_ibc_types::{roles, Payload, PayloadMetadata, ProofMetadata}; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; @@ -290,10 +303,9 @@ mod tests { .wrong_light_client_program .unwrap_or(MOCK_LIGHT_CLIENT_ID); - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); let (client_pda, client_data) = setup_client( params.source_client_id, - authority, client_light_client_program, params.dest_client_id, params.active_client, @@ -365,8 +377,13 @@ mod tests { test_proof, ); + // Setup access control with the authority having RELAYER_ROLE + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::RELAYER_ROLE, &[authority])]); + let mut instruction_accounts = vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(ibc_app_pda, false), AccountMeta::new(packet_commitment_pda, false), AccountMeta::new_readonly(app_program_id, false), @@ -418,6 +435,7 @@ mod tests { let mut accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(ibc_app_pda, ibc_app_data, crate::ID), packet_commitment_account, create_bpf_program_account(app_program_id), @@ -425,11 +443,12 @@ mod tests { create_bpf_program_account(crate::ID), // router_program create_system_account(relayer), // relayer (also signer) create_program_account(system_program::ID), - create_instructions_sysvar_account(), + create_instructions_sysvar_account_with_caller(crate::ID), create_account(client_pda, client_data, crate::ID), create_bpf_program_account(instruction_light_client_program), create_account(client_state, vec![0u8; 100], client_light_client_program), create_account(consensus_state, vec![0u8; 100], client_light_client_program), + create_program_account(access_manager::ID), ]; // Add chunk accounts as remaining accounts @@ -490,10 +509,10 @@ mod tests { ..Default::default() }); - let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + let mollusk = setup_mollusk_with_mock_programs(); let checks = vec![Check::err(ProgramError::Custom( - ANCHOR_ERROR_OFFSET + RouterError::UnauthorizedSender as u32, + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::Unauthorized as u32, ))]; mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); @@ -665,4 +684,47 @@ mod tests { mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); } + + #[test] + fn test_ack_packet_fake_sysvar_wormhole_attack() { + let mut ctx = setup_ack_packet_test_with_params(AckPacketTestParams::default()); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = + setup_fake_sysvar_attack(ctx.instruction, crate::ID); + ctx.instruction = instruction; + ctx.accounts.push(fake_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + + mollusk.process_and_validate_instruction( + &ctx.instruction, + &ctx.accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_ack_packet_cpi_rejection() { + let mut ctx = setup_ack_packet_test_with_params(AckPacketTestParams::default()); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = + setup_cpi_call_test(ctx.instruction, malicious_program); + ctx.instruction = instruction; + + // Remove the existing direct-call sysvar and replace with CPI sysvar + ctx.accounts + .retain(|(pubkey, _)| *pubkey != solana_sdk::sysvar::instructions::ID); + ctx.accounts.push(cpi_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); + } } diff --git a/programs/solana/programs/ics26-router/src/instructions/add_ibc_app.rs b/programs/solana/programs/ics26-router/src/instructions/add_ibc_app.rs index afe133b8a..d30986412 100644 --- a/programs/solana/programs/ics26-router/src/instructions/add_ibc_app.rs +++ b/programs/solana/programs/ics26-router/src/instructions/add_ibc_app.rs @@ -12,6 +12,14 @@ pub struct AddIbcApp<'info> { )] pub router_state: Account<'info, RouterState>, + /// CHECK: Validated via seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + #[account( init, payer = payer, @@ -31,20 +39,27 @@ pub struct AddIbcApp<'info> { pub authority: Signer<'info>, pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } pub fn add_ibc_app(ctx: Context, port_id: String) -> Result<()> { - let router_state = &ctx.accounts.router_state; - let ibc_app = &mut ctx.accounts.ibc_app; - - require_keys_eq!( - ctx.accounts.authority.key(), - router_state.authority, - RouterError::UnauthorizedSender - ); + // Ethereum: ICS26Router.sol:89 - addIBCApp(string portId, ...) restricted to ID_CUSTOMIZER_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ID_CUSTOMIZER_ROLE, + &ctx.accounts.authority, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; require!(!port_id.is_empty(), RouterError::InvalidPortIdentifier); + let ibc_app = &mut ctx.accounts.ibc_app; ibc_app.version = AccountVersion::V1; ibc_app.port_id = port_id; ibc_app.app_program_id = ctx.accounts.app_program.key(); @@ -66,6 +81,7 @@ mod tests { use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; + use solana_ibc_types::roles; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; @@ -78,7 +94,10 @@ mod tests { let port_id = "test-port"; let app_program = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); + + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); let (ibc_app_pda, _) = Pubkey::find_program_address(&[IBCApp::SEED, port_id.as_bytes()], &crate::ID); @@ -91,21 +110,26 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(ibc_app_pda, false), AccountMeta::new_readonly(app_program, false), AccountMeta::new(payer, true), AccountMeta::new_readonly(authority, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; let accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_uninitialized_account(ibc_app_pda, 0), create_account(app_program, vec![], system_program::ID), create_system_account(payer), + create_system_account(authority), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -120,13 +144,17 @@ mod tests { #[test] fn test_add_ibc_app_unauthorized_sender() { - let authority = Pubkey::new_unique(); + let authorized_user = Pubkey::new_unique(); let unauthorized_sender = Pubkey::new_unique(); let payer = unauthorized_sender; let port_id = "test-port"; let app_program = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); + + // Setup access manager with authorized_user having ID_CUSTOMIZER_ROLE, but NOT unauthorized_sender + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authorized_user])]); let (ibc_app_pda, _) = Pubkey::find_program_address(&[IBCApp::SEED, port_id.as_bytes()], &crate::ID); @@ -139,21 +167,26 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(ibc_app_pda, false), AccountMeta::new_readonly(app_program, false), AccountMeta::new(payer, true), AccountMeta::new_readonly(unauthorized_sender, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; let accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_uninitialized_account(ibc_app_pda, 0), create_account(app_program, vec![], system_program::ID), create_system_account(payer), + create_system_account(unauthorized_sender), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -172,8 +205,9 @@ mod tests { let port_id = ""; // Empty port ID let app_program = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); - + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); let (ibc_app_pda, _) = Pubkey::find_program_address(&[IBCApp::SEED, port_id.as_bytes()], &crate::ID); @@ -185,21 +219,26 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(ibc_app_pda, false), AccountMeta::new_readonly(app_program, false), AccountMeta::new(payer, true), AccountMeta::new_readonly(authority, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; let accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_uninitialized_account(ibc_app_pda, 0), create_account(app_program, vec![], system_program::ID), create_system_account(payer), + create_system_account(authority), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -218,8 +257,9 @@ mod tests { let port_id = "test-port"; let app_program = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); - + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); // IBC app already exists let (ibc_app_pda, existing_ibc_app_data) = setup_ibc_app(port_id, Pubkey::new_unique()); @@ -231,21 +271,26 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(ibc_app_pda, false), AccountMeta::new_readonly(app_program, false), AccountMeta::new(payer, true), AccountMeta::new_readonly(authority, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; let accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(ibc_app_pda, existing_ibc_app_data, crate::ID), create_account(app_program, vec![], system_program::ID), create_system_account(payer), + create_system_account(authority), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -254,4 +299,118 @@ mod tests { let result = mollusk.process_instruction(&instruction, &accounts); assert!(result.program_result.is_err()); } + + #[test] + fn test_add_ibc_app_fake_sysvar_wormhole_attack() { + let authority = Pubkey::new_unique(); + let payer = authority; + let port_id = "test-port"; + let app_program = Pubkey::new_unique(); + + let (router_state_pda, router_state_data) = setup_router_state(); + + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); + + let (ibc_app_pda, _) = + Pubkey::find_program_address(&[IBCApp::SEED, port_id.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::AddIbcApp { + port_id: port_id.to_string(), + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new(ibc_app_pda, false), + AccountMeta::new_readonly(app_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), + create_uninitialized_account(ibc_app_pda, 0), + create_account(app_program, vec![], system_program::ID), + create_system_account(payer), + create_program_account(system_program::ID), + fake_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_add_ibc_app_cpi_rejection() { + let authority = Pubkey::new_unique(); + let payer = authority; + let port_id = "test-port"; + let app_program = Pubkey::new_unique(); + + let (router_state_pda, router_state_data) = setup_router_state(); + + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); + + let (ibc_app_pda, _) = + Pubkey::find_program_address(&[IBCApp::SEED, port_id.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::AddIbcApp { + port_id: port_id.to_string(), + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new(ibc_app_pda, false), + AccountMeta::new_readonly(app_program, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), + create_uninitialized_account(ibc_app_pda, 0), + create_account(app_program, vec![], system_program::ID), + create_system_account(payer), + create_system_account(authority), + create_program_account(system_program::ID), + cpi_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } } diff --git a/programs/solana/programs/ics26-router/src/instructions/client.rs b/programs/solana/programs/ics26-router/src/instructions/client.rs index 802488b04..6c87f9e17 100644 --- a/programs/solana/programs/ics26-router/src/instructions/client.rs +++ b/programs/solana/programs/ics26-router/src/instructions/client.rs @@ -11,11 +11,18 @@ pub struct AddClient<'info> { #[account( seeds = [RouterState::SEED], - bump, - constraint = router_state.authority == authority.key() @ RouterError::UnauthorizedAuthority, + bump )] pub router_state: Account<'info, RouterState>, + /// CHECK: Validated via seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + #[account( init, payer = authority, @@ -40,6 +47,11 @@ pub struct AddClient<'info> { pub light_client_program: AccountInfo<'info>, pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } #[derive(Accounts)] @@ -50,20 +62,31 @@ pub struct MigrateClient<'info> { #[account( seeds = [RouterState::SEED], - bump, - constraint = router_state.authority == authority.key() @ RouterError::UnauthorizedAuthority, + bump )] pub router_state: Account<'info, RouterState>, + /// CHECK: Validated via seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + #[account( mut, seeds = [Client::SEED, client_id.as_bytes()], - bump, - constraint = client.authority == authority.key() @ RouterError::UnauthorizedAuthority, + bump )] pub client: Account<'info, Client>, pub relayer: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } /// Parameters for migrating a client @@ -82,14 +105,18 @@ pub fn add_client( client_id: String, counterparty_info: CounterpartyInfo, ) -> Result<()> { + // Ethereum: ICS02ClientUpgradeable.sol:91 - addClient(string clientId, ...) restricted to ID_CUSTOMIZER_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ID_CUSTOMIZER_ROLE, + &ctx.accounts.relayer, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + let client = &mut ctx.accounts.client; let light_client_program = &ctx.accounts.light_client_program; - let router_state = &ctx.accounts.router_state; - - require!( - ctx.accounts.relayer.key() == router_state.authority, - RouterError::UnauthorizedSender - ); require!( validate_custom_ibc_identifier(&client_id), @@ -108,7 +135,6 @@ pub fn add_client( client.client_id = client_id; client.client_program_id = light_client_program.key(); client.counterparty_info = counterparty_info; - client.authority = ctx.accounts.authority.key(); client.active = true; client._reserved = [0u8; 256]; @@ -129,12 +155,16 @@ pub fn migrate_client( params: MigrateClientParams, ) -> Result<()> { let client = &mut ctx.accounts.client; - let router_state = &ctx.accounts.router_state; - require!( - ctx.accounts.relayer.key() == router_state.authority, - RouterError::UnauthorizedSender - ); + // Ethereum: ICS02ClientUpgradeable.sol:142 - migrateClient restricted to ADMIN_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ADMIN_ROLE, + &ctx.accounts.relayer, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; require!( params.client_program_id.is_some() @@ -215,6 +245,7 @@ mod tests { use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; + use solana_ibc_types::roles; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; @@ -261,7 +292,9 @@ mod tests { let relayer = authority; let light_client_program = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); let (client_pda, _) = Pubkey::find_program_address(&[Client::SEED, config.client_id.as_bytes()], &crate::ID); let (client_sequence_pda, _) = Pubkey::find_program_address( @@ -281,11 +314,13 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new(client_sequence_pda, false), AccountMeta::new_readonly(relayer, true), AccountMeta::new_readonly(light_client_program, false), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -293,10 +328,13 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_uninitialized_account(client_pda, 0), create_uninitialized_account(client_sequence_pda, 0), + create_system_account(relayer), create_program_account(light_client_program), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -386,7 +424,6 @@ mod tests { .expect("Failed to deserialize client"); assert_eq!(deserialized_client.client_id, client_id); - assert_eq!(deserialized_client.authority, authority); assert!(deserialized_client.active); assert_eq!( deserialized_client.counterparty_info.client_id, @@ -470,14 +507,11 @@ mod tests { let light_client_program = Pubkey::new_unique(); let client_id = "test-client-02"; - let (router_state_pda, router_state_data) = setup_router_state(authority); - let (client_pda, client_data) = setup_client( - client_id, - authority, - light_client_program, - "counterparty-client", - true, - ); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); + let (client_pda, client_data) = + setup_client(client_id, light_client_program, "counterparty-client", true); let instruction_data = crate::instruction::MigrateClient { client_id: client_id.to_string(), @@ -493,8 +527,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -502,7 +538,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -524,7 +563,6 @@ mod tests { assert!(!deserialized_client.active, "Client should be deactivated"); assert_eq!(deserialized_client.client_id, client_id); assert_eq!(deserialized_client.client_program_id, light_client_program); - assert_eq!(deserialized_client.authority, authority); } #[test] @@ -535,10 +573,11 @@ mod tests { let new_light_client_program = Pubkey::new_unique(); let client_id = "test-client-03"; - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); let (client_pda, client_data) = setup_client( client_id, - authority, old_light_client_program, "counterparty-client", true, @@ -558,8 +597,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -567,7 +608,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -590,7 +634,6 @@ mod tests { "Client program ID should be updated" ); assert_eq!(deserialized_client.client_id, client_id); - assert_eq!(deserialized_client.authority, authority); assert!(deserialized_client.active); } @@ -606,14 +649,11 @@ mod tests { merkle_prefix: vec![vec![0x02, 0x03]], }; - let (router_state_pda, router_state_data) = setup_router_state(authority); - let (client_pda, client_data) = setup_client( - client_id, - authority, - light_client_program, - "old-counterparty", - true, - ); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); + let (client_pda, client_data) = + setup_client(client_id, light_client_program, "old-counterparty", true); let instruction_data = crate::instruction::MigrateClient { client_id: client_id.to_string(), @@ -629,8 +669,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -638,7 +680,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -680,10 +725,11 @@ mod tests { merkle_prefix: vec![vec![0x04, 0x05, 0x06]], }; - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); let (client_pda, client_data) = setup_client( client_id, - authority, old_light_client_program, "old-counterparty", true, @@ -703,8 +749,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -712,7 +760,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -752,14 +803,11 @@ mod tests { let light_client_program = Pubkey::new_unique(); let client_id = "test-client-07"; - let (router_state_pda, router_state_data) = setup_router_state(authority); - let (client_pda, client_data) = setup_client( - client_id, - authority, - light_client_program, - "counterparty-client", - true, - ); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); + let (client_pda, client_data) = + setup_client(client_id, light_client_program, "counterparty-client", true); let instruction_data = crate::instruction::MigrateClient { client_id: client_id.to_string(), @@ -775,8 +823,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -784,7 +834,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -803,14 +856,11 @@ mod tests { let light_client_program = Pubkey::new_unique(); let client_id = "test-client-08"; - let (router_state_pda, router_state_data) = setup_router_state(authority); - let (client_pda, client_data) = setup_client( - client_id, - authority, - light_client_program, - "counterparty-client", - true, - ); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ADMIN_ROLE, &[authority])]); + let (client_pda, client_data) = + setup_client(client_id, light_client_program, "counterparty-client", true); let instruction_data = crate::instruction::MigrateClient { client_id: client_id.to_string(), @@ -829,8 +879,10 @@ mod tests { accounts: vec![ AccountMeta::new(authority, true), AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -838,7 +890,10 @@ mod tests { let accounts = vec![ create_system_account(authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(client_pda, client_data, crate::ID), + create_system_account(relayer), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -852,13 +907,15 @@ mod tests { #[test] fn test_add_client_unauthorized_authority() { - let correct_authority = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); let wrong_authority = Pubkey::new_unique(); let relayer = wrong_authority; let light_client_program = Pubkey::new_unique(); let client_id = "test-client"; - let (router_state_pda, router_state_data) = setup_router_state(correct_authority); + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); let (client_pda, _) = Pubkey::find_program_address(&[Client::SEED, client_id.as_bytes()], &crate::ID); let (client_sequence_pda, _) = @@ -877,11 +934,13 @@ mod tests { accounts: vec![ AccountMeta::new(wrong_authority, true), // Wrong authority tries to add client AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new(client_pda, false), AccountMeta::new(client_sequence_pda, false), AccountMeta::new_readonly(relayer, true), AccountMeta::new_readonly(light_client_program, false), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -889,18 +948,147 @@ mod tests { let accounts = vec![ create_system_account(wrong_authority), create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_uninitialized_account(client_pda, 0), create_uninitialized_account(client_sequence_pda, 0), + create_system_account(relayer), create_program_account(light_client_program), create_program_account(system_program::ID), + create_instructions_sysvar_account_with_caller(crate::ID), + create_program_account(access_manager::ID), ]; - let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + let mollusk = setup_mollusk_with_light_client(); let checks = vec![Check::err(ProgramError::Custom( - ANCHOR_ERROR_OFFSET + RouterError::UnauthorizedAuthority as u32, + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::Unauthorized as u32, ))]; mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } + + #[test] + fn test_add_client_fake_sysvar_wormhole_attack() { + let authority = Pubkey::new_unique(); + let relayer = authority; + let light_client_program = Pubkey::new_unique(); + let client_id = "test-client"; + + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); + let (client_pda, _) = + Pubkey::find_program_address(&[Client::SEED, client_id.as_bytes()], &crate::ID); + let (client_sequence_pda, _) = + Pubkey::find_program_address(&[ClientSequence::SEED, client_id.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::AddClient { + client_id: client_id.to_string(), + counterparty_info: CounterpartyInfo { + client_id: "counterparty-client".to_string(), + merkle_prefix: vec![vec![0x01, 0x02, 0x03]], + }, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(authority, true), + AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new(client_pda, false), + AccountMeta::new(client_sequence_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(light_client_program, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + create_system_account(authority), + create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), + create_uninitialized_account(client_pda, 0), + create_uninitialized_account(client_sequence_pda, 0), + create_program_account(light_client_program), + create_program_account(system_program::ID), + fake_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_add_client_cpi_rejection() { + let authority = Pubkey::new_unique(); + let relayer = authority; + let light_client_program = Pubkey::new_unique(); + let client_id = "test-client"; + + let (router_state_pda, router_state_data) = setup_router_state(); + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::ID_CUSTOMIZER_ROLE, &[authority])]); + let (client_pda, _) = + Pubkey::find_program_address(&[Client::SEED, client_id.as_bytes()], &crate::ID); + let (client_sequence_pda, _) = + Pubkey::find_program_address(&[ClientSequence::SEED, client_id.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::AddClient { + client_id: client_id.to_string(), + counterparty_info: CounterpartyInfo { + client_id: "counterparty-client".to_string(), + merkle_prefix: vec![vec![0x01, 0x02, 0x03]], + }, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(authority, true), + AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new(client_pda, false), + AccountMeta::new(client_sequence_pda, false), + AccountMeta::new_readonly(relayer, true), + AccountMeta::new_readonly(light_client_program, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + create_system_account(authority), + create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), + create_uninitialized_account(client_pda, 0), + create_uninitialized_account(client_sequence_pda, 0), + create_system_account(relayer), + create_program_account(light_client_program), + create_program_account(system_program::ID), + cpi_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } } diff --git a/programs/solana/programs/ics26-router/src/instructions/initialize.rs b/programs/solana/programs/ics26-router/src/instructions/initialize.rs index 7994d1273..74515f142 100644 --- a/programs/solana/programs/ics26-router/src/instructions/initialize.rs +++ b/programs/solana/programs/ics26-router/src/instructions/initialize.rs @@ -1,5 +1,7 @@ +use crate::errors::RouterError; use crate::state::{AccountVersion, RouterState}; use anchor_lang::prelude::*; +use solana_ibc_types::reject_cpi; #[derive(Accounts)] pub struct Initialize<'info> { @@ -16,12 +18,21 @@ pub struct Initialize<'info> { pub payer: Signer<'info>, pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } -pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { +pub fn initialize(ctx: Context, access_manager: Pubkey) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(RouterError::from)?; + let router_state = &mut ctx.accounts.router_state; router_state.version = AccountVersion::V1; - router_state.authority = authority; + router_state.paused = false; + router_state.access_manager = access_manager; router_state._reserved = [0u8; 256]; Ok(()) } @@ -29,22 +40,28 @@ pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use crate::errors::RouterError; + use crate::test_utils::*; use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; use solana_sdk::account::Account; use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; use solana_sdk::{native_loader, system_program}; + const ANCHOR_ERROR_OFFSET: u32 = 6000; + #[test] fn test_initialize_happy_path() { let payer = Pubkey::new_unique(); - let authority = Pubkey::new_unique(); let (router_state_pda, _) = Pubkey::find_program_address(&[RouterState::SEED], &crate::ID); - let instruction_data = crate::instruction::Initialize { authority }; + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; let instruction = Instruction { program_id: crate::ID, @@ -52,6 +69,7 @@ mod tests { AccountMeta::new(router_state_pda, false), AccountMeta::new(payer, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -88,6 +106,7 @@ mod tests { rent_epoch: 0, }, ), + crate::test_utils::create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -131,6 +150,150 @@ mod tests { RouterState::try_deserialize(&mut &router_state_account.data[..]) .expect("Failed to deserialize router state"); - assert_eq!(deserialized_router_state.authority, authority); + assert!(!deserialized_router_state.paused); + assert_eq!(deserialized_router_state.version, AccountVersion::V1); + assert_eq!(deserialized_router_state.access_manager, access_manager::ID); + } + + #[test] + fn test_initialize_fake_sysvar_wormhole_attack() { + let payer = Pubkey::new_unique(); + + let (router_state_pda, _) = Pubkey::find_program_address(&[RouterState::SEED], &crate::ID); + + // Simulate Wormhole attack: pass a completely different account with fake sysvar data + let (fake_sysvar_pubkey, fake_sysvar_account) = + crate::test_utils::create_fake_instructions_sysvar_account(crate::ID); + + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; + + let mut instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Modify the instruction to reference the fake sysvar (simulating attacker control) + instruction.accounts[3] = AccountMeta::new_readonly(fake_sysvar_pubkey, false); + + let accounts = vec![ + ( + router_state_pda, + Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + payer, + Account { + lamports: 10_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + system_program::ID, + Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ), + // Wormhole attack: provide a DIFFERENT account instead of the real sysvar + (fake_sysvar_pubkey, fake_sysvar_account), + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + + // Should be rejected by Anchor's address constraint check + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ErrorCode::ConstraintAddress as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_initialize_cpi_rejection() { + let payer = Pubkey::new_unique(); + + let (router_state_pda, _) = Pubkey::find_program_address(&[RouterState::SEED], &crate::ID); + + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let payer_lamports = 10_000_000_000; + let accounts = vec![ + ( + router_state_pda, + solana_sdk::account::Account { + lamports: 0, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + payer, + solana_sdk::account::Account { + lamports: payer_lamports, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + }, + ), + ( + system_program::ID, + solana_sdk::account::Account { + lamports: 0, + data: vec![], + owner: native_loader::ID, + executable: true, + rent_epoch: 0, + }, + ), + cpi_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + + // When CPI is detected by reject_cpi, it returns RouterError::UnauthorizedSender (mapped from CpiValidationError::UnauthorizedCaller) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + RouterError::UnauthorizedSender as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } } diff --git a/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs b/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs index 471974326..33c872b5d 100644 --- a/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/recv_packet.rs @@ -17,6 +17,15 @@ pub struct RecvPacket<'info> { )] pub router_state: Account<'info, RouterState>, + /// Global access control account (owned by access-manager program) + /// CHECK: Validated by seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager, + )] + pub access_manager: AccountInfo<'info>, + // Note: Port validation is done in the handler function to avoid Anchor macro issues pub ibc_app: Account<'info, IBCApp>, @@ -101,19 +110,22 @@ pub fn recv_packet<'info>( ctx: Context<'_, '_, '_, 'info, RecvPacket<'info>>, msg: MsgRecvPacket, ) -> Result<()> { - let router_state = &ctx.accounts.router_state; + // Performs: CPI rejection + signer verification + role check + // Ethereum: ICS26Router.sol:147 - recvPacket restricted to RELAYER_ROLE + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::RELAYER_ROLE, + &ctx.accounts.relayer, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + let packet_receipt = &mut ctx.accounts.packet_receipt; let packet_ack = &mut ctx.accounts.packet_ack; let client = &ctx.accounts.client; // Get clock directly via syscall let clock = Clock::get()?; - require_keys_eq!( - ctx.accounts.relayer.key(), - router_state.authority, - RouterError::UnauthorizedSender - ); - require_eq!( &msg.packet.source_client, &client.counterparty_info.client_id, @@ -282,7 +294,7 @@ mod tests { use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; - use solana_ibc_types::{Payload, PayloadMetadata, ProofMetadata}; + use solana_ibc_types::{roles, Payload, PayloadMetadata, ProofMetadata}; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; @@ -297,6 +309,7 @@ mod tests { let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + // Expect RouterError::UnauthorizedSender let checks = vec![Check::err(ProgramError::Custom( ANCHOR_ERROR_OFFSET + RouterError::UnauthorizedSender as u32, ))]; @@ -385,12 +398,11 @@ mod tests { let port_id = "test-port"; let light_client_program = MOCK_LIGHT_CLIENT_ID; - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); // Always setup client expecting "source-client" as counterparty let (client_pda, client_data) = setup_client( client_id, - authority, light_client_program, "source-client", params.active_client, @@ -452,9 +464,12 @@ mod tests { let client_state = Pubkey::new_unique(); let consensus_state = Pubkey::new_unique(); + // The transaction signer is the relayer + let transaction_signer = relayer; + // Create chunk accounts for 1 payload chunk and 1 proof chunk let payload_chunk_account = create_payload_chunk_account( - relayer, + transaction_signer, client_id, 1, 0, // payload_index @@ -463,12 +478,22 @@ mod tests { ); let proof_chunk_account = create_proof_chunk_account( - relayer, client_id, 1, 0, // chunk_index + transaction_signer, + client_id, + 1, + 0, // chunk_index test_proof, ); + // Setup access control: authority always has RELAYER_ROLE + // For authorized tests: transaction_signer == authority (has the role) + // For unauthorized tests: transaction_signer != authority (does NOT have the role) + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::RELAYER_ROLE, &[authority])]); + let mut instruction_accounts = vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(ibc_app_pda, false), AccountMeta::new(client_sequence_pda, false), AccountMeta::new(packet_receipt_pda, false), @@ -498,12 +523,13 @@ mod tests { let packet_receipt_account = create_uninitialized_commitment_account(packet_receipt_pda); let packet_ack_account = create_uninitialized_commitment_account(packet_ack_pda); - // Create signer account (relayer and payer are the same) - let signer_account = create_system_account(relayer); + // Create signer account (transaction_signer and payer are the same) + let signer_account = create_system_account(transaction_signer); // Accounts must be in the exact order of the instruction let mut accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(ibc_app_pda, ibc_app_data, crate::ID), create_account(client_sequence_pda, client_sequence_data, crate::ID), packet_receipt_account, @@ -513,7 +539,7 @@ mod tests { create_bpf_program_account(crate::ID), // router_program signer_account, // relayer create_program_account(system_program::ID), - create_instructions_sysvar_account(), + create_instructions_sysvar_account_with_caller(crate::ID), create_account(client_pda, client_data, crate::ID), create_bpf_program_account(light_client_program), create_account(client_state, vec![0u8; 100], light_client_program), @@ -779,13 +805,13 @@ mod tests { // Find and replace the IBC app account if let Some(pos) = ctx.accounts.iter().position(|(pubkey, _)| { - // The IBC app is at index 1 in the accounts list based on instruction_accounts setup - *pubkey == ctx.accounts[1].0 + // The IBC app is at index 2 (after access_manager at 0 and router_state at 1) + *pubkey == ctx.accounts[2].0 }) { ctx.accounts[pos] = (wrong_ibc_app, wrong_ibc_app_account); // Also update the instruction to use the wrong account - ctx.instruction.accounts[1].pubkey = wrong_ibc_app; + ctx.instruction.accounts[2].pubkey = wrong_ibc_app; } let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); @@ -819,8 +845,8 @@ mod tests { let mut cursor = std::io::Cursor::new(&mut data[8..]); existing_ack.serialize(&mut cursor).unwrap(); - // Find the packet_ack account (it's at index 4 in the accounts list) - let packet_ack_pubkey = ctx.instruction.accounts[4].pubkey; + // Find the packet_ack account (it's at index 5 after access_manager, router_state, ibc_app, client_sequence, packet_receipt) + let packet_ack_pubkey = ctx.instruction.accounts[5].pubkey; let ack_index = ctx .accounts .iter() @@ -959,4 +985,46 @@ mod tests { mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); } + + #[test] + fn test_recv_packet_fake_sysvar_wormhole_attack() { + let mut ctx = setup_recv_packet_test(true, 1000); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = + setup_fake_sysvar_attack(ctx.instruction, crate::ID); + ctx.instruction = instruction; + ctx.accounts.push(fake_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + mollusk.process_and_validate_instruction( + &ctx.instruction, + &ctx.accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_recv_packet_cpi_rejection() { + let mut ctx = setup_recv_packet_test(true, 1000); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = + setup_cpi_call_test(ctx.instruction, malicious_program); + ctx.instruction = instruction; + + // Remove the existing direct-call sysvar and replace with CPI sysvar + ctx.accounts + .retain(|(pubkey, _)| *pubkey != solana_sdk::sysvar::instructions::ID); + ctx.accounts.push(cpi_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); + } } diff --git a/programs/solana/programs/ics26-router/src/instructions/send_packet.rs b/programs/solana/programs/ics26-router/src/instructions/send_packet.rs index 0e9a92df1..4b79a20d7 100644 --- a/programs/solana/programs/ics26-router/src/instructions/send_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/send_packet.rs @@ -162,14 +162,12 @@ mod tests { } fn setup_send_packet_test_with_params(params: SendPacketTestParams) -> SendPacketTestContext { - let authority = Pubkey::new_unique(); let app_program_id = params.app_program_id.unwrap_or_else(Pubkey::new_unique); let payer = Pubkey::new_unique(); - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); let (client_pda, client_data) = setup_client( params.client_id, - authority, Pubkey::new_unique(), "counterparty-client", params.active_client, @@ -445,18 +443,16 @@ mod tests { #[test] fn test_send_packet_independent_client_sequences() { // Test that two different clients have independent sequence counters - let authority = Pubkey::new_unique(); let app_program_id = Pubkey::new_unique(); let port_id = "test-port"; - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); let (ibc_app_pda, ibc_app_data) = setup_ibc_app(port_id, app_program_id); // Create first client with sequence 10 let client_id_1 = "test-client-1"; let (client_pda_1, client_data_1) = setup_client( client_id_1, - authority, Pubkey::new_unique(), "counterparty-client-1", true, @@ -468,7 +464,6 @@ mod tests { let client_id_2 = "test-client-2"; let (client_pda_2, client_data_2) = setup_client( client_id_2, - authority, Pubkey::new_unique(), "counterparty-client-2", true, diff --git a/programs/solana/programs/ics26-router/src/instructions/set_access_manager.rs b/programs/solana/programs/ics26-router/src/instructions/set_access_manager.rs new file mode 100644 index 000000000..8766a54e8 --- /dev/null +++ b/programs/solana/programs/ics26-router/src/instructions/set_access_manager.rs @@ -0,0 +1,218 @@ +use crate::state::RouterState; +use anchor_lang::prelude::*; +use solana_ibc_types::events::AccessManagerUpdated; + +#[derive(Accounts)] +pub struct SetAccessManager<'info> { + #[account( + mut, + seeds = [RouterState::SEED], + bump + )] + pub router_state: Account<'info, RouterState>, + + /// CHECK: Validated via seeds constraint using the stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + + pub admin: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn set_access_manager( + ctx: Context, + new_access_manager: Pubkey, +) -> Result<()> { + let old_access_manager = ctx.accounts.router_state.access_manager; + + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ADMIN_ROLE, + &ctx.accounts.admin, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + + ctx.accounts.router_state.access_manager = new_access_manager; + + emit!(AccessManagerUpdated { + old_access_manager, + new_access_manager, + }); + + msg!( + "Access manager updated from {} to {}", + old_access_manager, + new_access_manager + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use access_manager::AccessManagerError; + use mollusk_svm::result::Check; + use solana_ibc_types::roles; + use solana_sdk::instruction::AccountMeta; + + #[test] + fn test_set_access_manager_success() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (router_state_pda, router_state_account) = create_initialized_router_state(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (router_state_pda, router_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + create_instructions_sysvar_account_with_caller(crate::ID), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let router_state = get_router_state_from_result(&result, &router_state_pda); + assert_eq!(router_state.access_manager, new_access_manager); + } + + #[test] + fn test_set_access_manager_not_admin() { + let admin = Pubkey::new_unique(); + let non_admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (router_state_pda, router_state_account) = create_initialized_router_state(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(non_admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (router_state_pda, router_state_account), + (access_manager_pda, access_manager_account), + (non_admin, create_signer_account()), + create_instructions_sysvar_account_with_caller(crate::ID), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_set_access_manager_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (router_state_pda, router_state_account) = create_initialized_router_state(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (router_state_pda, router_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + fake_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_set_access_manager_cpi_rejection() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (router_state_pda, router_state_account) = create_initialized_router_state(); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (router_state_pda, router_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + cpi_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } +} diff --git a/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs b/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs index cba7408e9..7acda34b9 100644 --- a/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs +++ b/programs/solana/programs/ics26-router/src/instructions/timeout_packet.rs @@ -19,6 +19,15 @@ pub struct TimeoutPacket<'info> { )] pub router_state: Account<'info, RouterState>, + /// Global access control account (owned by access-manager program) + /// CHECK: Validated by seeds constraint using stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = router_state.access_manager, + )] + pub access_manager: AccountInfo<'info>, + // Note: Port validation is done in the handler function to avoid Anchor macro issues pub ibc_app: Account<'info, IBCApp>, @@ -82,17 +91,20 @@ pub fn timeout_packet<'info>( ctx: Context<'_, '_, '_, 'info, TimeoutPacket<'info>>, msg: MsgTimeoutPacket, ) -> Result<()> { + // Performs: CPI rejection + signer verification + role check + // Ethereum: ICS26Router.sol:266 - timeoutPacket restricted to RELAYER_ROLE + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::RELAYER_ROLE, + &ctx.accounts.relayer, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + // TODO: Support multi-payload packets #602 - let router_state = &ctx.accounts.router_state; let packet_commitment_account = &ctx.accounts.packet_commitment; let client = &ctx.accounts.client; - require_keys_eq!( - ctx.accounts.relayer.key(), - router_state.authority, - RouterError::UnauthorizedSender - ); - require_eq!( &msg.packet.source_client, &client.client_id, @@ -229,7 +241,7 @@ mod tests { use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; - use solana_ibc_types::{Payload, PayloadMetadata, ProofMetadata}; + use solana_ibc_types::{roles, Payload, PayloadMetadata, ProofMetadata}; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; @@ -283,10 +295,9 @@ mod tests { let app_program_id = params.app_program_id.unwrap_or(MOCK_IBC_APP_PROGRAM_ID); let light_client_program = MOCK_LIGHT_CLIENT_ID; - let (router_state_pda, router_state_data) = setup_router_state(authority); + let (router_state_pda, router_state_data) = setup_router_state(); let (client_pda, client_data) = setup_client( params.source_client_id, - authority, light_client_program, params.dest_client_id, params.active_client, @@ -357,8 +368,13 @@ mod tests { test_proof, ); + // Setup access control with the authority having RELAYER_ROLE + let (access_manager_pda, access_manager_data) = + setup_access_manager_with_roles(&[(roles::RELAYER_ROLE, &[authority])]); + let mut instruction_accounts = vec![ AccountMeta::new_readonly(router_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(ibc_app_pda, false), AccountMeta::new(packet_commitment_pda, false), AccountMeta::new_readonly(app_program_id, false), @@ -410,6 +426,7 @@ mod tests { let mut accounts = vec![ create_account(router_state_pda, router_state_data, crate::ID), + create_account(access_manager_pda, access_manager_data, access_manager::ID), create_account(ibc_app_pda, ibc_app_data, crate::ID), packet_commitment_account, create_bpf_program_account(app_program_id), @@ -417,11 +434,12 @@ mod tests { create_bpf_program_account(crate::ID), // router_program create_system_account(relayer), // relayer (also signer) create_program_account(system_program::ID), - create_instructions_sysvar_account(), + create_instructions_sysvar_account_with_caller(crate::ID), create_account(client_pda, client_data, crate::ID), create_bpf_program_account(light_client_program), create_account(client_state, vec![0u8; 100], light_client_program), create_account(consensus_state, vec![0u8; 100], light_client_program), + create_program_account(access_manager::ID), ]; // Add chunk accounts as remaining accounts @@ -482,10 +500,10 @@ mod tests { ..Default::default() }); - let mollusk = Mollusk::new(&crate::ID, crate::test_utils::get_router_program_path()); + let mollusk = setup_mollusk_with_mock_programs(); let checks = vec![Check::err(ProgramError::Custom( - ANCHOR_ERROR_OFFSET + RouterError::UnauthorizedSender as u32, + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::Unauthorized as u32, ))]; mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); @@ -638,4 +656,46 @@ mod tests { mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); } + + #[test] + fn test_timeout_packet_fake_sysvar_wormhole_attack() { + let mut ctx = setup_timeout_packet_test_with_params(TimeoutPacketTestParams::default()); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = + setup_fake_sysvar_attack(ctx.instruction, crate::ID); + ctx.instruction = instruction; + ctx.accounts.push(fake_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + mollusk.process_and_validate_instruction( + &ctx.instruction, + &ctx.accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_timeout_packet_cpi_rejection() { + let mut ctx = setup_timeout_packet_test_with_params(TimeoutPacketTestParams::default()); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = + setup_cpi_call_test(ctx.instruction, malicious_program); + ctx.instruction = instruction; + + // Remove the existing direct-call sysvar and replace with CPI sysvar + ctx.accounts + .retain(|(pubkey, _)| *pubkey != solana_sdk::sysvar::instructions::ID); + ctx.accounts.push(cpi_sysvar_account); + + let mollusk = setup_mollusk_with_mock_programs(); + + // When CPI is detected by access_manager::require_role, it returns AccessManagerError::CpiNotAllowed (6005) + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&ctx.instruction, &ctx.accounts, &checks); + } } diff --git a/programs/solana/programs/ics26-router/src/lib.rs b/programs/solana/programs/ics26-router/src/lib.rs index 0e9135964..297291c19 100644 --- a/programs/solana/programs/ics26-router/src/lib.rs +++ b/programs/solana/programs/ics26-router/src/lib.rs @@ -22,8 +22,8 @@ declare_id!("FRGF7cthWUvDvAHMUARUHFycyUK2VDUtBchmkwrz7hgx"); pub mod ics26_router { use super::*; - pub fn initialize(ctx: Context, authority: Pubkey) -> Result<()> { - instructions::initialize(ctx, authority) + pub fn initialize(ctx: Context, access_manager: Pubkey) -> Result<()> { + instructions::initialize(ctx, access_manager) } pub fn add_ibc_app(ctx: Context, port_id: String) -> Result<()> { @@ -88,4 +88,11 @@ pub mod ics26_router { ) -> Result<()> { instructions::cleanup_chunks(ctx, msg) } + + pub fn set_access_manager( + ctx: Context, + new_access_manager: Pubkey, + ) -> Result<()> { + instructions::set_access_manager(ctx, new_access_manager) + } } diff --git a/programs/solana/programs/ics26-router/src/state.rs b/programs/solana/programs/ics26-router/src/state.rs index 2d4578a6e..dae1fb00f 100644 --- a/programs/solana/programs/ics26-router/src/state.rs +++ b/programs/solana/programs/ics26-router/src/state.rs @@ -11,14 +11,15 @@ pub const MIN_PORT_ID_LENGTH: usize = 2; pub const MAX_PORT_ID_LENGTH: usize = 128; /// Router state account -/// TODO: Implement multi-router ACL #[account] #[derive(InitSpace)] pub struct RouterState { /// Schema version for upgrades pub version: AccountVersion, - /// Authority that can perform restricted operations - pub authority: Pubkey, + /// Whether the router is paused (emergency stop) + pub paused: bool, + /// Access manager program ID for role-based access control + pub access_manager: Pubkey, /// Reserved space for future fields pub _reserved: [u8; 256], } @@ -61,8 +62,6 @@ pub struct Client { pub client_program_id: Pubkey, /// Counterparty chain information pub counterparty_info: CounterpartyInfo, - /// Authority that registered this client - pub authority: Pubkey, /// Whether the client is active pub active: bool, /// Reserved space for future fields @@ -79,7 +78,6 @@ impl Client { client_id: self.client_id.clone(), client_program_id: self.client_program_id, counterparty_info: self.counterparty_info.clone(), - authority: self.authority, active: self.active, _reserved: self._reserved, } @@ -217,9 +215,36 @@ mod compatibility_tests { assert_eq!(Client::SEED, solana_ibc_types::Client::SEED); } - /// Ensures `RouterState` in this program remains compatible with marker type pattern + /// Ensures `RouterState` in this program remains compatible with `solana_ibc_types::RouterState` + /// This is critical because the relayer deserializes on-chain `RouterState` accounts + /// using `solana_ibc_types::RouterState` #[test] - fn test_router_state_seed_compatibility() { + fn test_router_state_serialization_compatibility() { + let router_state = RouterState { + version: AccountVersion::V1, + paused: false, + access_manager: Pubkey::new_unique(), + _reserved: [0; 256], + }; + + // Serialize the program's RouterState + // Note: try_to_vec() doesn't include discriminator - that's only added by Anchor + // when writing to on-chain accounts + let serialized = router_state.try_to_vec().unwrap(); + + // Deserialize as solana_ibc_types::RouterState to verify compatibility + let types_router_state: solana_ibc_types::RouterState = + AnchorDeserialize::deserialize(&mut &serialized[..]).unwrap(); + + // Verify all fields match + assert_eq!(router_state.paused, types_router_state.paused); + assert_eq!( + router_state.access_manager, + types_router_state.access_manager + ); + assert_eq!(router_state._reserved, types_router_state._reserved); + + // Verify SEED constant matches assert_eq!(RouterState::SEED, solana_ibc_types::RouterState::SEED); } diff --git a/programs/solana/programs/ics26-router/src/test_utils.rs b/programs/solana/programs/ics26-router/src/test_utils.rs index 655336a50..e2c4f62fb 100644 --- a/programs/solana/programs/ics26-router/src/test_utils.rs +++ b/programs/solana/programs/ics26-router/src/test_utils.rs @@ -1,23 +1,18 @@ use crate::constants::ANCHOR_DISCRIMINATOR_SIZE; use crate::state::*; +use access_manager::RoleData; use anchor_lang::{AccountDeserialize, AnchorSerialize, Discriminator, Space}; -use solana_ibc_types::Payload; +use solana_ibc_types::{roles, Payload}; use solana_sdk::pubkey::Pubkey; use solana_sdk::sysvar::Sysvar; pub const ANCHOR_ERROR_OFFSET: u32 = 6000; -// Mock light client program ID - must match the ID in mock-light-client/src/lib.rs -pub const MOCK_LIGHT_CLIENT_ID: Pubkey = - solana_sdk::pubkey!("CSLS3A9jS7JAD8aUe3LRXMYZ1U8Lvxn9usGygVrA2arZ"); - -// Dummy IBC app program ID - must match the ID in dummy-ibc-app/src/lib.rs -pub const DUMMY_IBC_APP_PROGRAM_ID: Pubkey = - solana_sdk::pubkey!("5E73beFMq9QZvbwPN5i84psh2WcyJ9PgqF4avBaRDgCC"); - -// Mock IBC app program ID - must match the ID in mock-ibc-app/src/lib.rs -pub const MOCK_IBC_APP_PROGRAM_ID: Pubkey = - solana_sdk::pubkey!("9qnEj3T1NsaGkN3Sj7hgJZiKrVbKVBNmVphJ6PW1PDAB"); +// Import program IDs directly from their lib.rs files +// These automatically stay in sync with `anchor keys sync` +pub use dummy_ibc_app::ID as DUMMY_IBC_APP_PROGRAM_ID; +pub use mock_ibc_app::ID as MOCK_IBC_APP_PROGRAM_ID; +pub use mock_light_client::ID as MOCK_LIGHT_CLIENT_ID; pub fn get_router_program_path() -> &'static str { use std::sync::OnceLock; @@ -56,11 +51,12 @@ pub fn create_account_data(account: &T) -> V data } -pub fn setup_router_state(authority: Pubkey) -> (Pubkey, Vec) { +pub fn setup_router_state() -> (Pubkey, Vec) { let (router_state_pda, _) = Pubkey::find_program_address(&[RouterState::SEED], &crate::ID); let router_state = RouterState { version: AccountVersion::V1, - authority, + paused: false, + access_manager: access_manager::ID, _reserved: [0; 256], }; let router_state_data = create_account_data(&router_state); @@ -69,7 +65,6 @@ pub fn setup_router_state(authority: Pubkey) -> (Pubkey, Vec) { pub fn setup_client( client_id: &str, - authority: Pubkey, light_client_program: Pubkey, counterparty_client_id: &str, active: bool, @@ -85,7 +80,6 @@ pub fn setup_client( client_id: counterparty_client_id.to_string(), merkle_prefix: vec![vec![0x01, 0x02, 0x03]], }, - authority, active, _reserved: [0; 256], }; @@ -120,6 +114,38 @@ pub fn setup_ibc_app(port_id: &str, app_program_id: Pubkey) -> (Pubkey, Vec) (ibc_app_pda, ibc_app_data) } +pub fn setup_access_manager(relayers: Vec) -> (Pubkey, Vec) { + setup_access_manager_with_roles(&[(solana_ibc_types::roles::RELAYER_ROLE, relayers.as_slice())]) +} + +pub fn setup_access_manager_with_roles(roles: &[(u64, &[Pubkey])]) -> (Pubkey, Vec) { + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + let mut role_data: Vec = roles + .iter() + .map(|(role_id, members)| RoleData { + role_id: *role_id, + members: members.to_vec(), + }) + .collect(); + + // Ensure ADMIN_ROLE exists with at least one member + if !role_data.iter().any(|r| r.role_id == roles::ADMIN_ROLE) { + role_data.push(RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![Pubkey::new_unique()], + }); + } + + let access_manager = access_manager::state::AccessManager { roles: role_data }; + + let mut data = access_manager::state::AccessManager::DISCRIMINATOR.to_vec(); + access_manager.serialize(&mut data).unwrap(); + + (access_manager_pda, data) +} + pub fn create_test_packet( sequence: u64, source_client: &str, @@ -343,6 +369,40 @@ pub fn create_program_account(pubkey: Pubkey) -> (Pubkey, solana_sdk::account::A ) } +pub fn create_system_account_with_lamports( + pubkey: Pubkey, + lamports: u64, +) -> (Pubkey, solana_sdk::account::Account) { + ( + pubkey, + solana_sdk::account::Account { + lamports, + data: vec![], + owner: solana_sdk::system_program::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +pub fn create_account_with_lamports( + pubkey: Pubkey, + owner: &Pubkey, + lamports: u64, + data_len: usize, +) -> (Pubkey, solana_sdk::account::Account) { + ( + pubkey, + solana_sdk::account::Account { + lamports, + data: vec![0; data_len], + owner: *owner, + executable: false, + rent_epoch: 0, + }, + ) +} + pub fn create_uninitialized_commitment_account( pubkey: Pubkey, ) -> (Pubkey, solana_sdk::account::Account) { @@ -475,7 +535,7 @@ pub fn get_client_sequence_from_result_by_pubkey( /// Setup mollusk with mock programs for testing /// -/// This adds the router, mock light client, and mock IBC app programs to mollusk +/// This adds the router, mock light client, mock IBC app, and access control programs to mollusk pub fn setup_mollusk_with_mock_programs() -> mollusk_svm::Mollusk { use mollusk_svm::Mollusk; @@ -490,12 +550,17 @@ pub fn setup_mollusk_with_mock_programs() -> mollusk_svm::Mollusk { get_mock_ibc_app_program_path(), &solana_sdk::bpf_loader_upgradeable::ID, ); + mollusk.add_program( + &access_manager::ID, + access_manager::get_access_manager_program_path(), + &solana_sdk::bpf_loader_upgradeable::ID, + ); mollusk } /// Setup mollusk with just the mock light client for testing scenarios that don't need IBC apps /// -/// This adds the router and mock light client programs to mollusk +/// This adds the router, mock light client, and access control programs to mollusk pub fn setup_mollusk_with_light_client() -> mollusk_svm::Mollusk { use mollusk_svm::Mollusk; @@ -505,6 +570,11 @@ pub fn setup_mollusk_with_light_client() -> mollusk_svm::Mollusk { get_mock_client_program_path(), &solana_sdk::bpf_loader_upgradeable::ID, ); + mollusk.add_program( + &access_manager::ID, + access_manager::get_access_manager_program_path(), + &solana_sdk::bpf_loader_upgradeable::ID, + ); mollusk } @@ -662,3 +732,198 @@ fn get_error_code(error: &anchor_lang::prelude::ProgramError) -> Option { _ => None, } } + +/// Create initialized router state for tests +pub fn create_initialized_router_state() -> (Pubkey, solana_sdk::account::Account) { + let (router_state_pda, router_state_data) = setup_router_state(); + + ( + router_state_pda, + solana_sdk::account::Account { + lamports: 1_000_000, + data: router_state_data, + owner: crate::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +/// Create access manager with a specific role +pub fn create_access_manager_with_role( + admin: Pubkey, + role_id: u64, + member: Pubkey, +) -> (Pubkey, solana_sdk::account::Account) { + let admin_members = [admin]; + let role_members = [member]; + + let roles: &[(u64, &[Pubkey])] = + if role_id == solana_ibc_types::roles::ADMIN_ROLE && member == admin { + &[(role_id, &role_members[..])] + } else { + &[ + (solana_ibc_types::roles::ADMIN_ROLE, &admin_members[..]), + (role_id, &role_members[..]), + ] + }; + + let (pda, data) = setup_access_manager_with_roles(roles); + + ( + pda, + solana_sdk::account::Account { + lamports: 1_000_000, + data, + owner: access_manager::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + +/// Build instruction for router program +pub fn build_instruction( + instruction_data: T, + accounts: Vec, +) -> solana_sdk::instruction::Instruction { + solana_sdk::instruction::Instruction { + program_id: crate::ID, + accounts, + data: instruction_data.data(), + } +} + +/// Create signer account for tests +pub fn create_signer_account() -> solana_sdk::account::Account { + solana_sdk::account::Account { + lamports: 1_000_000_000, + data: vec![], + owner: solana_sdk::system_program::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Setup mollusk for tests +pub fn setup_mollusk() -> mollusk_svm::Mollusk { + use mollusk_svm::Mollusk; + + let mut mollusk = Mollusk::new(&crate::ID, get_router_program_path()); + mollusk.add_program( + &access_manager::ID, + access_manager::get_access_manager_program_path(), + &solana_sdk::bpf_loader_upgradeable::ID, + ); + mollusk +} + +/// Get router state from mollusk instruction result +pub fn get_router_state_from_result( + result: &mollusk_svm::result::InstructionResult, + pda: &Pubkey, +) -> RouterState { + use anchor_lang::AccountDeserialize; + + let account = result + .resulting_accounts + .iter() + .find(|(pubkey, _)| pubkey == pda) + .map(|(_, account)| account) + .expect("Router state account not found"); + + RouterState::try_deserialize(&mut &account.data[..]) + .expect("Failed to deserialize router state") +} + +/// Helper for testing Wormhole-style fake sysvar attacks +/// Automatically finds and replaces the instructions sysvar with a fake one +/// Returns (`modified_instruction`, `fake_sysvar_account_tuple`) +pub fn setup_fake_sysvar_attack( + mut instruction: solana_sdk::instruction::Instruction, + program_id: Pubkey, +) -> ( + solana_sdk::instruction::Instruction, + (Pubkey, solana_sdk::account::Account), +) { + let (fake_sysvar_pubkey, fake_sysvar_account) = + create_fake_instructions_sysvar_account(program_id); + + // Find the instructions sysvar account and replace it with the fake one + let sysvar_account_index = instruction + .accounts + .iter() + .position(|acc| acc.pubkey == solana_sdk::sysvar::instructions::ID) + .expect("Instructions sysvar account not found in instruction"); + + instruction.accounts[sysvar_account_index] = + solana_sdk::instruction::AccountMeta::new_readonly(fake_sysvar_pubkey, false); + + (instruction, (fake_sysvar_pubkey, fake_sysvar_account)) +} + +/// Expected error for Wormhole-style sysvar attacks (Anchor's address constraint violation) +pub fn expect_sysvar_attack_error() -> mollusk_svm::result::Check<'static> { + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ErrorCode::ConstraintAddress as u32, + )) +} + +/// Create instructions sysvar that simulates a CPI call from another program +/// Uses the REAL sysvar address but with a different `program_id` to simulate CPI context +pub fn create_cpi_instructions_sysvar_account( + caller_program_id: Pubkey, +) -> solana_sdk::account::Account { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &caller_program_id, // Different program calling via CPI + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + solana_sdk::account::Account { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Helper for testing CPI rejection +/// Replaces the instructions sysvar with one that simulates a CPI call +/// Returns (`modified_instruction`, `cpi_sysvar_account_tuple`) +pub fn setup_cpi_call_test( + instruction: solana_sdk::instruction::Instruction, + caller_program_id: Pubkey, +) -> ( + solana_sdk::instruction::Instruction, + (Pubkey, solana_sdk::account::Account), +) { + let cpi_sysvar_account = create_cpi_instructions_sysvar_account(caller_program_id); + + // Use the REAL sysvar address (unlike Wormhole attack which uses fake) + ( + instruction, + (solana_sdk::sysvar::instructions::ID, cpi_sysvar_account), + ) +} + +/// Expected error for CPI rejection (`UnauthorizedCaller` from `reject_cpi`) +pub fn expect_cpi_rejection_error() -> mollusk_svm::result::Check<'static> { + use solana_ibc_types::CpiValidationError; + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ERROR_CODE_OFFSET + CpiValidationError::UnauthorizedCaller as u32, + )) +} diff --git a/programs/solana/programs/ics26-router/src/utils/ics24.rs b/programs/solana/programs/ics26-router/src/utils/ics24.rs index 2ff5fe7c0..c2e4c496e 100644 --- a/programs/solana/programs/ics26-router/src/utils/ics24.rs +++ b/programs/solana/programs/ics26-router/src/utils/ics24.rs @@ -1,9 +1,9 @@ use crate::errors::RouterError; use crate::state::Packet; use anchor_lang::prelude::*; -use anchor_lang::solana_program::keccak::hash as keccak256; use sha2::{Digest, Sha256}; use solana_ibc_types::Payload; +use solana_program::keccak::hash as keccak256; // Include auto-generated constants from build.rs include!(concat!(env!("OUT_DIR"), "/constants.rs")); diff --git a/programs/solana/programs/ics26-router/tests/upgrade_migration_example.rs b/programs/solana/programs/ics26-router/tests/upgrade_migration_example.rs index 537ae8f48..37a3c02f7 100644 --- a/programs/solana/programs/ics26-router/tests/upgrade_migration_example.rs +++ b/programs/solana/programs/ics26-router/tests/upgrade_migration_example.rs @@ -16,12 +16,13 @@ fn create_account_data(account: &T) -> Vec (Pubkey, Vec) { +fn setup_router_state() -> (Pubkey, Vec) { let (router_state_pda, _) = Pubkey::find_program_address(&[RouterState::SEED], &ics26_router::ID); let router_state = RouterState { version: AccountVersion::V1, - authority, + paused: false, + access_manager: access_manager::ID, _reserved: [0; 256], }; let router_state_data = create_account_data(&router_state); @@ -30,7 +31,6 @@ fn setup_router_state(authority: Pubkey) -> (Pubkey, Vec) { fn setup_client_state( client_id: &str, - authority: Pubkey, light_client_program: Pubkey, counterparty_client_id: &str, active: bool, @@ -46,7 +46,6 @@ fn setup_client_state( client_id: counterparty_client_id.to_string(), merkle_prefix: vec![vec![0x01, 0x02, 0x03]], }, - authority, active, _reserved: [0; 256], }; @@ -61,12 +60,18 @@ pub enum AccountVersionExample { V2, // New version added } +/// Example V2 `RouterState` demonstrating data migration pattern. +/// +/// NOTE: Authorization for upgrades is handled by `AccessManager` (`ADMIN_ROLE`). +/// This test focuses on data serialization/migration, not authorization. #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct RouterStateExample { /// Schema version for upgrades pub version: AccountVersionExample, - /// Authority that can perform restricted operations - pub authority: Pubkey, + /// Whether the router is paused (existing V1 field) + pub paused: bool, + /// Access manager program ID (existing V1 field) + pub access_manager: Pubkey, // ========== NEW V2 FIELDS ========== /// Fee collector account @@ -78,6 +83,8 @@ pub struct RouterStateExample { pub _reserved: [u8; 215], } +/// Example V2 Client demonstrating data migration pattern. +/// NOTE: Authorization is handled by `AccessManager`, not stored per-client. #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct ClientExample { /// Schema version for upgrades @@ -88,8 +95,6 @@ pub struct ClientExample { pub client_program_id: Pubkey, /// Counterparty chain information pub counterparty_info: CounterpartyInfo, - /// Authority that registered this client - pub authority: Pubkey, /// Whether the client is active pub active: bool, @@ -106,8 +111,7 @@ pub struct ClientExample { #[test] fn test_router_state_migration_v1_to_v2() { // Create V1 account - let authority = Pubkey::new_unique(); - let (_, v1_data) = setup_router_state(authority); + let (_, v1_data) = setup_router_state(); // Deserialize account into the struct with new added fields let mut cursor = &v1_data[8..]; // Skip discriminator @@ -115,6 +119,7 @@ fn test_router_state_migration_v1_to_v2() { // Verify it's V1 assert_eq!(state.version, AccountVersionExample::V1); + assert!(!state.paused); // V1 field preserved // Here the actual migration logic would be done state.version = AccountVersionExample::V2; @@ -123,7 +128,7 @@ fn test_router_state_migration_v1_to_v2() { // Verify migration assert_eq!(state.version, AccountVersionExample::V2); - assert_eq!(state.authority, authority); + assert!(!state.paused); // V1 field still preserved assert!(state.fee_collector.is_some()); assert_eq!(state.global_rate_limit, 10); assert_eq!(state._reserved.len(), 215); @@ -133,11 +138,10 @@ fn test_router_state_migration_v1_to_v2() { fn test_client_migration_v1_to_v2() { // Create V1 client let client_id = "07-tendermint-0"; - let authority = Pubkey::new_unique(); let light_client = Pubkey::new_unique(); let counterparty = "07-tendermint-1"; - let (_, v1_data) = setup_client_state(client_id, authority, light_client, counterparty, true); + let (_, v1_data) = setup_client_state(client_id, light_client, counterparty, true); // Deserialize V1 account let mut cursor = &v1_data[8..]; // Skip discriminator @@ -155,7 +159,6 @@ fn test_client_migration_v1_to_v2() { assert_eq!(state.version, AccountVersionExample::V2); assert_eq!(state.client_id, client_id); assert_eq!(state.client_program_id, light_client); - assert_eq!(state.authority, authority); assert!(state.active); assert_eq!(state.counterparty_info.client_id, counterparty); diff --git a/programs/solana/programs/ics27-gmp/Cargo.toml b/programs/solana/programs/ics27-gmp/Cargo.toml index 7f3b45166..8c1b4e2a7 100644 --- a/programs/solana/programs/ics27-gmp/Cargo.toml +++ b/programs/solana/programs/ics27-gmp/Cargo.toml @@ -27,10 +27,12 @@ idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] [dependencies] anchor-lang = { workspace = true, features = ["init-if-needed"] } anchor-spl.workspace = true +solana-program.workspace = true solana-ibc-types.workspace = true solana-ibc-proto.workspace = true solana-ibc-macros.workspace = true ics26-router = { workspace = true, features = ["cpi"] } +access-manager = { workspace = true, features = ["cpi"] } [dev-dependencies] mollusk-svm.workspace = true diff --git a/programs/solana/programs/ics27-gmp/src/events.rs b/programs/solana/programs/ics27-gmp/src/events.rs index e31dd4753..303e3ddd9 100644 --- a/programs/solana/programs/ics27-gmp/src/events.rs +++ b/programs/solana/programs/ics27-gmp/src/events.rs @@ -5,8 +5,6 @@ use anchor_lang::prelude::*; pub struct GMPAppInitialized { /// Router program managing this app pub router_program: Pubkey, - /// Administrative authority - pub authority: Pubkey, /// Port ID bound to this app pub port_id: String, /// App initialization timestamp diff --git a/programs/solana/programs/ics27-gmp/src/instructions/admin.rs b/programs/solana/programs/ics27-gmp/src/instructions/admin.rs index 5a3d10371..b92de26df 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/admin.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/admin.rs @@ -1,5 +1,4 @@ use crate::constants::*; -use crate::errors::GMPError; use crate::events::{GMPAppPaused, GMPAppUnpaused}; use crate::state::GMPAppState; use anchor_lang::prelude::*; @@ -15,13 +14,33 @@ pub struct PauseApp<'info> { )] pub app_state: Account<'info, GMPAppState>, + /// CHECK: Validated via seeds constraint using stored `access_manager` program ID #[account( - constraint = authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = app_state.access_manager )] + pub access_manager: AccountInfo<'info>, + pub authority: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } pub fn pause_app(ctx: Context) -> Result<()> { + // Ethereum: ICS20Transfer.sol:116 - pause() restricted to PAUSER_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::PAUSER_ROLE, + &ctx.accounts.authority, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + let clock = Clock::get()?; let app_state = &mut ctx.accounts.app_state; @@ -48,13 +67,33 @@ pub struct UnpauseApp<'info> { )] pub app_state: Account<'info, GMPAppState>, + /// CHECK: Validated via seeds constraint using stored `access_manager` program ID using stored `access_manager` program ID #[account( - constraint = authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = app_state.access_manager )] + pub access_manager: AccountInfo<'info>, + pub authority: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } pub fn unpause_app(ctx: Context) -> Result<()> { + // Ethereum: ICS20Transfer.sol:121 - unpause() restricted to UNPAUSER_ROLE + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::UNPAUSER_ROLE, + &ctx.accounts.authority, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + let clock = Clock::get()?; let app_state = &mut ctx.accounts.app_state; @@ -73,48 +112,16 @@ pub fn unpause_app(ctx: Context) -> Result<()> { Ok(()) } -/// Update app authority (admin only) -#[derive(Accounts)] -pub struct UpdateAuthority<'info> { - /// App state account - validated by Anchor PDA constraints - #[account( - mut, - seeds = [GMPAppState::SEED, GMP_PORT_ID.as_bytes()], - bump = app_state.bump - )] - pub app_state: Account<'info, GMPAppState>, - - #[account( - constraint = current_authority.key() == app_state.authority @ GMPError::UnauthorizedAdmin - )] - pub current_authority: Signer<'info>, - - /// CHECK: New authority can be any valid Pubkey - pub new_authority: AccountInfo<'info>, -} - -pub fn update_authority(ctx: Context) -> Result<()> { - let app_state = &mut ctx.accounts.app_state; - let old_authority = app_state.authority; - - app_state.authority = ctx.accounts.new_authority.key(); - - msg!( - "GMP app authority updated: {} -> {}", - old_authority, - app_state.authority - ); - - Ok(()) -} - #[cfg(test)] mod tests { use super::*; use crate::state::{AccountVersion, GMPAppState}; use crate::test_utils::*; use anchor_lang::InstructionData; + use mollusk_svm::result::Check; use mollusk_svm::Mollusk; + use solana_ibc_types::roles; + use solana_sdk::program_error::ProgramError; use solana_sdk::{ account::Account as SolanaAccount, instruction::{AccountMeta, Instruction}, @@ -130,20 +137,21 @@ mod tests { fn test_initialize_success() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); + let payer = Pubkey::new_unique(); let (app_state_pda, _bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - let payer = authority; - let instruction_data = crate::instruction::Initialize {}; + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; let instruction = Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state_pda, false), AccountMeta::new(payer, true), - AccountMeta::new_readonly(authority, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -152,6 +160,7 @@ mod tests { create_pda_for_init(app_state_pda), create_payer_account(payer), create_system_program_account(), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let result = mollusk.process_instruction(&instruction, &accounts); @@ -167,7 +176,8 @@ mod tests { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); let authority = Pubkey::new_unique(); - + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::PAUSER_ROLE, &[authority])]); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); @@ -177,7 +187,9 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -185,11 +197,12 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, false, // not paused ), + (access_manager_pda, access_manager_account), create_authority_account(authority), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let result = mollusk.process_instruction(&instruction, &accounts); @@ -210,15 +223,16 @@ mod tests { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); let authority = Pubkey::new_unique(); - + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::UNPAUSER_ROLE, &[authority])]); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); let app_state = GMPAppState { version: AccountVersion::V1, - authority, paused: true, bump: app_state_bump, + access_manager: access_manager::ID, _reserved: [0; 256], }; @@ -232,7 +246,9 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -248,7 +264,9 @@ mod tests { rent_epoch: 0, }, ), + (access_manager_pda, access_manager_account), create_authority_account(authority), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let result = mollusk.process_instruction(&instruction, &accounts); @@ -270,7 +288,8 @@ mod tests { let authority = Pubkey::new_unique(); let wrong_authority = Pubkey::new_unique(); - + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::PAUSER_ROLE, &[authority])]); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); @@ -280,7 +299,9 @@ mod tests { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(wrong_authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; @@ -288,11 +309,12 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, false, // not paused ), + (access_manager_pda, access_manager_account), create_authority_account(wrong_authority), + create_instructions_sysvar_account(), ]; let result = mollusk.process_instruction(&instruction, &accounts); @@ -302,138 +324,210 @@ mod tests { ); } - // ======================================================================== - // Update Authority Tests // ======================================================================== #[test] - fn test_update_authority_success() { + fn test_pause_app_invalid_pda() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let current_authority = Pubkey::new_unique(); - let new_authority = Pubkey::new_unique(); - - let (app_state_pda, app_state_bump) = + let authority = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::PAUSER_ROLE, &[authority])]); + let (_correct_app_state_pda, _correct_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - let instruction_data = crate::instruction::UpdateAuthority {}; + // Use wrong PDA + let wrong_app_state_pda = Pubkey::new_unique(); + + let instruction_data = crate::instruction::PauseApp {}; let instruction = Instruction { program_id: crate::ID, accounts: vec![ - AccountMeta::new(app_state_pda, false), - AccountMeta::new_readonly(current_authority, true), - AccountMeta::new_readonly(new_authority, false), + AccountMeta::new(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; let accounts = vec![ + // Create account state at wrong PDA for testing create_gmp_app_state_account( - app_state_pda, - current_authority, - app_state_bump, + wrong_app_state_pda, + 255u8, false, // not paused ), - create_authority_account(current_authority), - create_authority_account(new_authority), + (access_manager_pda, access_manager_account), + create_authority_account(authority), + create_instructions_sysvar_account(), ]; let result = mollusk.process_instruction(&instruction, &accounts); assert!( - !result.program_result.is_err(), - "Update authority should succeed: {:?}", - result.program_result + result.program_result.is_err(), + "PauseApp should fail with invalid app_state PDA" ); + } - let app_state_account = result.get_account(&app_state_pda).unwrap(); - let app_state_data = &app_state_account.data[crate::constants::DISCRIMINATOR_SIZE..]; - let app_state = GMPAppState::try_from_slice(app_state_data).unwrap(); - assert_eq!( - app_state.authority, new_authority, - "Authority should be updated" + #[test] + fn test_pause_app_fake_sysvar_wormhole_attack() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::PAUSER_ROLE, &[authority])]); + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::PauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + create_gmp_app_state_account(app_state_pda, app_state_bump, false), + (access_manager_pda, access_manager_account), + create_authority_account(authority), + fake_sysvar_account, + ]; + + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], ); } #[test] - fn test_update_authority_unauthorized() { + fn test_pause_app_cpi_rejection() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let current_authority = Pubkey::new_unique(); - let wrong_authority = Pubkey::new_unique(); - let new_authority = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::PAUSER_ROLE, &[authority])]); + let (app_state_pda, app_state_bump) = + Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + let instruction_data = crate::instruction::PauseApp {}; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + create_gmp_app_state_account(app_state_pda, app_state_bump, false), + (access_manager_pda, access_manager_account), + create_authority_account(authority), + cpi_sysvar_account, + ]; + + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_unpause_app_fake_sysvar_wormhole_attack() { + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let authority = Pubkey::new_unique(); + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::UNPAUSER_ROLE, &[authority])]); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - let instruction_data = crate::instruction::UpdateAuthority {}; + let instruction_data = crate::instruction::UnpauseApp {}; let instruction = Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state_pda, false), - AccountMeta::new_readonly(wrong_authority, true), - AccountMeta::new_readonly(new_authority, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + let accounts = vec![ - create_gmp_app_state_account( - app_state_pda, - current_authority, - app_state_bump, - false, // not paused - ), - create_authority_account(wrong_authority), - create_authority_account(new_authority), + create_gmp_app_state_account(app_state_pda, app_state_bump, true), // paused + (access_manager_pda, access_manager_account), + create_authority_account(authority), + fake_sysvar_account, ]; - let result = mollusk.process_instruction(&instruction, &accounts); - assert!( - result.program_result.is_err(), - "Update authority should fail with wrong authority" + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], ); } #[test] - fn test_pause_app_invalid_pda() { + fn test_unpause_app_cpi_rejection() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); let authority = Pubkey::new_unique(); - - let (_correct_app_state_pda, _correct_bump) = + let (access_manager_pda, access_manager_account) = + setup_access_manager_with_roles(&[(roles::UNPAUSER_ROLE, &[authority])]); + let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - // Use wrong PDA - let wrong_app_state_pda = Pubkey::new_unique(); - - let instruction_data = crate::instruction::PauseApp {}; + let instruction_data = crate::instruction::UnpauseApp {}; let instruction = Instruction { program_id: crate::ID, accounts: vec![ - AccountMeta::new(wrong_app_state_pda, false), // Wrong PDA! + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), AccountMeta::new_readonly(authority, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), }; + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + let accounts = vec![ - // Create account state at wrong PDA for testing - create_gmp_app_state_account( - wrong_app_state_pda, - authority, - 255u8, - false, // not paused - ), + create_gmp_app_state_account(app_state_pda, app_state_bump, true), // paused + (access_manager_pda, access_manager_account), create_authority_account(authority), + cpi_sysvar_account, ]; - let result = mollusk.process_instruction(&instruction, &accounts); - assert!( - result.program_result.is_err(), - "PauseApp should fail with invalid app_state PDA" - ); + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + access_manager::AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); } } diff --git a/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs b/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs index daa212d24..d5314179e 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/initialize.rs @@ -1,7 +1,9 @@ use crate::constants::*; +use crate::errors::GMPError; use crate::events::GMPAppInitialized; use crate::state::{AccountVersion, GMPAppState}; use anchor_lang::prelude::*; +use solana_ibc_types::reject_cpi; /// Initialize the ICS27 GMP application #[derive(Accounts)] @@ -18,25 +20,31 @@ pub struct Initialize<'info> { #[account(mut)] pub payer: Signer<'info>, - pub authority: Signer<'info>, pub system_program: Program<'info, System>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, } -pub fn initialize(ctx: Context) -> Result<()> { +pub fn initialize(ctx: Context, access_manager: Pubkey) -> Result<()> { + // Reject CPI calls - this instruction must be called directly + reject_cpi(&ctx.accounts.instructions_sysvar, &crate::ID).map_err(GMPError::from)?; + let app_state = &mut ctx.accounts.app_state; let clock = Clock::get()?; // Initialize app state app_state.version = AccountVersion::V1; - app_state.authority = ctx.accounts.authority.key(); app_state.paused = false; app_state.bump = ctx.bumps.app_state; + app_state.access_manager = access_manager; app_state._reserved = [0; 256]; // Emit initialization event emit!(GMPAppInitialized { router_program: ics26_router::ID, - authority: app_state.authority, port_id: GMP_PORT_ID.to_string(), timestamp: clock.unix_timestamp, }); @@ -52,27 +60,27 @@ pub fn initialize(ctx: Context) -> Result<()> { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::*; use anchor_lang::InstructionData; use mollusk_svm::result::Check; use mollusk_svm::Mollusk; use solana_sdk::instruction::{AccountMeta, Instruction}; + use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; use solana_sdk::system_program; - fn create_initialize_instruction( - app_state: Pubkey, - payer: Pubkey, - authority: Pubkey, - ) -> Instruction { - let instruction_data = crate::instruction::Initialize {}; + fn create_initialize_instruction(app_state: Pubkey, payer: Pubkey) -> Instruction { + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(app_state, false), AccountMeta::new(payer, true), - AccountMeta::new_readonly(authority, true), AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), ], data: instruction_data.data(), } @@ -80,13 +88,12 @@ mod tests { #[test] fn test_initialize_success() { - let authority = Pubkey::new_unique(); - let payer = authority; + let payer = Pubkey::new_unique(); let (app_state_pda, _) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - let instruction = create_initialize_instruction(app_state_pda, payer, authority); + let instruction = create_initialize_instruction(app_state_pda, payer); let accounts = vec![ (app_state_pda, solana_sdk::account::Account::default()), @@ -107,6 +114,7 @@ mod tests { ..Default::default() }, ), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); @@ -121,13 +129,12 @@ mod tests { #[test] fn test_initialize_already_initialized() { - let authority = Pubkey::new_unique(); - let payer = authority; + let payer = Pubkey::new_unique(); let (app_state_pda, _) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); - let instruction = create_initialize_instruction(app_state_pda, payer, authority); + let instruction = create_initialize_instruction(app_state_pda, payer); // Create accounts that are already initialized (owned by program, not system) let accounts = vec![ @@ -157,6 +164,7 @@ mod tests { ..Default::default() }, ), + create_instructions_sysvar_account_with_caller(crate::ID), ]; let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); @@ -167,4 +175,116 @@ mod tests { "Initialize should fail when account already initialized" ); } + + #[test] + fn test_initialize_fake_sysvar_wormhole_attack() { + let payer = Pubkey::new_unique(); + + let (app_state_pda, _) = + Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (app_state_pda, solana_sdk::account::Account::default()), + ( + payer, + solana_sdk::account::Account { + lamports: 1_000_000_000, + owner: system_program::ID, + ..Default::default() + }, + ), + ( + system_program::ID, + solana_sdk::account::Account { + lamports: 1, + executable: true, + owner: solana_sdk::native_loader::ID, + ..Default::default() + }, + ), + fake_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_initialize_cpi_rejection() { + let payer = Pubkey::new_unique(); + + let (app_state_pda, _) = + Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + let instruction_data = crate::instruction::Initialize { + access_manager: access_manager::ID, + }; + + let instruction = Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new(payer, true), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + data: instruction_data.data(), + }; + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (app_state_pda, solana_sdk::account::Account::default()), + ( + payer, + solana_sdk::account::Account { + lamports: 1_000_000_000, + owner: system_program::ID, + ..Default::default() + }, + ), + ( + system_program::ID, + solana_sdk::account::Account { + lamports: 1, + executable: true, + owner: solana_sdk::native_loader::ID, + ..Default::default() + }, + ), + cpi_sysvar_account, + ]; + + let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); + + let checks = vec![Check::err(ProgramError::Custom( + ANCHOR_ERROR_OFFSET + crate::errors::GMPError::UnauthorizedRouter as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } } diff --git a/programs/solana/programs/ics27-gmp/src/instructions/mod.rs b/programs/solana/programs/ics27-gmp/src/instructions/mod.rs index 8a7d72c9a..916222814 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/mod.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/mod.rs @@ -4,6 +4,7 @@ pub mod on_ack_packet; pub mod on_recv_packet; pub mod on_timeout_packet; pub mod send_call; +pub mod set_access_manager; pub use admin::*; pub use initialize::*; @@ -11,3 +12,4 @@ pub use on_ack_packet::*; pub use on_recv_packet::*; pub use on_timeout_packet::*; pub use send_call::*; +pub use set_access_manager::*; diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs index 265bff61a..8c5dc64bd 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_ack_packet.rs @@ -102,7 +102,6 @@ mod tests { fn test_on_ack_packet_app_paused() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -111,7 +110,7 @@ mod tests { let instruction = create_ack_instruction(app_state_pda, router_program, payer); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, true), + create_gmp_app_state_account(app_state_pda, app_state_bump, true), create_router_program_account(router_program), create_instructions_sysvar_account_with_caller(router_program), create_authority_account(payer), @@ -129,7 +128,6 @@ mod tests { fn test_on_ack_packet_invalid_app_state_pda() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let port_id = "gmpport".to_string(); @@ -174,7 +172,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( wrong_app_state_pda, - authority, wrong_bump, false, // not paused ), @@ -194,7 +191,6 @@ mod tests { fn test_on_ack_packet_direct_call_rejected() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -203,7 +199,7 @@ mod tests { let instruction = create_ack_instruction(app_state_pda, router_program, payer); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), create_instructions_sysvar_account_with_caller(crate::ID), // Direct call create_authority_account(payer), @@ -221,7 +217,6 @@ mod tests { fn test_on_ack_packet_unauthorized_router() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -231,7 +226,7 @@ mod tests { let unauthorized_program = Pubkey::new_unique(); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), create_instructions_sysvar_account_with_caller(unauthorized_program), // Unauthorized create_authority_account(payer), @@ -249,7 +244,6 @@ mod tests { fn test_on_ack_packet_fake_sysvar_wormhole_attack() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -265,7 +259,7 @@ mod tests { instruction.accounts[2] = AccountMeta::new_readonly(fake_sysvar_pubkey, false); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), // Wormhole attack: provide a DIFFERENT account instead of the real sysvar (fake_sysvar_pubkey, fake_sysvar_account), diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs index fec016792..8a050c821 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_recv_packet.rs @@ -275,7 +275,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, true, // paused ), @@ -316,7 +315,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -363,7 +361,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -415,7 +412,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -443,7 +439,6 @@ mod tests { fn test_on_recv_packet_invalid_app_state_pda() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let port_id = "gmpport".to_string(); @@ -499,7 +494,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( wrong_app_state_pda, - authority, wrong_bump, false, // not paused ), @@ -566,7 +560,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -630,7 +623,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -694,7 +686,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -736,7 +727,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -788,7 +778,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -842,7 +831,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -896,7 +884,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -950,7 +937,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -993,7 +979,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -1055,7 +1040,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( ctx.app_state_pda, - ctx.authority, ctx.app_state_bump, false, // not paused ), @@ -1099,13 +1083,13 @@ mod tests { &bpf_loader_upgradeable::ID, ); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); // Create packet data that will call the counter app + let authority = Pubkey::new_unique(); let (client_id, sender, salt, gmp_account_pda) = create_test_account_data(); // Counter app state and user counter PDAs @@ -1226,7 +1210,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, false, // not paused ), @@ -1373,13 +1356,13 @@ mod tests { &bpf_loader_upgradeable::ID, ); - let authority = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); // Create packet data + let authority = Pubkey::new_unique(); let (client_id, sender, salt, gmp_account_pda) = create_test_account_data(); // Counter app state PDA @@ -1500,7 +1483,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, false, // not paused ), diff --git a/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs b/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs index 74d60a415..aadb41d60 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/on_timeout_packet.rs @@ -99,13 +99,8 @@ mod tests { #[test] fn test_on_timeout_packet_app_paused() { - use crate::test_utils::ANCHOR_ERROR_OFFSET; - use mollusk_svm::result::Check; - use solana_sdk::program_error::ProgramError; - let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -142,7 +137,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, true, // paused ), @@ -163,7 +157,6 @@ mod tests { fn test_on_timeout_packet_invalid_app_state_pda() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let port_id = "gmpport".to_string(); @@ -207,7 +200,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( wrong_app_state_pda, - authority, wrong_bump, false, // not paused ), @@ -227,7 +219,6 @@ mod tests { fn test_on_timeout_packet_direct_call_rejected() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -236,7 +227,7 @@ mod tests { let instruction = create_timeout_instruction(app_state_pda, router_program, payer); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), create_instructions_sysvar_account_with_caller(crate::ID), // Direct call create_authority_account(payer), @@ -254,7 +245,6 @@ mod tests { fn test_on_timeout_packet_unauthorized_router() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -264,7 +254,7 @@ mod tests { let unauthorized_program = Pubkey::new_unique(); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), create_instructions_sysvar_account_with_caller(unauthorized_program), // Unauthorized create_authority_account(payer), @@ -282,7 +272,6 @@ mod tests { fn test_on_timeout_packet_fake_sysvar_wormhole_attack() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let router_program = ics26_router::ID; let payer = Pubkey::new_unique(); let (app_state_pda, app_state_bump) = @@ -298,7 +287,7 @@ mod tests { instruction.accounts[2] = AccountMeta::new_readonly(fake_sysvar_pubkey, false); let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_router_program_account(router_program), // Wormhole attack: provide a DIFFERENT account instead of the real sysvar (fake_sysvar_pubkey, fake_sysvar_account), diff --git a/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs b/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs index 8ccee9536..16ab30ce3 100644 --- a/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs +++ b/programs/solana/programs/ics27-gmp/src/instructions/send_call.rs @@ -159,7 +159,6 @@ mod tests { fn test_send_call_app_paused() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -206,7 +205,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, true, // paused ), @@ -233,7 +231,6 @@ mod tests { fn test_send_call_invalid_timeout() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -280,7 +277,6 @@ mod tests { let accounts = vec![ create_gmp_app_state_account( app_state_pda, - authority, app_state_bump, false, // not paused ), @@ -307,7 +303,6 @@ mod tests { fn test_send_call_invalid_app_state_pda() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -357,7 +352,7 @@ mod tests { }; let accounts = vec![ - create_gmp_app_state_account(wrong_app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(wrong_app_state_pda, app_state_bump, false), create_authority_account(sender), create_authority_account(payer), create_router_program_account(router_program), @@ -381,7 +376,6 @@ mod tests { fn test_send_call_wrong_router_program() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let wrong_router_program = Pubkey::new_unique(); // Different router! @@ -426,7 +420,7 @@ mod tests { }; let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_authority_account(sender), create_authority_account(payer), create_router_program_account(wrong_router_program), // Wrong one passed @@ -450,7 +444,6 @@ mod tests { fn test_send_call_payload_too_large() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -495,7 +488,7 @@ mod tests { }; let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_authority_account(sender), create_authority_account(payer), create_router_program_account(router_program), @@ -519,7 +512,6 @@ mod tests { fn test_send_call_empty_payload() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -564,7 +556,7 @@ mod tests { }; let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_authority_account(sender), create_authority_account(payer), create_router_program_account(router_program), @@ -588,7 +580,6 @@ mod tests { fn test_send_call_empty_client_id() { let mollusk = Mollusk::new(&crate::ID, crate::get_gmp_program_path()); - let authority = Pubkey::new_unique(); let sender = Pubkey::new_unique(); let payer = Pubkey::new_unique(); let router_program = Pubkey::new_unique(); @@ -633,7 +624,7 @@ mod tests { }; let accounts = vec![ - create_gmp_app_state_account(app_state_pda, authority, app_state_bump, false), + create_gmp_app_state_account(app_state_pda, app_state_bump, false), create_authority_account(sender), create_authority_account(payer), create_router_program_account(router_program), diff --git a/programs/solana/programs/ics27-gmp/src/instructions/set_access_manager.rs b/programs/solana/programs/ics27-gmp/src/instructions/set_access_manager.rs new file mode 100644 index 000000000..0b13755d1 --- /dev/null +++ b/programs/solana/programs/ics27-gmp/src/instructions/set_access_manager.rs @@ -0,0 +1,217 @@ +use crate::constants::GMP_PORT_ID; +use crate::state::GMPAppState; +use anchor_lang::prelude::*; +use solana_ibc_types::events::AccessManagerUpdated; + +#[derive(Accounts)] +pub struct SetAccessManager<'info> { + #[account( + mut, + seeds = [GMPAppState::SEED, GMP_PORT_ID.as_bytes()], + bump = app_state.bump + )] + pub app_state: Account<'info, GMPAppState>, + + /// CHECK: Validated via seeds constraint using the stored `access_manager` program ID + #[account( + seeds = [access_manager::state::AccessManager::SEED], + bump, + seeds::program = app_state.access_manager + )] + pub access_manager: AccountInfo<'info>, + + pub admin: Signer<'info>, + + /// Instructions sysvar for CPI validation + /// CHECK: Address constraint verifies this is the instructions sysvar + #[account(address = anchor_lang::solana_program::sysvar::instructions::ID)] + pub instructions_sysvar: AccountInfo<'info>, +} + +pub fn set_access_manager( + ctx: Context, + new_access_manager: Pubkey, +) -> Result<()> { + let old_access_manager = ctx.accounts.app_state.access_manager; + + // Performs: CPI rejection + signer verification + role check + access_manager::require_role( + &ctx.accounts.access_manager, + solana_ibc_types::roles::ADMIN_ROLE, + &ctx.accounts.admin, + &ctx.accounts.instructions_sysvar, + &crate::ID, + )?; + + ctx.accounts.app_state.access_manager = new_access_manager; + + emit!(AccessManagerUpdated { + old_access_manager, + new_access_manager, + }); + + msg!( + "Access manager updated from {} to {}", + old_access_manager, + new_access_manager + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::*; + use access_manager::AccessManagerError; + use mollusk_svm::result::Check; + use solana_ibc_types::roles; + use solana_sdk::instruction::AccountMeta; + + #[test] + fn test_set_access_manager_success() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (app_state_pda, app_state_account) = create_initialized_app_state(access_manager::ID); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + create_instructions_sysvar_account_with_caller(crate::ID), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::success()]; + let result = mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + + let app_state = get_app_state_from_result(&result, &app_state_pda); + assert_eq!(app_state.access_manager, new_access_manager); + } + + #[test] + fn test_set_access_manager_not_admin() { + let admin = Pubkey::new_unique(); + let non_admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (app_state_pda, app_state_account) = create_initialized_app_state(access_manager::ID); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(non_admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + let accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (non_admin, create_signer_account()), + create_instructions_sysvar_account_with_caller(crate::ID), + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::Unauthorized as u32, + ))]; + + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } + + #[test] + fn test_set_access_manager_fake_sysvar_wormhole_attack() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (app_state_pda, app_state_account) = create_initialized_app_state(access_manager::ID); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Replace real sysvar with fake one (Wormhole-style attack) + let (instruction, fake_sysvar_account) = setup_fake_sysvar_attack(instruction, crate::ID); + + let accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + fake_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + mollusk.process_and_validate_instruction( + &instruction, + &accounts, + &[expect_sysvar_attack_error()], + ); + } + + #[test] + fn test_set_access_manager_cpi_rejection() { + let admin = Pubkey::new_unique(); + let new_access_manager = Pubkey::new_unique(); + + let (app_state_pda, app_state_account) = create_initialized_app_state(access_manager::ID); + + let (access_manager_pda, access_manager_account) = + create_access_manager_with_role(admin, roles::ADMIN_ROLE, admin); + + let instruction = build_instruction( + crate::instruction::SetAccessManager { new_access_manager }, + vec![ + AccountMeta::new(app_state_pda, false), + AccountMeta::new_readonly(access_manager_pda, false), + AccountMeta::new_readonly(admin, true), + AccountMeta::new_readonly(solana_sdk::sysvar::instructions::ID, false), + ], + ); + + // Simulate CPI call from unauthorized program + let malicious_program = Pubkey::new_unique(); + let (instruction, cpi_sysvar_account) = setup_cpi_call_test(instruction, malicious_program); + + let accounts = vec![ + (app_state_pda, app_state_account), + (access_manager_pda, access_manager_account), + (admin, create_signer_account()), + cpi_sysvar_account, + ]; + + let mollusk = setup_mollusk(); + let checks = vec![Check::err(solana_sdk::program_error::ProgramError::Custom( + ANCHOR_ERROR_OFFSET + AccessManagerError::CpiNotAllowed as u32, + ))]; + mollusk.process_and_validate_instruction(&instruction, &accounts, &checks); + } +} diff --git a/programs/solana/programs/ics27-gmp/src/lib.rs b/programs/solana/programs/ics27-gmp/src/lib.rs index b81f292db..7fb974685 100644 --- a/programs/solana/programs/ics27-gmp/src/lib.rs +++ b/programs/solana/programs/ics27-gmp/src/lib.rs @@ -33,8 +33,8 @@ pub mod ics27_gmp { use super::*; /// Initialize the ICS27 GMP application - pub fn initialize(ctx: Context) -> Result<()> { - instructions::initialize(ctx) + pub fn initialize(ctx: Context, access_manager: Pubkey) -> Result<()> { + instructions::initialize(ctx, access_manager) } /// Send a GMP call packet @@ -76,8 +76,11 @@ pub mod ics27_gmp { instructions::unpause_app(ctx) } - /// Update app authority (admin only) - pub fn update_authority(ctx: Context) -> Result<()> { - instructions::update_authority(ctx) + /// Set the access manager program (admin only) + pub fn set_access_manager( + ctx: Context, + new_access_manager: Pubkey, + ) -> Result<()> { + instructions::set_access_manager(ctx, new_access_manager) } } diff --git a/programs/solana/programs/ics27-gmp/src/router_cpi.rs b/programs/solana/programs/ics27-gmp/src/router_cpi.rs index 6af5aa5fd..1e350f623 100644 --- a/programs/solana/programs/ics27-gmp/src/router_cpi.rs +++ b/programs/solana/programs/ics27-gmp/src/router_cpi.rs @@ -2,6 +2,7 @@ use crate::errors::GMPError; use anchor_lang::prelude::*; use anchor_lang::solana_program::{instruction::Instruction, program::invoke}; use solana_ibc_types::MsgSendPacket; +use solana_program::hash::hash; /// Send IBC packet via CPI to the ICS26 router /// This function creates and sends a GMP packet from Solana to another chain @@ -22,7 +23,7 @@ pub fn send_packet_cpi<'a>( let mut instruction_data = Vec::with_capacity(256); // Anchor instruction discriminator: first 8 bytes of hash of "global:send_packet" - let discriminator = anchor_lang::solana_program::hash::hash(b"global:send_packet").to_bytes(); + let discriminator = hash(b"global:send_packet").to_bytes(); instruction_data.extend_from_slice(&discriminator[..8]); // Append serialized MsgSendPacket data diff --git a/programs/solana/programs/ics27-gmp/src/state.rs b/programs/solana/programs/ics27-gmp/src/state.rs index 6337ee4c3..f70648548 100644 --- a/programs/solana/programs/ics27-gmp/src/state.rs +++ b/programs/solana/programs/ics27-gmp/src/state.rs @@ -14,15 +14,15 @@ pub struct GMPAppState { /// Schema version for upgrades pub version: AccountVersion, - /// Administrative authority - pub authority: Pubkey, - /// Emergency pause flag pub paused: bool, /// PDA bump seed pub bump: u8, + /// Access manager program ID for role-based access control + pub access_manager: Pubkey, + /// Reserved space for future fields pub _reserved: [u8; 256], } diff --git a/programs/solana/programs/ics27-gmp/src/test_utils.rs b/programs/solana/programs/ics27-gmp/src/test_utils.rs index 45882a2c9..cdc37fd42 100644 --- a/programs/solana/programs/ics27-gmp/src/test_utils.rs +++ b/programs/solana/programs/ics27-gmp/src/test_utils.rs @@ -1,7 +1,9 @@ use crate::constants::{GMP_PORT_ID, ICS27_ENCODING, ICS27_VERSION}; use crate::state::{AccountVersion, GMPAppState}; +use access_manager::RoleData; use anchor_lang::{AnchorSerialize, Discriminator, InstructionData}; use mollusk_svm::Mollusk; +use solana_ibc_types::roles; use solana_sdk::{ account::Account as SolanaAccount, instruction::{AccountMeta, Instruction}, @@ -20,15 +22,14 @@ pub const DUMMY_TARGET_PROGRAM: Pubkey = Pubkey::new_from_array([ pub fn create_gmp_app_state_account( pubkey: Pubkey, - authority: Pubkey, bump: u8, paused: bool, ) -> (Pubkey, SolanaAccount) { let app_state = GMPAppState { version: AccountVersion::V1, - authority, paused, bump, + access_manager: access_manager::ID, _reserved: [0; 256], }; @@ -48,6 +49,43 @@ pub fn create_gmp_app_state_account( ) } +pub fn setup_access_manager_with_roles(roles: &[(u64, &[Pubkey])]) -> (Pubkey, SolanaAccount) { + let (access_manager_pda, _) = + solana_ibc_types::access_manager::AccessManager::pda(access_manager::ID); + + let mut role_data: Vec = roles + .iter() + .map(|(role_id, members)| RoleData { + role_id: *role_id, + members: members.to_vec(), + }) + .collect(); + + // Ensure ADMIN_ROLE exists with at least one member + if !role_data.iter().any(|r| r.role_id == roles::ADMIN_ROLE) { + role_data.push(RoleData { + role_id: roles::ADMIN_ROLE, + members: vec![Pubkey::new_unique()], + }); + } + + let access_manager = access_manager::state::AccessManager { roles: role_data }; + + let mut data = access_manager::state::AccessManager::DISCRIMINATOR.to_vec(); + access_manager.serialize(&mut data).unwrap(); + + ( + access_manager_pda, + SolanaAccount { + lamports: 1_000_000, + data, + owner: access_manager::ID, + executable: false, + rent_epoch: 0, + }, + ) +} + pub const fn create_authority_account(pubkey: Pubkey) -> (Pubkey, SolanaAccount) { ( pubkey, @@ -320,3 +358,161 @@ pub fn create_recv_packet_instruction( data: instruction_data.data(), } } + +/// Create initialized app state for tests +pub fn create_initialized_app_state(_access_manager_program_id: Pubkey) -> (Pubkey, SolanaAccount) { + let (app_state_pda, bump) = + Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &crate::ID); + + create_gmp_app_state_account(app_state_pda, bump, false) +} + +/// Create access manager with a specific role +pub fn create_access_manager_with_role( + admin: Pubkey, + role_id: u64, + member: Pubkey, +) -> (Pubkey, SolanaAccount) { + let admin_members = [admin]; + let role_members = [member]; + + let roles: &[(u64, &[Pubkey])] = if role_id == roles::ADMIN_ROLE && member == admin { + &[(role_id, &role_members[..])] + } else { + &[ + (roles::ADMIN_ROLE, &admin_members[..]), + (role_id, &role_members[..]), + ] + }; + + setup_access_manager_with_roles(roles) +} + +/// Build instruction for GMP program +pub fn build_instruction( + instruction_data: T, + accounts: Vec, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts, + data: instruction_data.data(), + } +} + +/// Create signer account for tests +pub fn create_signer_account() -> SolanaAccount { + SolanaAccount { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Setup mollusk for tests +pub fn setup_mollusk() -> Mollusk { + Mollusk::new(&crate::ID, crate::get_gmp_program_path()) +} + +/// Get app state from mollusk instruction result +pub fn get_app_state_from_result( + result: &mollusk_svm::result::InstructionResult, + pda: &Pubkey, +) -> GMPAppState { + use anchor_lang::AccountDeserialize; + + let account = result + .resulting_accounts + .iter() + .find(|(pubkey, _)| pubkey == pda) + .map(|(_, account)| account) + .expect("App state account not found"); + + GMPAppState::try_deserialize(&mut &account.data[..]).expect("Failed to deserialize app state") +} + +/// Helper for testing Wormhole-style fake sysvar attacks +/// Automatically finds and replaces the instructions sysvar with a fake one +/// Returns (`modified_instruction`, `fake_sysvar_account_tuple`) +pub fn setup_fake_sysvar_attack( + mut instruction: Instruction, + program_id: Pubkey, +) -> (Instruction, (Pubkey, SolanaAccount)) { + let (fake_sysvar_pubkey, fake_sysvar_account) = + create_fake_instructions_sysvar_account(program_id); + + // Find the instructions sysvar account and replace it with the fake one + let sysvar_account_index = instruction + .accounts + .iter() + .position(|acc| acc.pubkey == solana_sdk::sysvar::instructions::ID) + .expect("Instructions sysvar account not found in instruction"); + + instruction.accounts[sysvar_account_index] = + AccountMeta::new_readonly(fake_sysvar_pubkey, false); + + (instruction, (fake_sysvar_pubkey, fake_sysvar_account)) +} + +/// Expected error for Wormhole-style sysvar attacks (Anchor's address constraint violation) +pub fn expect_sysvar_attack_error() -> mollusk_svm::result::Check<'static> { + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ErrorCode::ConstraintAddress as u32, + )) +} + +/// Create instructions sysvar that simulates a CPI call from another program +/// Uses the REAL sysvar address but with a different `program_id` to simulate CPI context +pub fn create_cpi_instructions_sysvar_account(caller_program_id: Pubkey) -> SolanaAccount { + use solana_sdk::sysvar::instructions::{ + construct_instructions_data, BorrowedAccountMeta, BorrowedInstruction, + }; + + let account_pubkey = Pubkey::new_unique(); + let account = BorrowedAccountMeta { + pubkey: &account_pubkey, + is_signer: false, + is_writable: true, + }; + let mock_instruction = BorrowedInstruction { + program_id: &caller_program_id, // Different program calling via CPI + accounts: vec![account], + data: &[], + }; + + let ixs_data = construct_instructions_data(&[mock_instruction]); + + SolanaAccount { + lamports: 1_000_000, + data: ixs_data, + owner: solana_sdk::sysvar::ID, + executable: false, + rent_epoch: 0, + } +} + +/// Helper for testing CPI rejection +/// Replaces the instructions sysvar with one that simulates a CPI call +/// Returns (`modified_instruction`, `cpi_sysvar_account_tuple`) +pub fn setup_cpi_call_test( + instruction: Instruction, + caller_program_id: Pubkey, +) -> (Instruction, (Pubkey, SolanaAccount)) { + let cpi_sysvar_account = create_cpi_instructions_sysvar_account(caller_program_id); + + // Use the REAL sysvar address (unlike Wormhole attack which uses fake) + ( + instruction, + (solana_sdk::sysvar::instructions::ID, cpi_sysvar_account), + ) +} + +/// Expected error for CPI rejection (`UnauthorizedCaller` from `reject_cpi`) +pub fn expect_cpi_rejection_error() -> mollusk_svm::result::Check<'static> { + use solana_ibc_types::CpiValidationError; + mollusk_svm::result::Check::err(solana_sdk::program_error::ProgramError::Custom( + anchor_lang::error::ERROR_CODE_OFFSET + CpiValidationError::UnauthorizedCaller as u32, + )) +} diff --git a/programs/solana/programs/ics27-gmp/tests/upgrade_migration_example.rs b/programs/solana/programs/ics27-gmp/tests/upgrade_migration_example.rs index 0037d07c3..3f081ab3f 100644 --- a/programs/solana/programs/ics27-gmp/tests/upgrade_migration_example.rs +++ b/programs/solana/programs/ics27-gmp/tests/upgrade_migration_example.rs @@ -17,14 +17,14 @@ fn create_account_data(account: &T) -> Vec (Pubkey, Vec) { +fn setup_gmp_app_state(paused: bool) -> (Pubkey, Vec) { let (app_state_pda, bump) = Pubkey::find_program_address(&[GMPAppState::SEED, GMP_PORT_ID.as_bytes()], &ics27_gmp::ID); let app_state = GMPAppState { version: AccountVersion::V1, - authority, paused, bump, + access_manager: access_manager::ID, _reserved: [0; 256], }; let app_state_data = create_account_data(&app_state); @@ -37,17 +37,20 @@ pub enum AccountVersionExample { V2, // New version added } -/// Example V2 `GMPAppState` with additional fields +/// Example V2 `GMPAppState` with additional fields. +/// +/// NOTE: Authorization for admin operations is handled by `AccessManager` (`PAUSER_ROLE`, `UNPAUSER_ROLE`). +/// This test focuses on data serialization/migration patterns, not authorization. #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct GMPAppStateV2Example { /// Schema version for upgrades pub version: AccountVersionExample, - /// Authority that can perform admin operations - pub authority: Pubkey, - /// Whether the app is paused + /// Whether the app is paused (existing V1 field) pub paused: bool, - /// PDA bump seed + /// PDA bump seed (existing V1 field) pub bump: u8, + /// Access manager program ID (existing V1 field) + pub access_manager: Pubkey, // ========== NEW V2 FIELDS ========== /// Fee collector account for GMP operations (NEW in V2) @@ -56,16 +59,15 @@ pub struct GMPAppStateV2Example { pub global_rate_limit: u64, // 8 bytes /// Total number of packets processed (NEW in V2) pub total_packets_processed: u64, // 8 bytes - // Total new fields: 33 + 8 + 8 = 49 bytes - /// Reserved space for future fields (reduced from 256 to 207) - pub _reserved: [u8; 207], + // Total new fields: 33 + 8 + 8 = 49 bytes (plus access_manager: 32 bytes = 81 bytes total) + /// Reserved space for future fields (reduced from 256 to 175) + pub _reserved: [u8; 175], } #[test] fn test_gmp_app_state_migration_v1_to_v2() { // Create V1 account - let authority = Pubkey::new_unique(); - let (_, v1_data) = setup_gmp_app_state(authority, false); + let (_, v1_data) = setup_gmp_app_state(false); // Deserialize account into the struct with new added fields let mut cursor = &v1_data[8..]; // Skip discriminator @@ -73,6 +75,7 @@ fn test_gmp_app_state_migration_v1_to_v2() { // Verify it's V1 assert_eq!(state.version, AccountVersionExample::V1); + assert!(!state.paused); // V1 field preserved // Perform migration logic state.version = AccountVersionExample::V2; @@ -82,21 +85,19 @@ fn test_gmp_app_state_migration_v1_to_v2() { // Verify migration preserved V1 fields assert_eq!(state.version, AccountVersionExample::V2); - assert_eq!(state.authority, authority); - assert!(!state.paused); + assert!(!state.paused); // V1 field still preserved // Verify new V2 fields assert!(state.fee_collector.is_some()); assert_eq!(state.global_rate_limit, 1000); assert_eq!(state.total_packets_processed, 0); - assert_eq!(state._reserved.len(), 207); + assert_eq!(state._reserved.len(), 175); // 256 - 49 (V2 fields) - 32 (access_manager) = 175 } #[test] fn test_gmp_app_state_migration_with_paused_state() { // Create V1 account that is paused - let authority = Pubkey::new_unique(); - let (_, v1_data) = setup_gmp_app_state(authority, true); + let (_, v1_data) = setup_gmp_app_state(true); // Deserialize and migrate let mut cursor = &v1_data[8..]; // Skip discriminator @@ -113,8 +114,7 @@ fn test_gmp_app_state_migration_with_paused_state() { // Verify paused state is preserved assert_eq!(state.version, AccountVersionExample::V2); - assert!(state.paused); - assert_eq!(state.authority, authority); + assert!(state.paused); // V1 field preserved // Verify new fields assert!(state.fee_collector.is_some()); @@ -125,25 +125,24 @@ fn test_gmp_app_state_migration_with_paused_state() { #[test] fn test_gmp_app_state_reserved_space_sufficient() { // Create V1 account - let authority = Pubkey::new_unique(); - let (_, v1_data) = setup_gmp_app_state(authority, false); + let (_, v1_data) = setup_gmp_app_state(false); // Deserialize to verify reserved space let mut cursor = &v1_data[8..]; let state: GMPAppStateV2Example = AnchorDeserialize::deserialize(&mut cursor).unwrap(); // Verify we can add fields and still have reserved space - // V2 adds 49 bytes of new fields + // V1 added access_manager: 32 bytes + // V2 adds 49 bytes of new fields (fee_collector + global_rate_limit + total_packets_processed) // Original reserved: 256 bytes - // Remaining reserved: 207 bytes (still plenty for future upgrades) - assert_eq!(state._reserved.len(), 207); + // Remaining reserved: 175 bytes (still plenty for future upgrades) + assert_eq!(state._reserved.len(), 175); } #[test] fn test_gmp_app_state_pda_derivation_preserved() { // Create V1 account - let authority = Pubkey::new_unique(); - let (original_pda, v1_data) = setup_gmp_app_state(authority, false); + let (original_pda, v1_data) = setup_gmp_app_state(false); // Deserialize and migrate let mut cursor = &v1_data[8..]; diff --git a/programs/solana/programs/mock-ibc-app/src/lib.rs b/programs/solana/programs/mock-ibc-app/src/lib.rs index bb4315b99..e1809f888 100644 --- a/programs/solana/programs/mock-ibc-app/src/lib.rs +++ b/programs/solana/programs/mock-ibc-app/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use solana_ibc_macros::ibc_app; use solana_ibc_types::{OnAcknowledgementPacketMsg, OnRecvPacketMsg, OnTimeoutPacketMsg}; -declare_id!("9qnEj3T1NsaGkN3Sj7hgJZiKrVbKVBNmVphJ6PW1PDAB"); +declare_id!("4Fo5RuY7bEPZNz1FjkM9cUkUVc2BVhdYBjDA8P6Tmox1"); /// Mock IBC Application Program for Testing /// diff --git a/solana-keypairs/localnet/access_manager-keypair.json b/solana-keypairs/localnet/access_manager-keypair.json new file mode 100644 index 000000000..214b4cdaa --- /dev/null +++ b/solana-keypairs/localnet/access_manager-keypair.json @@ -0,0 +1 @@ +[87,200,70,60,149,127,55,235,144,156,131,80,5,143,162,83,190,18,53,78,173,23,102,206,237,211,109,117,178,198,157,77,54,102,142,239,64,150,35,184,58,55,175,62,246,200,67,254,235,183,190,141,66,189,81,48,98,84,59,36,146,209,8,170] \ No newline at end of file diff --git a/e2e/interchaintestv8/solana/keypairs/deployer_wallet.json b/solana-keypairs/localnet/deployer_wallet.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/deployer_wallet.json rename to solana-keypairs/localnet/deployer_wallet.json diff --git a/e2e/interchaintestv8/solana/keypairs/dummy_ibc_app-keypair.json b/solana-keypairs/localnet/dummy_ibc_app-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/dummy_ibc_app-keypair.json rename to solana-keypairs/localnet/dummy_ibc_app-keypair.json diff --git a/e2e/interchaintestv8/solana/keypairs/gmp_counter_app-keypair.json b/solana-keypairs/localnet/gmp_counter_app-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/gmp_counter_app-keypair.json rename to solana-keypairs/localnet/gmp_counter_app-keypair.json diff --git a/e2e/interchaintestv8/solana/keypairs/ics07_tendermint-keypair.json b/solana-keypairs/localnet/ics07_tendermint-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/ics07_tendermint-keypair.json rename to solana-keypairs/localnet/ics07_tendermint-keypair.json diff --git a/e2e/interchaintestv8/solana/keypairs/ics26_router-keypair.json b/solana-keypairs/localnet/ics26_router-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/ics26_router-keypair.json rename to solana-keypairs/localnet/ics26_router-keypair.json diff --git a/e2e/interchaintestv8/solana/keypairs/ics27_gmp-keypair.json b/solana-keypairs/localnet/ics27_gmp-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/ics27_gmp-keypair.json rename to solana-keypairs/localnet/ics27_gmp-keypair.json diff --git a/e2e/interchaintestv8/solana/keypairs/malicious_caller-keypair.json b/solana-keypairs/localnet/malicious_caller-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/malicious_caller-keypair.json rename to solana-keypairs/localnet/malicious_caller-keypair.json diff --git a/solana-keypairs/localnet/mock_ibc_app-keypair.json b/solana-keypairs/localnet/mock_ibc_app-keypair.json new file mode 100644 index 000000000..9b8127ce2 --- /dev/null +++ b/solana-keypairs/localnet/mock_ibc_app-keypair.json @@ -0,0 +1 @@ +[211,32,52,234,133,191,146,50,210,235,85,129,114,238,126,249,4,158,162,92,29,200,111,136,248,247,88,2,250,90,146,159,48,93,74,115,92,206,20,188,112,113,192,56,80,158,52,53,107,90,12,112,158,72,133,134,71,126,221,36,77,106,67,142] \ No newline at end of file diff --git a/e2e/interchaintestv8/solana/keypairs/mock_light_client-keypair.json b/solana-keypairs/localnet/mock_light_client-keypair.json similarity index 100% rename from e2e/interchaintestv8/solana/keypairs/mock_light_client-keypair.json rename to solana-keypairs/localnet/mock_light_client-keypair.json diff --git a/solana-keypairs/localnet/upgrader.json b/solana-keypairs/localnet/upgrader.json new file mode 100644 index 000000000..01ae7e9e9 --- /dev/null +++ b/solana-keypairs/localnet/upgrader.json @@ -0,0 +1 @@ +[13,141,227,224,165,167,103,132,61,199,107,24,166,33,79,43,74,42,250,167,253,157,203,179,154,221,9,53,149,239,178,36,91,254,205,247,161,16,165,75,155,150,207,196,64,135,141,106,248,231,67,3,45,194,201,160,184,188,77,57,37,217,199,89] \ No newline at end of file diff --git a/tools/solana-ibc/access_manager.go b/tools/solana-ibc/access_manager.go new file mode 100644 index 000000000..66c32479e --- /dev/null +++ b/tools/solana-ibc/access_manager.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + solanago "github.com/gagliardetto/solana-go" + + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" +) + +var accessManagerCmd = &cobra.Command{ + Use: "access-manager", + Short: "Manage AccessManager roles and initialization", +} + +var initializeCmd = &cobra.Command{ + Use: "initialize ", + Short: "Initialize AccessManager with an admin", + Args: cobra.ExactArgs(4), + Run: func(cmd *cobra.Command, args []string) { + clusterURL := args[0] + payerKeypairPath := args[1] + adminPubkey := solanago.MustPublicKeyFromBase58(args[2]) + accessManagerProgramID := solanago.MustPublicKeyFromBase58(args[3]) + + payerWallet := loadWallet(payerKeypairPath) + + accessManagerPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("access_manager")}, + accessManagerProgramID, + ) + + initIx, err := access_manager.NewInitializeInstruction( + adminPubkey, + accessManagerPda, + payerWallet.PublicKey(), + solanago.SystemProgramID, + solanago.SysVarInstructionsPubkey, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error building initialize instruction: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Initializing AccessManager with admin %s...\n", adminPubkey) + + sig := sendTransaction(clusterURL, payerWallet, []solanago.Instruction{initIx}) + + fmt.Printf("✅ Transaction sent: %s\n", sig) + fmt.Println("Waiting for confirmation...") + + if waitForConfirmation(clusterURL, sig) { + fmt.Printf("✅ AccessManager initialized with admin: %s\n", adminPubkey) + fmt.Printf(" AccessManager PDA: %s\n", accessManagerPda) + } + }, +} + +var grantCmd = &cobra.Command{ + Use: "grant ", + Short: "Grant a role to an account", + Args: cobra.ExactArgs(5), + Run: func(cmd *cobra.Command, args []string) { + clusterURL := args[0] + adminKeypairPath := args[1] + roleID := parseRoleID(args[2]) + accountPubkey := solanago.MustPublicKeyFromBase58(args[3]) + accessManagerProgramID := solanago.MustPublicKeyFromBase58(args[4]) + + adminWallet := loadWallet(adminKeypairPath) + + accessManagerPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("access_manager")}, + accessManagerProgramID, + ) + + grantRoleIx, err := access_manager.NewGrantRoleInstruction( + roleID, + accountPubkey, + accessManagerPda, + adminWallet.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error building grant role instruction: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Granting role %d to %s...\n", roleID, accountPubkey) + + sig := sendTransaction(clusterURL, adminWallet, []solanago.Instruction{grantRoleIx}) + + fmt.Printf("✅ Transaction sent: %s\n", sig) + fmt.Println("Waiting for confirmation...") + + if waitForConfirmation(clusterURL, sig) { + fmt.Printf("✅ Role %d granted to %s\n", roleID, accountPubkey) + } + }, +} + +var revokeCmd = &cobra.Command{ + Use: "revoke ", + Short: "Revoke a role from an account", + Args: cobra.ExactArgs(5), + Run: func(cmd *cobra.Command, args []string) { + clusterURL := args[0] + adminKeypairPath := args[1] + roleID := parseRoleID(args[2]) + accountPubkey := solanago.MustPublicKeyFromBase58(args[3]) + accessManagerProgramID := solanago.MustPublicKeyFromBase58(args[4]) + + adminWallet := loadWallet(adminKeypairPath) + + accessManagerPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("access_manager")}, + accessManagerProgramID, + ) + + revokeRoleIx, err := access_manager.NewRevokeRoleInstruction( + roleID, + accountPubkey, + accessManagerPda, + adminWallet.PublicKey(), + solanago.SysVarInstructionsPubkey, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error building revoke role instruction: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Revoking role %d from %s...\n", roleID, accountPubkey) + + sig := sendTransaction(clusterURL, adminWallet, []solanago.Instruction{revokeRoleIx}) + + fmt.Printf("✅ Transaction sent: %s\n", sig) + fmt.Println("Waiting for confirmation...") + + if waitForConfirmation(clusterURL, sig) { + fmt.Printf("✅ Role %d revoked from %s\n", roleID, accountPubkey) + } + }, +} + +func init() { + accessManagerCmd.AddCommand(initializeCmd) + accessManagerCmd.AddCommand(grantCmd) + accessManagerCmd.AddCommand(revokeCmd) +} diff --git a/tools/solana-ibc/go.mod b/tools/solana-ibc/go.mod new file mode 100644 index 000000000..2776185ce --- /dev/null +++ b/tools/solana-ibc/go.mod @@ -0,0 +1,45 @@ +module github.com/cosmos/solidity-ibc-eureka/tools/solana-ibc + +go 1.24.3 + +replace github.com/cosmos/solidity-ibc-eureka/packages/go-anchor => ../../packages/go-anchor + +require ( + github.com/cosmos/solidity-ibc-eureka/packages/go-anchor v0.0.0 + github.com/gagliardetto/solana-go v1.13.0 + github.com/spf13/cobra v1.1.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/gagliardetto/anchor-go v0.3.2 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect +) diff --git a/tools/solana-ibc/go.sum b/tools/solana-ibc/go.sum new file mode 100644 index 000000000..ef6d5e1de --- /dev/null +++ b/tools/solana-ibc/go.sum @@ -0,0 +1,625 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/wDiV7Gx4D+VUPODf0mWRGRc5kSk= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= +github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dfuse-io/logging v0.0.0-20201110202154-26697de88c79/go.mod h1:V+ED4kT/t/lKtH99JQmKIb0v9WL3VaYkJ36CfHlVECI= +github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70/go.mod h1:EoK/8RFbMEteaCaz89uessDTnCWjbbcr+DXcBh4el5o= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gagliardetto/anchor-go v0.3.2 h1:/nlAp3B6s4DBlNABWrQ5bA2zpsUGVDqBfwmbUUg4ChQ= +github.com/gagliardetto/anchor-go v0.3.2/go.mod h1:Jf9/DBNo6GsG6RguE4ZuJz+PZtypKg3iUpB++Oc6ynQ= +github.com/gagliardetto/binary v0.6.1/go.mod h1:aOfYkc20U0deHaHn/LVZXiqlkDbFAX0FpTlDhsXa0S0= +github.com/gagliardetto/binary v0.7.6/go.mod h1:mUuay5LL8wFVnIlecHakSZMvcdqfs+CsotR5n77kyjM= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/hashsearch v0.0.0-20191005111333-09dd671e19f9/go.mod h1:513DXpQPzeRo7d4dsCP3xO3XI8hgvruMl9njxyQeraQ= +github.com/gagliardetto/solana-go v1.5.0/go.mod h1:1KFOW7mlR/TSjYFeLCYmfpSptRdNJMtpgChelKy2oU0= +github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= +github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gagliardetto/utilz v0.1.1/go.mod h1:b+rGFkRHz3HWJD0RYMzat47JyvbTtpE0iEcYTRJTLLA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streamingfast/logging v0.0.0-20220405224725-2755dab2ce75/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/tools/solana-ibc/helpers.go b/tools/solana-ibc/helpers.go new file mode 100644 index 000000000..73eec2fd3 --- /dev/null +++ b/tools/solana-ibc/helpers.go @@ -0,0 +1,128 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "time" + + solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" +) + +func parseRoleID(roleIDStr string) uint64 { + var roleID uint64 + if _, err := fmt.Sscanf(roleIDStr, "%d", &roleID); err != nil { + fmt.Fprintf(os.Stderr, "Invalid role ID: %v\n", err) + os.Exit(1) + } + return roleID +} + +func loadWallet(keypairPath string) *solanago.Wallet { + keypairData, err := os.ReadFile(keypairPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading keypair: %v\n", err) + os.Exit(1) + } + + var secretKey []byte + if err := json.Unmarshal(keypairData, &secretKey); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing keypair: %v\n", err) + os.Exit(1) + } + + wallet, err := solanago.WalletFromPrivateKeyBase58(solanago.PrivateKey(secretKey).String()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating wallet: %v\n", err) + os.Exit(1) + } + + return wallet +} + +func createComputeBudgetInstruction(computeUnits uint32) solanago.Instruction { + computeBudgetData := make([]byte, 9) + computeBudgetData[0] = 0x02 + computeBudgetData[1] = byte(computeUnits) + computeBudgetData[2] = byte(computeUnits >> 8) + computeBudgetData[3] = byte(computeUnits >> 16) + computeBudgetData[4] = byte(computeUnits >> 24) + + return solanago.NewInstruction( + solanago.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111"), + solanago.AccountMetaSlice{}, + computeBudgetData, + ) +} + +func sendTransaction(clusterURL string, wallet *solanago.Wallet, instructions []solanago.Instruction) solanago.Signature { + client := rpc.New(clusterURL) + ctx := context.Background() + + recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentConfirmed) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting blockhash: %v\n", err) + os.Exit(1) + } + + tx, err := solanago.NewTransaction( + instructions, + recent.Value.Blockhash, + solanago.TransactionPayer(wallet.PublicKey()), + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating transaction: %v\n", err) + os.Exit(1) + } + + _, err = tx.Sign(func(key solanago.PublicKey) *solanago.PrivateKey { + if key.Equals(wallet.PublicKey()) { + return &wallet.PrivateKey + } + return nil + }) + if err != nil { + fmt.Fprintf(os.Stderr, "Error signing transaction: %v\n", err) + os.Exit(1) + } + + sig, err := client.SendTransactionWithOpts( + ctx, + tx, + rpc.TransactionOpts{ + SkipPreflight: true, + }, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error sending transaction: %v\n", err) + os.Exit(1) + } + + return sig +} + +func waitForConfirmation(clusterURL string, sig solanago.Signature) bool { + client := rpc.New(clusterURL) + ctx := context.Background() + + for i := 0; i < 30; i++ { + statuses, err := client.GetSignatureStatuses(ctx, false, sig) + if err == nil && len(statuses.Value) > 0 && statuses.Value[0] != nil { + if statuses.Value[0].Err != nil { + fmt.Fprintf(os.Stderr, "❌ Transaction failed: %v\n", statuses.Value[0].Err) + os.Exit(1) + } + if statuses.Value[0].ConfirmationStatus == rpc.ConfirmationStatusConfirmed || + statuses.Value[0].ConfirmationStatus == rpc.ConfirmationStatusFinalized { + return true + } + } + fmt.Print(".") + time.Sleep(1 * time.Second) + } + + fmt.Println("\n⚠️ Confirmation timeout - check transaction status manually") + return false +} diff --git a/tools/solana-ibc/main.go b/tools/solana-ibc/main.go new file mode 100644 index 000000000..b5e29e56f --- /dev/null +++ b/tools/solana-ibc/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "solana-ibc", + Short: "CLI tool for Solana IBC operations", + Long: `solana-ibc provides commands for managing AccessManager roles and program upgrades.`, +} + +func init() { + rootCmd.AddCommand(accessManagerCmd) + rootCmd.AddCommand(upgradeCmd) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/tools/solana-ibc/upgrade.go b/tools/solana-ibc/upgrade.go new file mode 100644 index 000000000..ad3572e2b --- /dev/null +++ b/tools/solana-ibc/upgrade.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + solanago "github.com/gagliardetto/solana-go" + + access_manager "github.com/cosmos/solidity-ibc-eureka/packages/go-anchor/accessmanager" +) + +var upgradeCmd = &cobra.Command{ + Use: "upgrade", + Short: "Program upgrade operations", +} + +var programCmd = &cobra.Command{ + Use: "program ", + Short: "Execute program upgrade via AccessManager", + Args: cobra.ExactArgs(6), + Run: func(cmd *cobra.Command, args []string) { + clusterURL := args[0] + upgraderKeypairPath := args[1] + targetProgramID := solanago.MustPublicKeyFromBase58(args[2]) + bufferAddress := solanago.MustPublicKeyFromBase58(args[3]) + accessManagerProgramID := solanago.MustPublicKeyFromBase58(args[4]) + programDataAddress := solanago.MustPublicKeyFromBase58(args[5]) + + upgraderWallet := loadWallet(upgraderKeypairPath) + + accessManagerPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("access_manager")}, + accessManagerProgramID, + ) + + upgradeAuthorityPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("upgrade_authority"), targetProgramID.Bytes()}, + accessManagerProgramID, + ) + + upgradeIx, err := access_manager.NewUpgradeProgramInstruction( + targetProgramID, + accessManagerPda, + targetProgramID, + programDataAddress, + bufferAddress, + upgradeAuthorityPda, + upgraderWallet.PublicKey(), + upgraderWallet.PublicKey(), + solanago.SysVarInstructionsPubkey, + solanago.BPFLoaderUpgradeableProgramID, + solanago.SysVarRentPubkey, + solanago.SysVarClockPubkey, + ) + if err != nil { + fmt.Fprintf(os.Stderr, "Error building upgrade instruction: %v\n", err) + os.Exit(1) + } + + computeBudgetIx := createComputeBudgetInstruction(400_000) + + fmt.Println("Sending upgrade transaction...") + + sig := sendTransaction(clusterURL, upgraderWallet, []solanago.Instruction{computeBudgetIx, upgradeIx}) + + fmt.Printf("✅ Upgrade transaction sent!\n") + fmt.Printf(" Signature: %s\n", sig) + fmt.Printf(" Explorer: https://explorer.solana.com/tx/%s?cluster=custom&customUrl=%s\n", sig, clusterURL) + + fmt.Println("\nWaiting for confirmation...") + + if waitForConfirmation(clusterURL, sig) { + fmt.Printf("✅ Upgrade confirmed! Program %s has been upgraded.\n", targetProgramID) + } + }, +} + +var derivePdaCmd = &cobra.Command{ + Use: "derive-pda ", + Short: "Derive upgrade authority PDA", + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + accessManagerProgramID := solanago.MustPublicKeyFromBase58(args[0]) + targetProgramID := solanago.MustPublicKeyFromBase58(args[1]) + + upgradeAuthorityPda, _, _ := solanago.FindProgramAddress( + [][]byte{[]byte("upgrade_authority"), targetProgramID.Bytes()}, + accessManagerProgramID, + ) + + fmt.Println(upgradeAuthorityPda.String()) + }, +} + +func init() { + upgradeCmd.AddCommand(programCmd) + upgradeCmd.AddCommand(derivePdaCmd) +}