diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3c09ee7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @brewmaster012 @lumtis @fbac @ws4charlie \ No newline at end of file diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..c28a17f --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,115 @@ +name: check +# This workflow runs whenever a PR is opened or updated, or a commit is pushed +# to main. It runs several checks: +# - fmt: checks that the code is formatted according to `rustfmt`. +# - clippy: checks that the code does not contain any `clippy` warnings. +# - doc: checks that the code can be documented without errors. +# - hack: check combinations of feature flags. +# - typos: checks for typos across the repo. +permissions: + contents: read +# This configuration allows maintainers of this repo to create a branch and +# pull request based on the new branch. Restricting the push trigger to the +# main branch ensures that the PR only gets built once. +on: + push: + branches: [ main ] + pull_request: +# If new code is pushed to a PR branch, then cancel in progress workflows for +# that PR. Ensures that we don't waste CI time, and returns results quicker. +# https://github.com/jonhoo/rust-ci-conf/pull/5 +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + CARGO_TERM_COLOR: always +jobs: + fmt: + runs-on: ubuntu-latest + name: nightly / fmt + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + # We run in nightly to make use of some features only available there. + # Check out `rustfmt.toml` to see which ones. + uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt + - name: cargo fmt --all --check + run: cargo fmt --all --check + clippy: + runs-on: ubuntu-latest + name: ${{ matrix.toolchain }} / clippy + permissions: + contents: read + checks: write + strategy: + fail-fast: false + matrix: + # Get early warning of new lints which are regularly introduced in beta + # channels. + toolchain: [ stable, beta ] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install ${{ matrix.toolchain }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + components: clippy + - name: cargo clippy + uses: giraffate/clippy-action@v1 + with: + reporter: 'github-pr-check' + github_token: ${{ secrets.GITHUB_TOKEN }} + doc: + # Run docs generation on nightly rather than stable. This enables features + # like https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html + # which allows an API be documented as only available in some specific + # platforms. + runs-on: ubuntu-latest + name: nightly / doc + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install nightly + uses: dtolnay/rust-toolchain@nightly + - name: cargo doc + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: --cfg docsrs + hack: + # `cargo-hack` checks combinations of feature flags to ensure that features + # are all additive which is required for feature unification. + runs-on: ubuntu-latest + name: stable / features + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + with: + target: wasm32-unknown-unknown + - name: cargo install cargo-hack + uses: taiki-e/install-action@cargo-hack + # Intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 + # `--feature-powerset` runs for every combination of features. Note that + # target in this context means one of `--lib`, `--bin`, etc, and not the + # target triple. + - name: cargo hack + run: cargo hack check --feature-powerset --depth 2 --release --target wasm32-unknown-unknown --skip std --workspace --exclude e2e --exclude basic-example-script --exclude benches + typos: + runs-on: ubuntu-latest + name: stable / typos + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Check spelling of files in the workspace + uses: crate-ci/typos@v1.23.6 + with: + config: .typos.toml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..6e5afec --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,44 @@ +name: coverage +permissions: + contents: read +on: + push: + branches: [ main ] + paths-ignore: + - "**.ts" + pull_request: + paths-ignore: + - "**.ts" +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + CARGO_TERM_COLOR: always +jobs: + coverage: + runs-on: ubuntu-latest + name: stable / coverage + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + with: + components: llvm-tools-preview + - name: cargo install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + - name: cargo llvm-cov + # FIXME: Include e2e tests in coverage. + run: cargo llvm-cov --locked --lcov --output-path lcov.info + - name: Record Rust version + run: echo "RUST=$(rustc --version)" >> "$GITHUB_ENV" + - name: Upload to codecov.io + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + env_vars: OS,RUST diff --git a/.github/workflows/nostd.yml b/.github/workflows/nostd.yml new file mode 100644 index 0000000..a9e1c6e --- /dev/null +++ b/.github/workflows/nostd.yml @@ -0,0 +1,32 @@ +# This workflow checks whether the library is able to run without the std +# library. See `check.yml` for information about how the concurrency +# cancellation and workflow triggering works. +name: no-std +permissions: + contents: read +on: + push: + branches: [ main ] + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + CARGO_TERM_COLOR: always +jobs: + nostd: + runs-on: ubuntu-latest + name: ${{ matrix.target }} + strategy: + matrix: + target: [ wasm32-unknown-unknown ] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: rustup target add ${{ matrix.target }} + run: rustup target add ${{ matrix.target }} + - name: cargo check + run: cargo check --release --target ${{ matrix.target }} --no-default-features diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ab03027 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,74 @@ +name: test +# This is the main CI workflow that runs the test suite on all pushes to main +# and all pull requests. It runs the following jobs: +# - required: runs the test suite on ubuntu with stable and beta rust +# toolchains. +# - os-check: runs the test suite on mac and windows. +# - coverage: runs the test suite and collects coverage information. +# See `check.yml` for information about how the concurrency cancellation and +# workflow triggering works. +permissions: + contents: read +on: + push: + branches: [ main ] + paths-ignore: + - "**.ts" + pull_request: + paths-ignore: + - "**.ts" +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true +env: + CARGO_TERM_COLOR: always +jobs: + required: + runs-on: ubuntu-latest + name: ${{ matrix.toolchain }} + strategy: + matrix: + # Run on stable and beta to ensure that tests won't break on the next + # version of the rust toolchain. + toolchain: [ stable, beta ] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install ${{ matrix.toolchain }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + - name: cargo generate-lockfile + # Enable this ci template to run regardless of whether the lockfile is + # checked in or not. + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + # https://twitter.com/jonhoo/status/1571290371124260865 + - name: cargo test --locked + run: cargo test --locked --all-targets + # https://github.com/rust-lang/cargo/issues/6669 + - name: cargo test --doc + run: cargo test --locked --doc + os-check: + # Run cargo test on MacOS and Windows. + runs-on: ${{ matrix.os }} + name: ${{ matrix.os }} / stable + strategy: + fail-fast: false + matrix: + os: [ macos-latest ] + # Windows fails because of `stylus-proc`. + # os: [macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: cargo generate-lockfile + if: hashFiles('Cargo.lock') == '' + run: cargo generate-lockfile + - name: cargo test + run: cargo test --locked --all-targets + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..427570f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.anchor/ +/target +**/.DS_Store diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..5e25bce --- /dev/null +++ b/.typos.toml @@ -0,0 +1,13 @@ +[files] +ignore-files = true +ignore-hidden = false +extend-exclude = [ + ".git/", +] + +[default] +extend-ignore-re = [ + # hardcoded ID in src/lib.rs:29 + '94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d', +] +check-filename = true \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1b5fa24 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +# assets +C_GREEN=\033[0;32m +C_RED=\033[0;31m +C_BLUE=\033[0;34m +C_END=\033[0m + +.PHONY: fmt +fmt: + @echo "$(C_GREEN)# Formatting rust code$(C_END)" + @./scripts/fmt.sh \ No newline at end of file diff --git a/README.md b/README.md index 32161d4..cc46223 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ https://github.com/zeta-chain/protocol-contracts-solana/blob/01eeb9733a00b6e972d The TSS signature is a ECDSA secp256k1 signature; its public key therefore address (Ethereum compatible hashing from pubkey) is therefore verifiable using the `secp256k1_recover` -function. Alternatively, Solana runtime also privides a program to provide this verification service +function. Alternatively, Solana runtime also provides a program to provide this verification service via CPI; see [proposal 48](https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0048-native-program-for-secp256r1-sigverify.md) which might be more cost efficient. diff --git a/programs/protocol-contracts-solana/src/lib.rs b/programs/protocol-contracts-solana/src/lib.rs index 0c897b5..58f05a9 100644 --- a/programs/protocol-contracts-solana/src/lib.rs +++ b/programs/protocol-contracts-solana/src/lib.rs @@ -3,8 +3,8 @@ use anchor_lang::system_program; use anchor_spl::token::{transfer, Token, TokenAccount}; use solana_program::keccak::hash; use solana_program::secp256k1_recover::secp256k1_recover; -use std::mem::size_of; use spl_associated_token_account; +use std::mem::size_of; #[error_code] pub enum Errors { @@ -28,12 +28,15 @@ pub enum Errors { declare_id!("94U5AHQMKkV5txNJ17QPXWoh474PheGou6cNP2FEuL1d"); - #[program] pub mod gateway { use super::*; - pub fn initialize(ctx: Context, tss_address:[u8; 20], chain_id: u64) -> Result<()> { + pub fn initialize( + ctx: Context, + tss_address: [u8; 20], + chain_id: u64, + ) -> Result<()> { let initialized_pda = &mut ctx.accounts.pda; initialized_pda.nonce = 0; initialized_pda.tss_address = tss_address; @@ -45,7 +48,10 @@ pub mod gateway { pub fn update_tss(ctx: Context, tss_address: [u8; 20]) -> Result<()> { let pda = &mut ctx.accounts.pda; - require!(ctx.accounts.signer.key() == pda.authority, Errors::SignerIsNotAuthority); + require!( + ctx.accounts.signer.key() == pda.authority, + Errors::SignerIsNotAuthority + ); pda.tss_address = tss_address; Ok(()) } @@ -61,12 +67,20 @@ pub mod gateway { }, ); system_program::transfer(cpi_context, amount)?; - msg!("{:?} deposits {:?} lamports to PDA", ctx.accounts.signer.key(), amount); + msg!( + "{:?} deposits {:?} lamports to PDA", + ctx.accounts.signer.key(), + amount + ); Ok(()) } - pub fn deposit_spl_token(ctx: Context, amount: u64, memo: Vec) -> Result<()> { + pub fn deposit_spl_token( + ctx: Context, + amount: u64, + memo: Vec, + ) -> Result<()> { require!(memo.len() >= 20, Errors::MemoLengthTooShort); require!(memo.len() <= 512, Errors::MemoLengthExceeded); let token = &ctx.accounts.token_program; @@ -77,7 +91,10 @@ pub mod gateway { &from.mint, ); // must deposit to the ATA from PDA in order to receive credit - require!(pda_ata == ctx.accounts.to.to_account_info().key(), Errors::DepositToAddressMismatch); + require!( + pda_ata == ctx.accounts.to.to_account_info().key(), + Errors::DepositToAddressMismatch + ); let xfer_ctx = CpiContext::new( token.to_account_info(), @@ -113,7 +130,10 @@ pub mod gateway { concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); concatenated_buffer.extend_from_slice(&amount.to_be_bytes()); concatenated_buffer.extend_from_slice(&ctx.accounts.to.key().to_bytes()); - require!(message_hash == hash(&concatenated_buffer[..]).to_bytes(), Errors::MessageHashMismatch); + require!( + message_hash == hash(&concatenated_buffer[..]).to_bytes(), + Errors::MessageHashMismatch + ); let address = recover_eth_address(&message_hash, recovery_id, &signature)?; // ethereum address is the last 20 Bytes of the hashed pubkey msg!("recovered address {:?}", address); @@ -150,7 +170,10 @@ pub mod gateway { concatenated_buffer.extend_from_slice(&nonce.to_be_bytes()); concatenated_buffer.extend_from_slice(&amount.to_be_bytes()); concatenated_buffer.extend_from_slice(&ctx.accounts.to.key().to_bytes()); - require!(message_hash == hash(&concatenated_buffer[..]).to_bytes(), Errors::MessageHashMismatch); + require!( + message_hash == hash(&concatenated_buffer[..]).to_bytes(), + Errors::MessageHashMismatch + ); let address = recover_eth_address(&message_hash, recovery_id, &signature)?; // ethereum address is the last 20 Bytes of the hashed pubkey msg!("recovered address {:?}", address); @@ -231,7 +254,7 @@ pub struct DepositSplToken<'info> { #[account(mut)] pub from: Account<'info, TokenAccount>, // this must be owned by signer; normally the ATA of signer #[account(mut)] - pub to: Account<'info, TokenAccount>, // this must be ATA of PDA + pub to: Account<'info, TokenAccount>, // this must be ATA of PDA } #[derive(Accounts)] @@ -278,7 +301,6 @@ pub struct Pda { chain_id: u64, } - #[cfg(test)] mod tests { use super::*; @@ -295,4 +317,4 @@ mod tests { let message_hash = hash(&concatenated_buffer[..]).to_bytes(); println!("message_hash: {:?}", message_hash); } -} \ No newline at end of file +} diff --git a/scripts/fmt.sh b/scripts/fmt.sh new file mode 100755 index 0000000..21baf59 --- /dev/null +++ b/scripts/fmt.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Exit on any error +set -e + +if ! command -v brew &> /dev/null +then + echo "brew is required to run the script." + exit 1 +fi + +if ! command -v rustfmt &> /dev/null +then + echo "rustfmt could not be found, installing..." + brew install rustfmt +fi + +cargo fmt +if [[ $? == 0 ]] ; then + echo "Code is formatted!" +else + echo "An error occurred during formatting." +fi