diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c81b7c70..c89cd20b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: build on: push: - branches: [ main ] + branches: [ main, migrate_v2] pull_request: branches: '*' @@ -17,6 +17,11 @@ jobs: - uses: actions/checkout@v2 with: submodules: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.60.0 + override: true + components: rustfmt - name: Check Rust formatting uses: actions-rs/cargo@v1 @@ -43,6 +48,10 @@ jobs: - uses: actions/checkout@v2 with: submodules: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.60.0 + override: true - name: cache-build-artifacts uses: actions/cache@v2 @@ -71,7 +80,6 @@ jobs: - name: Run unit tests run: | cargo test --manifest-path program/Cargo.toml - cargo test --manifest-path anker/Cargo.toml cargo test --manifest-path cli/maintainer/Cargo.toml cargo test --manifest-path cli/listener/Cargo.toml cargo test --manifest-path cli/common/Cargo.toml @@ -91,7 +99,6 @@ jobs: # But only run the tests for Solido itself, the SPL tests are already # executed upstream. RUST_BACKTRACE=1 cargo test-bpf --manifest-path program/Cargo.toml - RUST_BACKTRACE=1 cargo test-bpf --manifest-path anker/Cargo.toml - name: Build CLI client run: cargo build --bin solido @@ -122,17 +129,6 @@ jobs: killall -9 solana-test-validator rm -r test-ledger - - name: Run Anker integration test - run: | - export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH" - validator=$(tests/start_test_validator.py) - - tests/airdrop_lamports.sh - - tests/test_anker.py - killall -9 solana-test-validator - rm -r test-ledger - lint: runs-on: ubuntu-latest @@ -140,6 +136,11 @@ jobs: - uses: actions/checkout@v2 with: submodules: true + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.60.0 + override: true + components: rustfmt, clippy - name: cache-build-artifacts uses: actions/cache@v2 @@ -169,12 +170,10 @@ jobs: # TODO: Pin the exact version with Nix instead, to make it easier to use # the same version locally. sudo pip3 install mypy==0.902 - rustup component add clippy cargo install cargo-license --version 0.4.1 - name: Run Clippy run: | - cargo clippy --manifest-path anker/Cargo.toml -- --deny warnings cargo clippy --manifest-path cli/common/Cargo.toml -- --deny warnings cargo clippy --manifest-path cli/listener/Cargo.toml -- --deny warnings cargo clippy --manifest-path cli/listener/fuzz/Cargo.toml -- --deny warnings @@ -182,10 +181,10 @@ jobs: cargo clippy --manifest-path program/Cargo.toml -- --deny warnings cargo clippy --manifest-path testlib/Cargo.toml -- --deny warnings - - name: Typecheck Python - run: | - git ls-files | grep '\.py$' | xargs mypy --strict - - name: Check license compatibility run: | tests/check_licenses.py + + - name: Typecheck Python + run: | + git ls-files | grep '\.py$' | xargs mypy \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 866fdd8d4..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "multisig"] - path = multisig - url = https://github.com/lidofinance/multisig diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8964833..e11f488ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ New features: * Solido no longer requires that validators use a 100%-commission account of which Solido is the withdraw authority. Any vote account can now be used, as long as its commission does not exceed Solido’s configured maximum commission percentage. + Anchor protocol integration is removed. **Compatibility** diff --git a/Cargo.lock b/Cargo.lock index 4255a9865..6896b5325 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,4248 +3,1447 @@ version = 3 [[package]] -name = "Inflector" -version = "0.11.4" +name = "ahash" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "lazy_static", - "regex", + "cfg-if", + "once_cell", + "version_check", ] [[package]] -name = "addr2line" -version = "0.17.0" +name = "arrayref" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli", -] +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] -name = "adler" -version = "1.0.2" +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "ahash" -version = "0.7.6" +name = "autocfg" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.6", - "once_cell", - "version_check", -] +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] -name = "aho-corasick" -version = "0.7.18" +name = "base64" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] -name = "aliasable" -version = "0.1.3" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "anchor-attribute-access-control" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", - "regex", - "syn 1.0.96", + "serde", ] [[package]] -name = "anchor-attribute-account" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" -dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] -name = "anchor-attribute-error" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" dependencies = [ - "anchor-syn", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", ] [[package]] -name = "anchor-attribute-event" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "generic-array", ] [[package]] -name = "anchor-attribute-interface" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "anchor-syn", - "anyhow", - "heck 0.3.3", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "generic-array", ] [[package]] -name = "anchor-attribute-program" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "borsh-derive 0.10.3", + "hashbrown 0.13.2", ] [[package]] -name = "anchor-attribute-state" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "borsh-derive 1.5.3", + "cfg_aliases", ] [[package]] -name = "anchor-derive-accounts" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ - "anchor-syn", - "anyhow", - "proc-macro2 1.0.39", - "quote 1.0.18", + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", "syn 1.0.96", ] [[package]] -name = "anchor-lang" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "borsh-derive" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-error", - "anchor-attribute-event", - "anchor-attribute-interface", - "anchor-attribute-program", - "anchor-attribute-state", - "anchor-derive-accounts", - "base64 0.13.0", - "borsh 0.9.3", - "bytemuck", - "solana-program", - "thiserror", + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "anchor-syn" -version = "0.13.0" -source = "git+https://github.com/lidofinance/anchor?branch=solana-v1.9.28#6722b88a13f33ce072fcdab00bdad4a42b2c3ac5" +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ - "anyhow", - "bs58 0.3.1", - "heck 0.3.3", - "proc-macro2 1.0.39", - "proc-macro2-diagnostics", - "quote 1.0.18", - "serde", - "serde_json", - "sha2", + "proc-macro2", + "quote", "syn 1.0.96", - "thiserror", ] [[package]] -name = "anker" -version = "1.3.3" +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ - "bech32", - "borsh 0.9.3", - "hex", - "lido", - "num-derive", - "num-traits", - "serde", - "solana-program", - "solana-program-test", - "solana-sdk", - "spl-token", - "spl-token-swap", - "testlib", + "proc-macro2", + "quote", + "syn 1.0.96", ] [[package]] -name = "ansi_term" -version = "0.12.1" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "winapi", + "tinyvec", ] [[package]] -name = "anyhow" -version = "1.0.57" +name = "bumpalo" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] -name = "arbitrary" -version = "1.1.0" +name = "bv" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" dependencies = [ - "derive_arbitrary", + "feature-probe", + "serde", ] [[package]] -name = "arrayref" -version = "0.3.6" +name = "bytemuck" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" [[package]] -name = "arrayvec" -version = "0.7.2" +name = "bytemuck_derive" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] -name = "ascii" -version = "0.9.3" +name = "cc" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +dependencies = [ + "shlex", +] [[package]] -name = "ascii" +name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "assert_matches" -version = "1.5.0" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "async-trait" -version = "0.1.56" +name = "console_error_panic_hook" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "cfg-if", + "wasm-bindgen", ] [[package]] -name = "atty" -version = "0.2.14" +name = "console_log" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "log", + "web-sys", ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "constant_time_eq" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] -name = "backtrace" -version = "0.3.65" +name = "cpufeatures" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ - "addr2line", - "cc", - "cfg-if", "libc", - "miniz_oxide", - "object", - "rustc-demangle", ] [[package]] -name = "base32" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" - -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bech32" -version = "0.8.1" +name = "crunchy" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] -name = "bincode" -version = "1.3.3" +name = "crypto-common" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "serde", + "generic-array", + "typenum", ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] [[package]] -name = "blake3" -version = "1.3.1" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "digest 0.10.3", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "block-buffer" +name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "block-padding", "generic-array", ] [[package]] -name = "block-buffer" -version = "0.10.2" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "generic-array", + "block-buffer 0.10.4", + "crypto-common", + "subtle", ] [[package]] -name = "block-padding" -version = "0.2.1" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "borsh" -version = "0.7.2" +name = "feature-probe" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b13fa9bf62be34702e5ee4526aff22530ae22fe34a0c4290d30d5e4e782e6" -dependencies = [ - "borsh-derive 0.7.2", -] +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" [[package]] -name = "borsh" -version = "0.9.3" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" -dependencies = [ - "borsh-derive 0.9.3", - "hashbrown", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "borsh-derive" -version = "0.7.2" +name = "five8_const" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6aaa45f8eec26e4bf71e7e5492cf53a91591af8f871f422d550e7cc43f6b927" +checksum = "72b4f62f0f8ca357f93ae90c8c2dd1041a1f665fde2f889ea9b1787903829015" dependencies = [ - "borsh-derive-internal 0.7.2", - "borsh-schema-derive-internal 0.7.2", - "proc-macro2 1.0.39", - "syn 1.0.96", + "five8_core", ] [[package]] -name = "borsh-derive" -version = "0.8.2" +name = "five8_core" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" -dependencies = [ - "borsh-derive-internal 0.8.2", - "borsh-schema-derive-internal 0.8.2", - "proc-macro-crate 0.1.5", - "proc-macro2 1.0.39", - "syn 1.0.96", -] +checksum = "94474d15a76982be62ca8a39570dccce148d98c238ebb7408b0a21b2c4bdddc4" [[package]] -name = "borsh-derive" -version = "0.9.3" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "borsh-derive-internal 0.9.3", - "borsh-schema-derive-internal 0.9.3", - "proc-macro-crate 0.1.5", - "proc-macro2 1.0.39", - "syn 1.0.96", + "typenum", + "version_check", ] [[package]] -name = "borsh-derive-internal" -version = "0.7.2" +name = "getrandom" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61621b9d3cca65cc54e2583db84ef912d59ae60d2f04ba61bc0d7fc57556bda2" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] -name = "borsh-derive-internal" -version = "0.8.2" +name = "getrandom" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] -name = "borsh-derive-internal" -version = "0.9.3" +name = "hashbrown" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "ahash", ] [[package]] -name = "borsh-schema-derive-internal" -version = "0.7.2" +name = "hashbrown" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b38abfda570837b0949c2c7ebd31417e15607861c23eacb2f668c69f6f3bf7" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" [[package]] -name = "borsh-schema-derive-internal" -version = "0.8.2" +name = "indexmap" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "equivalent", + "hashbrown 0.15.1", ] [[package]] -name = "borsh-schema-derive-internal" -version = "0.9.3" +name = "js-sys" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "wasm-bindgen", ] [[package]] -name = "bs58" -version = "0.3.1" +name = "keccak" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476e9cd489f9e121e02ffa6014a8ef220ecb15c05ed23fc34cca13925dc283fb" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] [[package]] -name = "bs58" -version = "0.4.0" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "bumpalo" -version = "3.10.0" +name = "libc" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] -name = "bv" -version = "0.11.1" +name = "libsecp256k1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" dependencies = [ - "feature-probe", + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", "serde", + "sha2 0.9.9", ] [[package]] -name = "bytemuck" -version = "1.9.1" +name = "libsecp256k1-core" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" dependencies = [ - "bytemuck_derive", + "crunchy", + "digest 0.9.0", + "subtle", ] [[package]] -name = "bytemuck_derive" -version = "1.1.0" +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562e382481975bc61d11275ac5e62a19abd00b0547d99516a415336f183dcd0e" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "libsecp256k1-core", ] [[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.1.0" +name = "libsecp256k1-gen-genmult" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core", +] [[package]] -name = "bzip2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +name = "lido" +version = "1.3.6" dependencies = [ - "bzip2-sys", - "libc", + "arrayref", + "borsh 1.5.3", + "num-derive 0.3.3", + "num-traits", + "serde", + "serde_derive", + "solana-program", + "spl-token", ] [[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" +name = "lock_api" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ - "cc", - "libc", - "pkg-config", + "autocfg", + "scopeguard", ] [[package]] -name = "caps" -version = "0.5.3" +name = "log" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61bf7211aad104ce2769ec05efcdfabf85ee84ac92461d142f22cf8badd0e54c" -dependencies = [ - "errno", - "libc", - "thiserror", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] -name = "cc" -version = "1.0.73" +name = "memchr" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" -dependencies = [ - "jobserver", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "memoffset" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] [[package]] -name = "chrono" -version = "0.4.19" +name = "num-bigint" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "libc", "num-integer", "num-traits", - "serde", - "time 0.1.44", - "winapi", ] [[package]] -name = "chrono-humanize" -version = "0.2.1" +name = "num-derive" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eddc119501d583fd930cb92144e605f44e0252c38dd89d9247fffa1993375cb" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "chrono", + "proc-macro2", + "quote", + "syn 1.0.96", ] [[package]] -name = "chunked_transfer" -version = "1.4.0" +name = "num-derive" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] [[package]] -name = "clap" -version = "2.34.0" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", + "num-traits", ] [[package]] -name = "clap" -version = "3.1.18" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "indexmap", - "lazy_static", - "strsim 0.10.0", - "termcolor", - "textwrap 0.15.0", + "autocfg", ] [[package]] -name = "clap_derive" -version = "3.1.18" +name = "num_enum" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ - "heck 0.4.0", - "proc-macro-error", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "num_enum_derive", ] [[package]] -name = "clap_lex" -version = "0.2.0" +name = "num_enum_derive" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "os_str_bytes", + "proc-macro-crate 1.1.3", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "combine" -version = "3.8.1" +name = "once_cell" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii 0.9.3", - "byteorder", - "either", - "memchr", - "unreachable", -] +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] -name = "console" -version = "0.15.0" +name = "opaque-debug" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "regex", - "terminal_size", - "unicode-width", - "winapi", -] +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] -name = "console_error_panic_hook" -version = "0.1.7" +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "cfg-if", - "wasm-bindgen", + "lock_api", + "parking_lot_core", ] [[package]] -name = "console_log" -version = "0.2.0" +name = "parking_lot_core" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501a375961cef1a0d44767200e66e4a559283097e91d0730b1d75dfb2f8a1494" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "log", - "web-sys", + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", ] [[package]] -name = "constant_time_eq" -version = "0.1.5" +name = "ppv-lite86" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] -name = "cpufeatures" -version = "0.2.2" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "libc", + "toml", ] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "proc-macro-crate" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "cfg-if", + "thiserror", + "toml", ] [[package]] -name = "crossbeam-channel" -version = "0.5.4" +name = "proc-macro-crate" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "cfg-if", - "crossbeam-utils", + "toml_edit", ] [[package]] -name = "crossbeam-deque" -version = "0.8.1" +name = "proc-macro2" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", + "unicode-ident", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.8" +name = "quote" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", + "proc-macro2", ] [[package]] -name = "crossbeam-utils" -version = "0.8.8" +name = "rand" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ - "cfg-if", - "lazy_static", + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", ] [[package]] -name = "crunchy" -version = "0.2.2" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] [[package]] -name = "crypto-common" -version = "0.1.3" +name = "rand_chacha" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" dependencies = [ - "generic-array", - "typenum", + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] -name = "crypto-mac" -version = "0.8.0" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "generic-array", - "subtle", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "crypto-mac" -version = "0.9.1" +name = "rand_core" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" dependencies = [ - "generic-array", - "subtle", + "getrandom 0.1.16", ] [[package]] -name = "crypto-mac" -version = "0.11.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "generic-array", - "subtle", + "getrandom 0.2.10", ] [[package]] -name = "curve25519-dalek" -version = "3.2.1" +name = "rand_hc" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" dependencies = [ - "byteorder", - "digest 0.9.0", "rand_core 0.5.1", - "subtle", - "zeroize", ] [[package]] -name = "dashmap" -version = "4.0.2" +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "cfg-if", - "num_cpus", - "rayon", + "bitflags 1.3.2", ] [[package]] -name = "derivation-path" -version = "0.1.3" +name = "rustc_version" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193388a8c8c75a490b604ff61775e236541b8975e98e5ca1f6ea97d122b7e2db" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "failure", + "semver", ] [[package]] -name = "derive_arbitrary" +name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98e23c06c035dac87bd802d98f368df73a7f2cb05a66ffbd1f377e821fac4af9" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "dialoguer" -version = "0.9.0" +name = "semver" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61579ada4ec0c6031cfac3f86fdba0d195a7ebeb5e36693bd53cb5999a25beeb" -dependencies = [ - "console", - "lazy_static", - "tempfile", - "zeroize", -] +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" [[package]] -name = "digest" -version = "0.9.0" +name = "serde" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ - "generic-array", + "serde_derive", ] [[package]] -name = "digest" -version = "0.10.3" +name = "serde_bytes" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ - "block-buffer 0.10.2", - "crypto-common", - "subtle", -] - -[[package]] -name = "dir-diff" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2860407d7d7e2e004bb2128510ad9e8d669e76fa005ccf567977b5d71b8b4a0b" -dependencies = [ - "walkdir", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dlopen" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" -dependencies = [ - "dlopen_derive", - "lazy_static", - "libc", - "winapi", -] - -[[package]] -name = "dlopen_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" -dependencies = [ - "libc", - "quote 0.6.13", - "syn 0.15.44", -] - -[[package]] -name = "ed25519" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" -dependencies = [ - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" -dependencies = [ - "curve25519-dalek", - "ed25519", - "rand 0.7.3", - "serde", - "sha2", - "zeroize", -] - -[[package]] -name = "ed25519-dalek-bip32" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3" -dependencies = [ - "derivation-path", - "ed25519-dalek", - "failure", - "hmac 0.9.0", - "sha2", -] - -[[package]] -name = "educe" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07b7cc9cd8c08d10db74fca3b20949b9b6199725c04a0cce6d543496098fcac" -dependencies = [ - "enum-ordinalize", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "encode_unicode" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" - -[[package]] -name = "encoding_rs" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-ordinalize" -version = "3.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2170fc0efee383079a8bdd05d6ea2a184d2a0f07a1c1dcabdb2fd5e9f24bc36c" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2 1.0.39", - "quote 1.0.18", - "rustc_version", - "syn 1.0.96", -] - -[[package]] -name = "enum_dispatch" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" -dependencies = [ - "once_cell", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", - "synstructure", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - -[[package]] -name = "feature-probe" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" - -[[package]] -name = "filetime" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "winapi", -] - -[[package]] -name = "flate2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" - -[[package]] -name = "futures" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" - -[[package]] -name = "futures-executor" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-macro" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "futures-sink" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" - -[[package]] -name = "futures-task" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-util" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" -dependencies = [ - "serde", - "typenum", - "version_check", -] - -[[package]] -name = "gethostname" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "goblin" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32401e89c6446dcd28185931a01b1093726d0356820ac744023e6850689bf926" -dependencies = [ - "log", - "plain", - "scroll", -] - -[[package]] -name = "h2" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util 0.7.2", - "tracing", -] - -[[package]] -name = "hash32" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" -dependencies = [ - "byteorder", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hidapi" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b1717343691998deb81766bfcd1dce6df0d5d6c37070b5a3de2bb6d39f7822" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "hmac" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" -dependencies = [ - "crypto-mac 0.8.0", - "digest 0.9.0", -] - -[[package]] -name = "hmac" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff" -dependencies = [ - "crypto-mac 0.9.1", - "digest 0.9.0", -] - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac 0.11.1", - "digest 0.9.0", -] - -[[package]] -name = "hmac-drbg" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" -dependencies = [ - "digest 0.9.0", - "generic-array", - "hmac 0.8.1", -] - -[[package]] -name = "http" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" -dependencies = [ - "http", - "hyper", - "rustls", - "tokio", - "tokio-rustls", -] - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "index_list" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9d968042a4902e08810946fc7cd5851eb75e80301342305af755ca06cb82ce" - -[[package]] -name = "indexmap" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "indicatif" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" -dependencies = [ - "console", - "lazy_static", - "number_prefix", - "regex", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jsonrpc-core" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "keccak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" - -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libsecp256k1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" -dependencies = [ - "arrayref", - "base64 0.12.3", - "digest 0.9.0", - "hmac-drbg", - "libsecp256k1-core", - "libsecp256k1-gen-ecmult", - "libsecp256k1-gen-genmult", - "rand 0.7.3", - "serde", - "sha2", - "typenum", -] - -[[package]] -name = "libsecp256k1-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" -dependencies = [ - "crunchy", - "digest 0.9.0", - "subtle", -] - -[[package]] -name = "libsecp256k1-gen-ecmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsecp256k1-gen-genmult" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" -dependencies = [ - "libsecp256k1-core", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "lido" -version = "1.3.3" -dependencies = [ - "bincode", - "borsh 0.9.3", - "num-derive", - "num-traits", - "serde", - "serde_derive", - "serde_json", - "solana-program", - "solana-program-test", - "solana-sdk", - "solana-vote-program", - "spl-token", - "testlib", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" - -[[package]] -name = "listener" -version = "1.3.3" -dependencies = [ - "arbitrary", - "chrono", - "clap 3.1.18", - "lido", - "num_cpus", - "rand 0.8.5", - "rusqlite", - "serde", - "serde_json", - "solana-client", - "solana-logger", - "solana-sdk", - "solido-cli-common", - "tiny_http 0.11.0", - "url", -] - -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5172b50c23043ff43dd53e51392f36519d9b35a8f3a410d30ece5d1aedd58ae" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "miniz_oxide" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" -dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "nix" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", - "memoffset", -] - -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" -dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "object" -version = "0.28.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "opentelemetry" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf9b1c4e9a6c4de793c632496fa490bdc0e1eea73f0c91394f7b6990935d22" -dependencies = [ - "async-trait", - "crossbeam-channel", - "futures", - "js-sys", - "lazy_static", - "percent-encoding", - "pin-project", - "rand 0.8.5", - "thiserror", -] - -[[package]] -name = "os_str_bytes" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" - -[[package]] -name = "ouroboros" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f357ef82d1b4db66fbed0b8d542cbd3c22d0bf5b393b3c257b9ba4568e70c9c3" -dependencies = [ - "aliasable", - "ouroboros_macro", - "stable_deref_trait", -] - -[[package]] -name = "ouroboros_macro" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44a0b52c2cbaef7dffa5fec1a43274afe8bd2a644fa9fc50a9ef4ff0269b1257" -dependencies = [ - "Inflector", - "proc-macro-error", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "pbkdf2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" -dependencies = [ - "crypto-mac 0.8.0", -] - -[[package]] -name = "pbkdf2" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05894bce6a1ba4be299d0c5f29563e08af2bc18bb7d48313113bed71e904739" -dependencies = [ - "crypto-mac 0.11.1", -] - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" -dependencies = [ - "unicode-xid 0.1.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proc-macro2-diagnostics" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", - "version_check", - "yansi", -] - -[[package]] -name = "qstring" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - -[[package]] -name = "quote" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" -dependencies = [ - "proc-macro2 1.0.39", -] - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", - "rand_pcg", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.3", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom 0.2.6", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "rayon" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.6", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" -dependencies = [ - "base64 0.13.0", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "rustls", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots", - "winreg", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rpassword" -version = "5.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "rusqlite" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.20.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - -[[package]] -name = "rustls-pemfile" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" -dependencies = [ - "base64 0.13.0", -] - -[[package]] -name = "rustversion" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scroll" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda28d4b4830b807a8b43f7b0e6b5df875311b3e7621d84577188c175b6ec1ec" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaaae8f38bb311444cfb7f1979af0bc9240d95795f75f9ceddf6a59b79ceffa0" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "semver" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" - -[[package]] -name = "serde" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "serde_json" -version = "1.0.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "serum-multisig" -version = "0.5.0" -dependencies = [ - "anchor-lang", -] - -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha3" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "keccak", - "opaque-debug", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" - -[[package]] -name = "slab" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "solana-account-decoder" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7760e28434b32eeaabd2fb57688d2a6d7bd58de2bde82607f57162ecbbb1c7" -dependencies = [ - "Inflector", - "base64 0.12.3", - "bincode", - "bs58 0.4.0", - "bv", - "lazy_static", - "serde", - "serde_derive", - "serde_json", - "solana-config-program", - "solana-sdk", - "solana-vote-program", - "spl-token", - "thiserror", - "zstd", -] - -[[package]] -name = "solana-address-lookup-table-program" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9508c839fd7cce94b21678ad28a6529cec2ea08acaa3514554ad99b564eed2f9" -dependencies = [ - "bincode", - "bytemuck", - "log", - "num-derive", - "num-traits", - "rustc_version", - "serde", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-program-runtime", - "solana-sdk", - "thiserror", -] - -[[package]] -name = "solana-banks-client" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02cda34ec70e84a9cf6636d8a2529531fb8cca75d31e6ebf0947662f7ee9b12" -dependencies = [ - "borsh 0.9.3", - "futures", - "solana-banks-interface", - "solana-program", - "solana-sdk", - "tarpc", - "thiserror", - "tokio", - "tokio-serde", -] - -[[package]] -name = "solana-banks-interface" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fac396ff86d7c8b5a3d086c4884c9b365440857933f6557114cc5d640eb4f9" -dependencies = [ - "serde", - "solana-sdk", - "tarpc", -] - -[[package]] -name = "solana-banks-server" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9fc996ae6c27db5af34b8144102ea2e6efbdd973c201fc08a917550ef74e50" -dependencies = [ - "bincode", - "futures", - "solana-banks-interface", - "solana-runtime", - "solana-sdk", - "solana-send-transaction-service", - "tarpc", - "tokio", - "tokio-serde", - "tokio-stream", -] - -[[package]] -name = "solana-bloom" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d16ac280328dabe13f77fc15be1e8e3c719a01ae90309e9f31df6dc24011af2" -dependencies = [ - "bv", - "fnv", - "log", - "rand 0.7.3", - "rayon", - "rustc_version", - "serde", - "serde_derive", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-sdk", -] - -[[package]] -name = "solana-bpf-loader-program" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb1e3df53df7bacecd261c0cdf0b1c6f4a84a724a1312d216ca3ee796c119b8" -dependencies = [ - "bincode", - "byteorder", - "libsecp256k1", - "log", - "solana-measure", - "solana-metrics", - "solana-program-runtime", - "solana-sdk", - "solana_rbpf", - "thiserror", -] - -[[package]] -name = "solana-bucket-map" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a97c11377eb1059d3fdddd02ad0fe0c480434f1df812f6b5d16ae7087af44a0" -dependencies = [ - "fs_extra", - "log", - "memmap2", - "rand 0.7.3", - "rayon", - "solana-logger", - "solana-measure", - "solana-sdk", - "tempfile", -] - -[[package]] -name = "solana-clap-utils" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec52a2bede69e10bb0583a55cce3945ab39c76415cc673069cbb2d2a60ee0ba" -dependencies = [ - "chrono", - "clap 2.34.0", - "rpassword", - "solana-perf", - "solana-remote-wallet", - "solana-sdk", - "thiserror", - "tiny-bip39", - "uriparse", - "url", -] - -[[package]] -name = "solana-cli-config" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76c1cdf42c00a375d8353ce013bc96697f45a2cee0c5473aec0c7ce5ff38478e" -dependencies = [ - "dirs-next", - "lazy_static", - "serde", - "serde_derive", - "serde_yaml", - "url", -] - -[[package]] -name = "solana-client" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6d0e50ba77919abd70de4bc935749a5d6dea4490e212f5f5959d827fc2d43e" -dependencies = [ - "base64 0.13.0", - "bincode", - "bs58 0.4.0", - "clap 2.34.0", - "indicatif", - "jsonrpc-core", - "log", - "rayon", - "reqwest", - "semver", - "serde", - "serde_derive", - "serde_json", - "solana-account-decoder", - "solana-clap-utils", - "solana-faucet", - "solana-measure", - "solana-net-utils", - "solana-sdk", - "solana-transaction-status", - "solana-version", - "solana-vote-program", - "thiserror", - "tokio", - "tungstenite", - "url", -] - -[[package]] -name = "solana-compute-budget-program" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe51d03e2899e00b8c32c6bd9930fc105be88b04243aa2d398b9825c9b09f34b" -dependencies = [ - "solana-program-runtime", - "solana-sdk", -] - -[[package]] -name = "solana-config-program" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416632936e7ac85e3a24925299e8c8ebc89ed190e090348e99a7158fb84551ff" -dependencies = [ - "bincode", - "chrono", - "serde", - "serde_derive", - "solana-program-runtime", - "solana-sdk", -] - -[[package]] -name = "solana-faucet" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ec717a20cd39a67b7e5ad2990be23c28eb08cb1dd40ee1591538420171d353e" -dependencies = [ - "bincode", - "byteorder", - "clap 2.34.0", - "log", - "serde", - "serde_derive", - "solana-clap-utils", - "solana-cli-config", - "solana-logger", - "solana-metrics", - "solana-sdk", - "solana-version", - "spl-memo", - "thiserror", - "tokio", -] - -[[package]] -name = "solana-frozen-abi" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30fc7f860ff75b2916735189534c5353db8d84953af7842f8dd9a6982dbcaaf" -dependencies = [ - "bs58 0.4.0", - "bv", - "generic-array", - "log", - "memmap2", - "rustc_version", - "serde", - "serde_derive", - "sha2", - "solana-frozen-abi-macro", - "solana-logger", - "thiserror", -] - -[[package]] -name = "solana-frozen-abi-macro" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e0c0121908ff5df45308b11eef48f696876064884d6aa12b70424dae7459d6" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "rustc_version", - "syn 1.0.96", -] - -[[package]] -name = "solana-logger" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd76a790f207ea9b523fb051c9d446e424e2e0b9fd539bb76c3ea797abf8ea5" -dependencies = [ - "env_logger", - "lazy_static", - "log", -] - -[[package]] -name = "solana-measure" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "405913a0367af42c58b34266d04cd8d3653f07e7ad95015e598aebdd84066169" -dependencies = [ - "log", - "solana-sdk", -] - -[[package]] -name = "solana-metrics" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4fd520d3360d0e8c90930da09512c5e896754c66faa05763b0ca516bf6a28c" -dependencies = [ - "env_logger", - "gethostname", - "lazy_static", - "log", - "reqwest", - "solana-sdk", -] - -[[package]] -name = "solana-net-utils" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca9ca350c274800cef7043bca93134c85dd615d7be4aebd5fab6ec9475ddfbe1" -dependencies = [ - "bincode", - "clap 2.34.0", - "log", - "nix", - "rand 0.7.3", - "serde", - "serde_derive", - "socket2", - "solana-logger", - "solana-sdk", - "solana-version", - "tokio", - "url", -] - -[[package]] -name = "solana-perf" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b922eca19a4d76a7181b1c72da6ea96a8bf0e4b36476155aa6cb826db0b8280" -dependencies = [ - "ahash", - "bincode", - "bv", - "caps", - "curve25519-dalek", - "dlopen", - "dlopen_derive", - "fnv", - "lazy_static", - "libc", - "log", - "nix", - "rand 0.7.3", - "rayon", - "serde", - "solana-bloom", - "solana-logger", - "solana-metrics", - "solana-rayon-threadlimit", - "solana-sdk", - "solana-vote-program", -] - -[[package]] -name = "solana-program" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c86be9edb9a0cb3fc44f776c6bef21d19bc5f69b0f83b3999d0d9d103e1c61" -dependencies = [ - "base64 0.13.0", - "bincode", - "bitflags", - "blake3", - "borsh 0.9.3", - "borsh-derive 0.9.3", - "bs58 0.4.0", - "bv", - "bytemuck", - "console_error_panic_hook", - "console_log", - "curve25519-dalek", - "getrandom 0.1.16", - "itertools", - "js-sys", - "lazy_static", - "libsecp256k1", - "log", - "num-derive", - "num-traits", - "parking_lot", - "rand 0.7.3", - "rustc_version", - "rustversion", - "serde", - "serde_bytes", - "serde_derive", - "sha2", - "sha3", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-sdk-macro", - "thiserror", - "wasm-bindgen", -] - -[[package]] -name = "solana-program-runtime" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b54e692cc189e105f30875b2ac4279da1ffccef8fb138e55e7339a94c1e9e5" -dependencies = [ - "base64 0.13.0", - "bincode", - "itertools", - "libc", - "libloading", - "log", - "num-derive", - "num-traits", - "rustc_version", - "serde", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-measure", - "solana-sdk", - "thiserror", -] - -[[package]] -name = "solana-program-test" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4342cdd61b179a74df61f9055b4e870959966a2207f50e9584d60f9c43b74a4" -dependencies = [ - "async-trait", - "base64 0.12.3", - "bincode", - "chrono-humanize", - "log", - "serde", - "solana-banks-client", - "solana-banks-server", - "solana-bpf-loader-program", - "solana-logger", - "solana-program-runtime", - "solana-runtime", - "solana-sdk", - "solana-vote-program", - "thiserror", - "tokio", -] - -[[package]] -name = "solana-rayon-threadlimit" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481ccd3319d37ebe6ba72c8b097247c8387912f8615702daf58919c0f86a2f9" -dependencies = [ - "lazy_static", - "num_cpus", -] - -[[package]] -name = "solana-remote-wallet" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26069e2c4c679817db11a1598f9bc400c273632f9e353449ee02bba125d6a3b7" -dependencies = [ - "base32", - "console", - "dialoguer", - "hidapi", - "log", - "num-derive", - "num-traits", - "parking_lot", - "qstring", - "semver", - "solana-sdk", - "thiserror", - "uriparse", -] - -[[package]] -name = "solana-runtime" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18bbc5aee342aeb48d03600b07b7908090376a8a66ecd6d0d2195818273cb088" -dependencies = [ - "arrayref", - "bincode", - "blake3", - "bv", - "bytemuck", - "byteorder", - "bzip2", - "crossbeam-channel", - "dashmap", - "dir-diff", - "flate2", - "fnv", - "index_list", - "itertools", - "lazy_static", - "log", - "memmap2", - "num-derive", - "num-traits", - "num_cpus", - "ouroboros", - "rand 0.7.3", - "rayon", - "regex", - "rustc_version", - "serde", - "serde_derive", - "solana-address-lookup-table-program", - "solana-bloom", - "solana-bucket-map", - "solana-compute-budget-program", - "solana-config-program", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-measure", - "solana-metrics", - "solana-program-runtime", - "solana-rayon-threadlimit", - "solana-sdk", - "solana-stake-program", - "solana-vote-program", - "symlink", - "tar", - "tempfile", - "thiserror", - "zstd", -] - -[[package]] -name = "solana-sdk" -version = "1.9.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1dc0a8e4f1dede7f0f91879a67459723e30c39018c34a1fdc30da2c4cd292fa" -dependencies = [ - "assert_matches", - "base64 0.13.0", - "bincode", - "bitflags", - "borsh 0.9.3", - "bs58 0.4.0", - "bytemuck", - "byteorder", - "chrono", - "derivation-path", - "digest 0.9.0", - "ed25519-dalek", - "ed25519-dalek-bip32", - "generic-array", - "hmac 0.11.0", - "itertools", - "js-sys", - "lazy_static", - "libsecp256k1", - "log", - "memmap2", - "num-derive", - "num-traits", - "pbkdf2 0.9.0", - "qstring", - "rand 0.7.3", - "rand_chacha 0.2.2", - "rustc_version", - "rustversion", "serde", - "serde_bytes", - "serde_derive", - "serde_json", - "sha2", - "sha3", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-program", - "solana-sdk-macro", - "thiserror", - "uriparse", - "wasm-bindgen", ] [[package]] -name = "solana-sdk-macro" -version = "1.9.28" +name = "serde_derive" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d66726cf3324c91601f047d34b7f9d9bf26982775f0f673655bb55df00ec87" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ - "bs58 0.4.0", - "proc-macro2 1.0.39", - "quote 1.0.18", - "rustversion", - "syn 1.0.96", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "solana-send-transaction-service" -version = "1.9.28" +name = "sha2" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdee500d68920df86128854a52816afd59d137ed5d4c59979d6fa6b36ef670dd" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "log", - "solana-logger", - "solana-metrics", - "solana-runtime", - "solana-sdk", + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", ] [[package]] -name = "solana-stake-program" -version = "1.9.28" +name = "sha2" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6311ce5695dbc1a6cdcbffb13f1f3a0b2ccfd08442352cb487d7bf52c99b17" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "bincode", - "log", - "num-derive", - "num-traits", - "rustc_version", - "serde", - "serde_derive", - "solana-config-program", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-metrics", - "solana-program-runtime", - "solana-sdk", - "solana-vote-program", - "thiserror", + "cfg-if", + "cpufeatures", + "digest 0.10.7", ] [[package]] -name = "solana-transaction-status" -version = "1.9.28" +name = "sha3" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b284ee14652a807ca6e4546257688bad2e771ea0f81ce31bc9e4d3757513ea6" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "Inflector", - "base64 0.12.3", - "bincode", - "bs58 0.4.0", - "lazy_static", - "log", - "serde", - "serde_derive", - "serde_json", - "solana-account-decoder", - "solana-measure", - "solana-metrics", - "solana-runtime", - "solana-sdk", - "solana-vote-program", - "spl-associated-token-account", - "spl-memo", - "spl-token", - "thiserror", + "digest 0.10.7", + "keccak", ] [[package]] -name = "solana-version" -version = "1.9.28" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5126f48ec74bef527b640b17843e35d7fbe89de9592d4b1afda94b08ae18540a" -dependencies = [ - "log", - "rustc_version", - "serde", - "serde_derive", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-sdk", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "solana-vote-program" -version = "1.9.28" +name = "smallvec" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554d67ee6ef9b559fe14d339baec35cf465ecc0f47bbd2622a03a0169c00b0eb" -dependencies = [ - "bincode", - "log", - "num-derive", - "num-traits", - "rustc_version", - "serde", - "serde_derive", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", - "solana-metrics", - "solana-program-runtime", - "solana-sdk", - "thiserror", -] +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] -name = "solana_rbpf" -version = "0.2.24" +name = "solana-account-info" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e138f6d6d4eb6a65f8e9f01ca620bc9907d79648d5038a69dd3f07b6ed3f1f" -dependencies = [ - "byteorder", - "combine", - "goblin", - "hash32", - "libc", - "log", - "rand 0.7.3", - "rustc-demangle", - "scroll", - "thiserror", - "time 0.1.44", -] - -[[package]] -name = "solido-cli" -version = "1.3.3" -dependencies = [ - "anchor-lang", - "anker", - "bincode", - "borsh 0.9.3", - "bs58 0.4.0", - "clap 3.1.18", - "derivation-path", - "itertools", - "lido", - "num-traits", - "num_cpus", - "rand 0.8.5", - "serde", - "serde_json", - "serum-multisig", - "solana-account-decoder", - "solana-clap-utils", - "solana-cli-config", - "solana-client", - "solana-config-program", - "solana-logger", - "solana-program", - "solana-remote-wallet", - "solana-sdk", - "solana-stake-program", - "solana-vote-program", - "solido-cli-common", - "spl-associated-token-account", - "spl-token", - "spl-token-swap", - "tiny_http 0.8.2", - "uriparse", -] - -[[package]] -name = "solido-cli-common" -version = "1.3.3" +checksum = "213a2c582fadaa92c84dbc7b1002a3c78d10cfed67a9a2795fae783ff3680f90" dependencies = [ - "anchor-lang", - "anker", "bincode", - "lido", - "num-traits", - "rusqlite", "serde", - "serde_json", - "serum-multisig", - "solana-account-decoder", - "solana-client", - "solana-config-program", - "solana-program", - "solana-sdk", - "solana-stake-program", - "solana-transaction-status", - "solana-vote-program", - "spl-token", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spl-associated-token-account" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393e2240d521c3dd770806bff25c2c00d761ac962be106e14e22dd912007f428" -dependencies = [ - "solana-program", - "spl-token", -] - -[[package]] -name = "spl-math" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ecdd22720b9e5ab578a862928f5010ca197419502bdace600ccd5d23dae9352" -dependencies = [ - "borsh 0.7.2", - "borsh-derive 0.8.2", - "num-derive", - "num-traits", - "solana-program", - "thiserror", - "uint", -] - -[[package]] -name = "spl-memo" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" -dependencies = [ - "solana-program", -] - -[[package]] -name = "spl-token" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93bfdd5bd7c869cb565c7d7635c4fafe189b988a0bdef81063cd9585c6b8dc01" -dependencies = [ - "arrayref", - "num-derive", - "num-traits", - "num_enum", - "solana-program", - "thiserror", -] - -[[package]] -name = "spl-token-swap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63b79be6174568e8724912b15e62d0c6b0424ac98397e9a5a867ac2881553af" -dependencies = [ - "arrayref", - "enum_dispatch", - "num-derive", - "num-traits", - "solana-program", - "spl-math", - "spl-token", - "thiserror", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "symlink" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" - -[[package]] -name = "syn" -version = "0.15.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" -dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", -] - -[[package]] -name = "syn" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", - "unicode-xid 0.2.3", + "solana-program-error", + "solana-program-memory", + "solana-pubkey", ] [[package]] -name = "tar" -version = "0.4.38" +name = "solana-atomic-u64" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +checksum = "7d7c8911028e3dd8f2cabe4471f10f64841644d2139fede5cb50eaac87c7e9e6" dependencies = [ - "filetime", - "libc", - "xattr", + "parking_lot", ] [[package]] -name = "tarpc" -version = "0.27.2" +name = "solana-bincode" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85d0a9369a919ba0db919b142a2b704cd207dfc676f7a43c2d105d0bc225487" +checksum = "ab3e4413097a51248c1c8cd24d6a6934272d677be54ad56b876999c335831dfc" dependencies = [ - "anyhow", - "fnv", - "futures", - "humantime", - "opentelemetry", - "pin-project", - "rand 0.8.5", + "bincode", "serde", - "static_assertions", - "tarpc-plugins", - "thiserror", - "tokio", - "tokio-serde", - "tokio-util 0.6.10", - "tracing", - "tracing-opentelemetry", -] - -[[package]] -name = "tarpc-plugins" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "solana-instruction", ] [[package]] -name = "termcolor" -version = "1.1.3" +name = "solana-borsh" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "6cfbaf3130c77c18399a51fa3c4e31db15d5c7a5fa2a0a024a378db21287e209" dependencies = [ - "winapi-util", + "borsh 0.10.3", + "borsh 1.5.3", ] [[package]] -name = "terminal_size" -version = "0.1.17" +name = "solana-clock" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "testlib" -version = "1.2.0" +checksum = "2a76c3f2dd8234264d738d30d032e083ceeb07b5a3168a5b129aafa437af1270" dependencies = [ - "anker", - "borsh 0.9.3", - "lido", - "num-derive", - "num-traits", - "rand 0.7.3", "serde", "serde_derive", - "solana-program", - "solana-program-test", - "solana-sdk", - "solana-vote-program", - "spl-memo", - "spl-token", - "spl-token-swap", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "textwrap" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" - -[[package]] -name = "thiserror" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" -dependencies = [ - "thiserror-impl", + "solana-sdk-macro", + "solana-sysvar-id", ] [[package]] -name = "thiserror-impl" -version = "1.0.31" +name = "solana-cpi" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "3a4d6df3dc4b5ccb24782e0f869f9ec279d6bde5326bb62ad830330e4e0d97e4" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "solana-account-info", + "solana-define-syscall", + "solana-instruction", + "solana-program-error", + "solana-pubkey", + "solana-stable-layout", ] [[package]] -name = "thread_local" -version = "1.1.4" +name = "solana-decode-error" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "a644cc267f15bc9b02fb97d9573869cd7f4bd654dc5c1eac1495bc7dd1acb074" dependencies = [ - "once_cell", + "num-traits", ] [[package]] -name = "time" -version = "0.1.44" +name = "solana-define-syscall" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] +checksum = "49d4c912636ef11caeca0c097d020060f0021bf2d45707c0d15811cc0ca1db78" [[package]] -name = "time" -version = "0.3.9" +name = "solana-epoch-schedule" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "a005abde2c87c2fef95bafbe1c91a1e255d9ba27e8fd692603f3ccee66bd8c4c" dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", + "serde", + "serde_derive", + "solana-sdk-macro", + "solana-sysvar-id", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "solana-fee-calculator" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "211959a60a1ff7d0ab839c8d63025fcffff9ed6ed345b396679c3d2a197ce2bf" +dependencies = [ + "log", + "serde", + "serde_derive", +] [[package]] -name = "tiny-bip39" -version = "0.8.2" +name = "solana-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +checksum = "6631c9888f0adfa287d7a09e669eea80017a655807352789f2b5056c0abd353a" dependencies = [ - "anyhow", - "hmac 0.8.1", - "once_cell", - "pbkdf2 0.4.0", - "rand 0.7.3", - "rustc-hash", - "sha2", - "thiserror", - "unicode-normalization", + "borsh 1.5.3", + "bs58", + "bytemuck", + "bytemuck_derive", + "js-sys", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-sanitize", "wasm-bindgen", - "zeroize", ] [[package]] -name = "tiny_http" -version = "0.8.2" +name = "solana-instruction" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39" +checksum = "c7398f745301e979ae0b7c9b54927f6fa471d063d4d65d49c3b392778f575692" dependencies = [ - "ascii 1.0.0", - "chrono", - "chunked_transfer", - "log", - "url", + "bincode", + "borsh 1.5.3", + "getrandom 0.2.10", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-define-syscall", + "solana-pubkey", + "wasm-bindgen", ] [[package]] -name = "tiny_http" -version = "0.11.0" -source = "git+https://github.com/ruuda/tiny-http?rev=3568e8880f995dd0348feff9e29645fce995b534#3568e8880f995dd0348feff9e29645fce995b534" +name = "solana-last-restart-slot" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8974782a4eeb5205c61d1e0b4c716f375ff11ed55b16a7837752cc50d5f7414" dependencies = [ - "ascii 1.0.0", - "chunked_transfer", - "log", - "time 0.3.9", - "url", + "serde", + "serde_derive", + "solana-sdk-macro", + "solana-sysvar-id", ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "solana-msg" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "aa324c0dabbbb43365e072a0bbc8285fbd2106ead49f9a69d654981adffaf4d6" dependencies = [ - "tinyvec_macros", + "solana-define-syscall", ] [[package]] -name = "tinyvec_macros" -version = "0.1.0" +name = "solana-native-token" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "56baf987d7700eb58dd7b522af5281cc40fdfc790290a17c16ec0b037f31084b" [[package]] -name = "tokio" -version = "1.14.1" +name = "solana-program" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d0183f6f6001549ab68f8c7585093bb732beefbcf6d23a10b9b95c73a1dd49" +checksum = "c02871dca5b8a09d8c97e3c8223ecf7e2f9be357df55730c2cad735f44354ac9" dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "once_cell", + "base64 0.22.1", + "bincode", + "bitflags 2.6.0", + "blake3", + "borsh 0.10.3", + "borsh 1.5.3", + "bs58", + "bv", + "bytemuck", + "bytemuck_derive", + "console_error_panic_hook", + "console_log", + "curve25519-dalek", + "five8_const", + "getrandom 0.2.10", + "js-sys", + "lazy_static", + "log", + "memoffset", + "num-bigint", + "num-derive 0.4.2", + "num-traits", "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "tokio-macros", - "winapi", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.10.8", + "sha3", + "solana-account-info", + "solana-atomic-u64", + "solana-bincode", + "solana-borsh", + "solana-clock", + "solana-cpi", + "solana-decode-error", + "solana-define-syscall", + "solana-epoch-schedule", + "solana-fee-calculator", + "solana-hash", + "solana-instruction", + "solana-last-restart-slot", + "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-sanitize", + "solana-sdk-macro", + "solana-secp256k1-recover", + "solana-serde-varint", + "solana-serialize-utils", + "solana-sha256-hasher", + "solana-short-vec", + "solana-slot-hashes", + "solana-slot-history", + "solana-stable-layout", + "solana-sysvar-id", + "solana-transaction-error", + "thiserror", + "wasm-bindgen", ] [[package]] -name = "tokio-macros" -version = "1.8.0" +name = "solana-program-entrypoint" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "fa2231a596d871922e3e96e7bd6fec3c1abb72a3f15dea90e004dfd42cd4ad13" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "solana-account-info", + "solana-msg", + "solana-program-error", + "solana-pubkey", ] [[package]] -name = "tokio-rustls" -version = "0.23.4" +name = "solana-program-error" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "f02256ba0287dd14b84ae8e56b93f56d69aba330c5eb5e356de40e9ed37d6471" dependencies = [ - "rustls", - "tokio", - "webpki", + "borsh 1.5.3", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-pubkey", ] [[package]] -name = "tokio-serde" -version = "0.8.0" +name = "solana-program-memory" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" +checksum = "81e8df1bb4b18fd92b0d2edac26660ffb9062d41e80da83722f174aeda1c1ba4" dependencies = [ - "bincode", - "bytes", - "educe", - "futures-core", - "futures-sink", - "pin-project", - "serde", - "serde_json", + "num-traits", + "solana-define-syscall", ] [[package]] -name = "tokio-stream" -version = "0.1.9" +name = "solana-program-option" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] +checksum = "6b67473cc913ec7f7438e73512a940d94e17c6248df6e2eff2c11a4fe685db1e" [[package]] -name = "tokio-util" -version = "0.6.10" +name = "solana-program-pack" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +checksum = "5ca6983f71f7bb2480e2a310e5ff99269414da1bf33bfd2828e25afdd0a16307" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", - "slab", - "tokio", + "solana-program-error", ] [[package]] -name = "tokio-util" -version = "0.7.2" +name = "solana-pubkey" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "a7e820ad0abe44b6c41878a0d655e678d535d32da1e67828e41684fc57f06918" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", + "borsh 0.10.3", + "borsh 1.5.3", + "bs58", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "five8_const", + "getrandom 0.2.10", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64", + "solana-decode-error", + "solana-define-syscall", + "solana-sanitize", + "solana-sha256-hasher", + "wasm-bindgen", ] [[package]] -name = "toml" -version = "0.5.9" +name = "solana-rent" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "d1d71c2a454e30cd4b190e57e95b8e89f339bfdab582c2d8443d83a0a020e175" dependencies = [ "serde", + "serde_derive", + "solana-sdk-macro", + "solana-sysvar-id", ] [[package]] -name = "tower-service" -version = "0.3.1" +name = "solana-sanitize" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "8e7bf74043d11d90e478a8fc2993fc34d332721c5958810a7dc49c9d5e9e086f" [[package]] -name = "tracing" -version = "0.1.35" +name = "solana-sdk-macro" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "6ffc7208fe8f6c36fdeec03eb3b8e7aa5e637c7992012d9c68e354605bcdc387" dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", + "bs58", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "tracing-attributes" -version = "0.1.21" +name = "solana-secp256k1-recover" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +checksum = "a8901b7befaa3864619392ef7302afce5d9769231acda2490b159009c83e21ab" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "libsecp256k1", + "solana-define-syscall", + "thiserror", ] [[package]] -name = "tracing-core" -version = "0.1.27" +name = "solana-serde-varint" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" +checksum = "3a43dacb52deca3af4a72a88b1a533506b75ff7d9428fc0f8ebe2ebd38f0190b" dependencies = [ - "once_cell", - "valuable", + "serde", ] [[package]] -name = "tracing-opentelemetry" -version = "0.15.0" +name = "solana-serialize-utils" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599f388ecb26b28d9c1b2e4437ae019a7b336018b45ed911458cd9ebf91129f6" +checksum = "1335151f8519df13183a7c913764cbfef8afaae282dc7196c1a30cc3ef8c2869" dependencies = [ - "opentelemetry", - "tracing", - "tracing-core", - "tracing-subscriber", + "solana-instruction", + "solana-pubkey", + "solana-sanitize", ] [[package]] -name = "tracing-subscriber" -version = "0.2.25" +name = "solana-sha256-hasher" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +checksum = "c49345545ea5144954b065aea76ea527766b3475402dc8278b91f6ffbb425975" dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", + "sha2 0.10.8", + "solana-define-syscall", + "solana-hash", ] [[package]] -name = "try-lock" -version = "0.2.3" +name = "solana-short-vec" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "1a7f15694e7c7bc31ac0946163a56fdeb73c66fa5601f3b4ca2dc5703b975e31" +dependencies = [ + "serde", +] [[package]] -name = "tungstenite" -version = "0.16.0" +name = "solana-slot-hashes" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad3713a14ae247f22a728a0456a545df14acf3867f905adff84be99e23b3ad1" +checksum = "861fadc8ece3779402ad16042fe60556fdb9fd5b94d624a1e70e35044db04a21" dependencies = [ - "base64 0.13.0", - "byteorder", - "bytes", - "http", - "httparse", - "log", - "rand 0.8.5", - "rustls", - "sha-1", - "thiserror", - "url", - "utf-8", - "webpki", - "webpki-roots", + "serde", + "serde_derive", + "solana-hash", + "solana-sysvar-id", ] [[package]] -name = "typenum" -version = "1.15.0" +name = "solana-slot-history" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "6ec1680dd1e71b236472e4f55218e3829b1e8e23f2216437b9a482307b8caa4a" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sysvar-id", +] [[package]] -name = "uint" -version = "0.8.5" +name = "solana-stable-layout" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db035e67dfaf7edd9aebfe8676afcd63eed53c8a4044fed514c8cccf1835177" +checksum = "36bffd885d1e90782076e1e624c32b29862316c85a5f80dd255878935e49c73b" dependencies = [ - "byteorder", - "crunchy", - "rustc-hex", - "static_assertions", + "solana-instruction", + "solana-pubkey", ] [[package]] -name = "unicode-bidi" -version = "0.3.8" +name = "solana-sysvar-id" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "393366deb64dfaaaf6f9bc49a7bde1271642eaa2a5ed461f20d393cefd6919e8" +dependencies = [ + "solana-pubkey", +] [[package]] -name = "unicode-ident" -version = "1.0.0" +name = "solana-transaction-error" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "18fdf1f0647266cf4477de798dc04adf5d82b37229732384a2b8453a6e57ea0d" +dependencies = [ + "solana-instruction", + "solana-sanitize", +] [[package]] -name = "unicode-normalization" -version = "0.1.19" +name = "spl-token" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" dependencies = [ - "tinyvec", + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum", + "solana-program", + "thiserror", ] [[package]] -name = "unicode-segmentation" -version = "1.9.0" +name = "subtle" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] -name = "unicode-width" -version = "0.1.9" +name = "syn" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "unicode-xid" -version = "0.1.0" +name = "syn" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "unicode-xid" -version = "0.2.3" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] [[package]] -name = "unreachable" -version = "1.0.0" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "void", + "proc-macro2", + "quote", + "syn 2.0.87", ] [[package]] -name = "untrusted" -version = "0.7.1" +name = "tinyvec" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] [[package]] -name = "uriparse" -version = "0.6.4" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" -dependencies = [ - "fnv", - "lazy_static", -] +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] -name = "url" -version = "2.2.2" +name = "toml" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", + "serde", ] [[package]] -name = "utf-8" -version = "0.7.6" +name = "toml_datetime" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] -name = "valuable" -version = "0.1.0" +name = "toml_edit" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "typenum" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] -name = "vec_map" -version = "0.8.2" +name = "unicode-ident" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "version_check" @@ -4252,33 +1451,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -4287,75 +1459,64 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", - "lazy_static", "log", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ - "quote 1.0.18", + "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", + "proc-macro2", + "quote", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" @@ -4368,134 +1529,73 @@ dependencies = [ ] [[package]] -name = "webpki" -version = "0.22.0" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "ring", - "untrusted", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "webpki-roots" -version = "0.22.3" +name = "windows_aarch64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" -dependencies = [ - "webpki", -] +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows_i686_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "winapi-util" -version = "0.1.5" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "winreg" -version = "0.10.1" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "xattr" -version = "0.2.3" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" -dependencies = [ - "libc", -] +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "winnow" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ - "linked-hash-map", + "memchr", ] -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - [[package]] name = "zeroize" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", - "synstructure", -] - -[[package]] -name = "zstd" -version = "0.9.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "4.1.3+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" -dependencies = [ - "cc", - "libc", -] diff --git a/Cargo.toml b/Cargo.toml index 1e3d95996..c85cf3bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,12 @@ [workspace] resolver = "2" members = [ - "anker", - "cli/maintainer", - "cli/common", - "cli/listener", - "multisig/programs/multisig", + # "cli/maintainer", + # "cli/common", + # "cli/listener", + # "multisig/programs/multisig", "program", - "testlib", + # "testlib", ] # Ensure that we don't leave half a program running if something in a thread panics. @@ -18,8 +17,8 @@ panic = "abort" panic = "abort" # patch Anchor 0.13 to be able to use Solana 1.9.28 -[patch.crates-io] -anchor-lang = { git = "https://github.com/lidofinance/anchor", branch = "solana-v1.9.28" } +# [patch.crates-io] +# anchor-lang = { git = "https://github.com/lidofinance/anchor", branch = "solana-v1.9.28" } # https://github.com/tiny-http/tiny-http/pull/225 -tiny_http = { git = "https://github.com/ruuda/tiny-http", rev = "3568e8880f995dd0348feff9e29645fce995b534" } +# tiny_http = { git = "https://github.com/tiny-http/tiny-http", rev = "f0fce7ed0bdf7439b5bd0b2d15fa82944aac30f6" } diff --git a/README.md b/README.md index e00d93e54..7668452f6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Lido for Solana -**Note:** The new upstream is at [lidofinance/solido](https://github.com/lidofinance/solido). - *Lido for Solana* (“Solido” for short) is a [Lido DAO][lido]-governed liquid staking protocol for the Solana blockchain. Anyone who stakes their SOL tokens with Lido will be issued an on-chain representation of the SOL staking position @@ -42,8 +40,6 @@ and documentation, are in a different repository, which is not yet public. * `program` — Solido, the on-chain Solana BPF program that implements Lido for Solana. - * `anker` — Anker, the on-chain Solana BPF program that implements integration - with the [Anchor Protocol][anchor-protocol] on [Terra][terra]. * `multisig` — A pinned version of the on-chain [Serum multisig program][multisig], used as the upgrade authority of the Solido program, and as the manager of the Solido instance. @@ -51,7 +47,7 @@ and documentation, are in a different repository, which is not yet public. programs. * `docker` — Dockerfiles for reproducible builds, and for the maintainer image. * `testlib` — Utilities for writing tests using the `solana-program-test` test - framework. The individual tests are in `program/tests` and `anker/tests`. + framework. The individual tests are in `program/tests`. * `tests` — Scripts that test the actual `solido` binary and on-chain program. [multisig]: https://github.com/project-serum/multisig @@ -75,6 +71,8 @@ you need: * `libhidapi-dev` * `pkg-config` * `openssl` + * `build-essential` + * `libsqlite3-dev` The Solana version that we test against is listed in our [CI config][ci-config]. @@ -82,7 +80,7 @@ The Solana version that we test against is listed in our [CI config][ci-config]. [solana-tools]: https://docs.solana.com/cli/install-solana-cli-tools [docker]: https://docs.docker.com/engine/install/ [reproduce]: https://chorusone.github.io/solido/development/reproducibility/ -[ci-config]: https://github.com/ChorusOne/solido/blob/main/.github/workflows/build.yml +[ci-config]: https://github.com/lidofinance/solido/blob/main/.github/workflows/build.yml ### Cloning the repository @@ -90,7 +88,7 @@ This repository contains a Git submodule. To clone it, pass `--recurse-submodules`: ```console -$ git clone --recurse-submodules https://github.com/chorusone/solido +$ git clone --recurse-submodules https://github.com/lidofinance/solido ``` If you already cloned the repository without submodules, you can still @@ -124,7 +122,7 @@ $ cargo build-bpf $ cargo test-bpf ``` -The programs `lido.so`, `anker.so`, and `serum_multisig.so` can then be found in +The programs `lido.so`, and `serum_multisig.so` can then be found in `target/deploy`. ### Docker container diff --git a/anker/Cargo.toml b/anker/Cargo.toml deleted file mode 100644 index 35ad80fb4..000000000 --- a/anker/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -authors = ["Chorus One "] -license = "GPL-3.0" -edition = "2018" -name = "anker" -version = "1.3.3" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[features] -no-entrypoint = [] -test-bpf = [] - -[dependencies] -bech32 = "0.8.1" -borsh = "0.9.1" -lido = {path = "../program", features = ["no-entrypoint"]} -num-derive = "0.3" -num-traits = "0.2" -serde = "1.0.121" -solana-program = "1.9.28" -spl-token = { version = "3.1.1", features = ["no-entrypoint"] } -spl-token-swap = { version = "2.1.0", features = ["no-entrypoint"] } - -[dev-dependencies] -testlib = { path = "../testlib"} -solana-program-test = "1.9.28" -solana-sdk = "1.9.28" -hex = "0.4.3" - -[lib] -crate-type = ["cdylib", "lib"] diff --git a/anker/SECURITY.md b/anker/SECURITY.md deleted file mode 100644 index 2ed959398..000000000 --- a/anker/SECURITY.md +++ /dev/null @@ -1,152 +0,0 @@ -# Anker program security - -This document outlines our threat model and security considerations for the -Anker program. For the general Solido security policy and how to report -vulnerabilities, see SECURITY.md in the root of the repository. - -## Overview - -The Anker program is the Solana-side program that implements bSOL (bonded SOL) -support for the [Anchor Protocol][anchorprotocol] on the [Terra][terra] -blockchain. Anchor is a money market between lenders, who deposit UST -stablecoin in return for a stable yield, and borrowers, who borrow that UST by -putting up bonded asses such as bSOL or bETH as collateral. - -Bonded assets are nominally pegged to their underlying native token (so 1 bSOL = -1 SOL), but backed by staked assets, in this case Lido’s stSOL. The Anker -program can mint and burn bSOL, and it maintains a reserve of stSOL, such that -for every bSOL minted, the reserve contains 1 SOL worth of stSOL. Because stSOL -is a value-accuring token, over time the value of the reserve will be higher -than what is needed to back the bSOL supply. To restore the peg, we swap the -excess stSOL for UST on an AMM, and the proceeds go to the Anchor protocol on -Terra (through [Wormhole][wormhole]), which uses them to provide the yield for -lenders. - -To avoid confusion with [the Solana development framework / eDSL that is also -called Anchor][serum-anchor], we named our program “Anker”. - -[anchorprotocol]: https://anchorprotocol.com/ -[terra]: https://www.terra.money/ -[wormhole]: https://wormholebridge.com/ -[serum-anchor]: https://github.com/project-serum/anchor - -## Components - -As a cross-chain protocol, bSOL support for the Anchor protocol involves -multiple components: - - * The Anker program on the Solana blockchain. This program accepts stSOL - deposits and mints bSOL in return, and when users return their bSOL, the - program burns it and returns stSOL. This means that bSOL is natively a Solana - SPL token, and the bSOL on Terra will be bridged. The source code of this - program is in the `anker` directory of . - - * The bSOL contract on the Terra blockchain. For technical reasons, on the - Terra side the Wormhole-wrapped bSOL tokens need to be wrapped once more, and - this contract is responsible for that. The repository is not public while - the contract is being developed, but it will be analogous to the - existing bETH contract at - . - - * The Anchor contract itself on the Terra blockchain. - - * Wormhole bridge, which is used in a few places: - * To get UST to Solana in the first place. (It lives natively on Terra.) - * To send the bSOL that users receive to Terra, so they can deposit it into - Anchor. This step will have to be done manually by users. - * To send the UST proceeds of the staking rewards to Terra. - - * Solido, the Lido for Solana program. The Anker program holds stSOL, which is - minted by Solido, and aside from that, Anker inspects the Solido state to - obtain the stSOL/SOL exchange rate, in order to compute bSOL/stSOL exchange - rate that is needed to maintain the 1 bSOL = 1 SOL peg. - - * An AMM for swapping stSOL for UST. We intend to use [Orca][orca] for this, - which is a deployment of the [SPL Token Swap][spl-token-swap] program. - -[orca]: https://www.orca.so/ -[spl-token-swap]: https://github.com/solana-labs/solana-program-library/tree/master/token-swap - -This repository only contains the Anker program (and the Solido program), and -only the Anker program is in scope for the purpose of auditing (and in the -future, for the bug bounty program). The bSOL contracts on the Terra side will -be audited separately. - -## Roles - -There are two types of actors involved in the Anker program: - - * **The manager**. The manager is the upgrade authority of the Anker program, - and it can sign configuration changes, such as the destination address for - UST rewards. The manager will be the same multisig instance that also acts as - the manager for the Solido program. We trust the manager and assume that the - manager acts in the best interest of the Anker program. We assume that the - manager will configure the Anker program correctly. - - * **Users**. Users deposit stSOL and receive bSOL in return. - -There is no separate “maintainer” role like in the Solido program. The Anker -program is intended to be fully permissionless, and aside from configuration -changes, there are no privileged instructions. In particular, swapping stSOL -for UST is something that can be done by anybody. - -## Functionality - -The Anker program has three main functions for everyday use: - - * **Deposit**, where users deposit stSOL and receive bSOL in return. - * **Withdraw**, where users redeem their stSOL by returning the bSOL. - * **Claim Rewards**, where, if the value of the stSOL in the reserve is greater - than what is needed to back the bSOL supply at a 1 bSOL = 1 SOL exchange - rate, the program can swap the excess stSOL for UST against an AMM, and it - sends the proceeds through Wormhole to a preconfigured address on Terra. - -None of these functions are privileged. In practice the existing Solido -maintainer bot is going to be responsible for calling *Claim Rewards* when -possible. Note that, because the Solido exchange rate changes at most once per -epoch, claiming rewards is possible at most once per epoch. - -## Singleton Anker instance - -We follow Neodyme’s recommendation about making the Anker instance a singleton, -with one modification: because Solido is not (enforced to be) a singleton, we -have one Anker instance per Solido instance. The Anker instance lives at a -program-derived address that is derived from the Solido instance’s address. This -ensures that there is one unique Anker instance associated with every Solido -instance. - -## Scope and trust - -For auditing, and a future bug bounty, the scope is limited to the Anker program -and its responsibilities. Examples of issues we would like to know about: - - * Minting bSOL without providing the backing stSOL of the right value. - * Withdrawing stSOL without returning any bSOL of the right value. - * Preventing future deposits or withdrawals. - * Changing configuration without signature from the manager. - * Sending UST proceeds to a different recipient than the address configured in - the Anker instance. - -Once we mint the bSOL, what happens to it (either on Solana or on Terra) is no -longer the concern of the Anker program. Examples of issues out of scope: - - * Vulnerabilities in the bSOL contract on the Terra side (it will be audited - separately). - * Attacks that involve manipulating Wormhole. - -The Anker program necessarily interacts with other programs, and those programs -are upgradeable. Therefore: - - * We trust the Orca Swap program and its upgrade authority. - * We trust the Wormhole program and its authority. - -We are interested in minimizing the impact that a compromised Orca or Wormhole -program could have, but in principle we trust these programs. - -We also trust the contracts on the Terra side that we send the UST rewards to. - -## Further resources - - * [Anchor Protocol documentation](https://docs.anchorprotocol.com/) - * [Lido for Solana documentation](https://docs.solana.lido.fi/) - * [Anchor bAssets guide](https://docs.google.com/document/d/1tvw_hHBRhLSLNCNOWxjfQU86jtQmR-KSfUoh-mhEWUo/edit) diff --git a/anker/src/entrypoint.rs b/anker/src/entrypoint.rs deleted file mode 100644 index ec7608ede..000000000 --- a/anker/src/entrypoint.rs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -//! Program entrypoint - -#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] - -use crate::processor; -use solana_program::{ - account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, -}; - -entrypoint!(process_instruction); -fn process_instruction( - program_id: &Pubkey, - accounts: &[AccountInfo], - instruction_data: &[u8], -) -> ProgramResult { - if let Err(error) = processor::process(program_id, accounts, instruction_data) { - Err(error) - } else { - Ok(()) - } -} diff --git a/anker/src/error.rs b/anker/src/error.rs deleted file mode 100644 index fc7bd6b83..000000000 --- a/anker/src/error.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::fmt::Formatter; - -use num_derive::FromPrimitive; -use solana_program::{decode_error::DecodeError, program_error::ProgramError}; - -/// Errors that may be returned by the Anker program. -/// -/// Note: the integer representations of these errors start counting at 4000, -/// to avoid them overlapping with the Solido errors. When a Solana program fails, -/// all we get is the error code, and if we use the same "namespace" of small -/// integers, we can't tell from the error alone which program it was that failed. -/// This matters in the CLI client where we print all possible interpretations of -/// the error code. -#[derive(Clone, Debug, Eq, FromPrimitive, PartialEq)] -pub enum AnkerError { - /// We failed to deserialize an SPL token account. - InvalidTokenAccount = 4000, - - /// We expected the SPL token account to be owned by the SPL token program. - InvalidTokenAccountOwner = 4001, - - /// The mint of a provided SPL token account does not match the expected mint. - InvalidTokenMint = 4002, - - /// The provided reserve is invalid. - InvalidReserveAccount = 4003, - - /// The provided Solido state is different from the stored one. - InvalidSolidoInstance = 4004, - - /// The one of the provided accounts does not match the expected derived address. - InvalidDerivedAccount = 4005, - - /// An account is not owned by the expected owner. - InvalidOwner = 4006, - - /// Wrong SPL Token Swap instance or program. - WrongSplTokenSwap = 4007, - - /// Wrong parameters for the SPL Token Swap instruction. - WrongSplTokenSwapParameters = 4008, - - /// The provided rewards destination is different from what is stored in the instance. - InvalidRewardsDestination = 4009, - - /// The amount of rewards to be claimed are zero. - ZeroRewardsToClaim = 4010, - - /// Arguments/Accounts for SendRewards are wrong. - InvalidSendRewardsParameters = 4011, - - /// After swapping, we are left with less stSOL than we intended. - TokenSwapAmountInvalid = 4012, - - /// The most recent price sample is too recent, we can’t call `FetchPoolPrice` yet. - FetchPoolPriceTooEarly = 4013, - - /// We failed to compute the price of stSOL in UST. - PoolPriceUndefined = 4014, - - /// `FetchPoolPrice` has not been called recently, we must call it before selling the rewards. - FetchPoolPriceNotCalledRecently = 4015, - - /// Value of `sell_rewards_min_out_bps` is greater than 100% (1_000_000). - InvalidSellRewardsMinOutBps = 4016, - - /// The most recent price sample is too recent, we can’t call `SellRewards` yet. - SellRewardsTooEarly = 4017, -} - -// Just reuse the generated Debug impl for Display. It shows the variant names. -impl std::fmt::Display for AnkerError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self, f) - } -} - -impl From for ProgramError { - fn from(e: AnkerError) -> Self { - ProgramError::Custom(e as u32) - } -} - -impl DecodeError for AnkerError { - fn type_of() -> &'static str { - "Anker Error" - } -} diff --git a/anker/src/instruction.rs b/anker/src/instruction.rs deleted file mode 100644 index 917f51e08..000000000 --- a/anker/src/instruction.rs +++ /dev/null @@ -1,598 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use lido::{accounts_struct, accounts_struct_meta, error::LidoError, token::StLamports}; -use solana_program::{ - account_info::AccountInfo, - instruction::{AccountMeta, Instruction}, - program_error::ProgramError, - pubkey::Pubkey, - system_program, sysvar, -}; - -use crate::{token::BLamports, wormhole::TerraAddress}; - -#[repr(C)] -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema)] -pub enum AnkerInstruction { - Initialize { - #[allow(dead_code)] // It is not dead code when compiled for BPF. - terra_rewards_destination: TerraAddress, - - #[allow(dead_code)] // It is not dead code when compiled for BPF. - sell_rewards_min_out_bps: u64, - }, - - /// Deposit a given amount of StSOL, gets bSOL in return. - /// - /// This can be called by anybody. - Deposit { - #[allow(dead_code)] // but it's not - amount: StLamports, - }, - - /// Withdraw a given amount of bSOL. - /// - /// Caller provides some `amount` of bLamports that are to be burned in - /// order to withdraw stSOL. - Withdraw { - #[allow(dead_code)] // but it's not - amount: BLamports, - }, - - /// Record the current stSOL/UST exchange rate, which is later used to limit - /// slippage in `SellRewards`. - FetchPoolPrice, - - /// Sell rewards to the UST reserve. - SellRewards, - - /// Transfer from the UST reserve to terra through Wormhole. - SendRewards { - /// Random number used to differentiate similar transactions. - #[allow(dead_code)] // It is not dead code when compiled for BPF. - wormhole_nonce: u32, - }, - - /// Change the Anker's rewards destination address on Terra: - /// `terra_rewards_destination`. - ChangeTerraRewardsDestination { - #[allow(dead_code)] // It is not dead code when compiled for BPF. - terra_rewards_destination: TerraAddress, - }, - - /// Change the token pool instance. - ChangeTokenSwapPool, - - /// Change the `sell_rewards_min_out_bps`. - ChangeSellRewardsMinOutBps { - #[allow(dead_code)] // It is not dead code when compiled for BPF. - sell_rewards_min_out_bps: u64, - }, -} - -impl AnkerInstruction { - pub fn to_vec(&self) -> Vec { - // `BorshSerialize::try_to_vec` returns a Result, because it uses - // `Borsh::serialize`, which takes an arbitrary writer, and which can - // therefore return an IoError. But when serializing to a vec, there - // is no IO, so for this particular writer, it should never fail. - self.try_to_vec() - .expect("Serializing an Instruction to Vec does not fail.") - } -} - -accounts_struct! { - InitializeAccountsMeta, InitializeAccountsInfo { - pub fund_rent_from { - is_signer: true, - is_writable: true, // It pays for the rent of the new accounts. - }, - pub anker { - is_signer: false, - is_writable: true, // Writable because we need to initialize it. - }, - pub solido { - is_signer: false, - is_writable: false, - }, - pub solido_program { - is_signer: false, - is_writable: false, - }, - // Store wormhole program ids - pub wormhole_core_bridge_program_id { - is_signer: false, - is_writable: false, - }, - pub wormhole_token_bridge_program_id { - is_signer: false, - is_writable: false, - }, - pub st_sol_mint { - is_signer: false, - is_writable: false, - }, - pub b_sol_mint { - is_signer: false, - is_writable: false, - }, - pub st_sol_reserve_account { - is_signer: false, - is_writable: true, // Writable because we need to initialize it. - }, - pub ust_reserve_account { - is_signer: false, - is_writable: true, // Writable because we need to initialize it. - }, - pub reserve_authority { - is_signer: false, - is_writable: false, - }, - // Instance of the token swap data used for trading StSOL for UST. - pub token_swap_pool { - is_signer: false, - is_writable: false, - }, - pub ust_mint { - is_signer: false, - is_writable: false, - }, - const sysvar_rent = sysvar::rent::id(), - const system_program = system_program::id(), - const spl_token = spl_token::id(), - } -} - -pub fn initialize( - program_id: &Pubkey, - accounts: &InitializeAccountsMeta, - terra_rewards_destination: TerraAddress, - sell_rewards_min_out_bps: u64, -) -> Instruction { - let data = AnkerInstruction::Initialize { - terra_rewards_destination, - sell_rewards_min_out_bps, - }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - DepositAccountsMeta, DepositAccountsInfo { - pub anker { - is_signer: false, - is_writable: true, // We update metrics. - }, - // For reading the stSOL/SOL exchange rate. - pub solido { - is_signer: false, - is_writable: false, - }, - pub from_account { - is_signer: false, - is_writable: true, // We will reduce its balance. - }, - // Owner of `from_account` SPL token account. - // Must sign the transaction in order to move tokens. - pub user_authority { - is_signer: true, - is_writable: false, - }, - pub to_reserve_account { - is_signer: false, - is_writable: true, // Needs to be writable to update the account's state. - }, - // User account that will receive the bSOL tokens, needs to be writable - // to update the account's state. - pub b_sol_user_account { - is_signer: false, - is_writable: true, - }, - pub b_sol_mint { - is_signer: false, - is_writable: true, // Minting changes the supply, which is stored in the mint. - }, - pub b_sol_mint_authority { - is_signer: false, - is_writable: false, - }, - const spl_token = spl_token::id(), - } -} - -pub fn deposit( - program_id: &Pubkey, - accounts: &DepositAccountsMeta, - amount: StLamports, -) -> Instruction { - let data = AnkerInstruction::Deposit { amount }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - WithdrawAccountsMeta, WithdrawAccountsInfo { - pub anker { - is_signer: false, - is_writable: true, // Needed to update metrics. - }, - // For reading the stSOL/SOL exchange rate. - pub solido { - is_signer: false, - is_writable: false, - }, - // SPL token account that holds the bSOL to return. - pub from_b_sol_account { - is_signer: false, - is_writable: true, // We will decrease its balance. - }, - // Owner of `from_b_sol_account` SPL token account. - // Must sign the transaction in order to move tokens. - pub from_b_sol_authority { - is_signer: true, - is_writable: false, - }, - // Recipient of the proceeds, must be an SPL token account that holds stSOL. - pub to_st_sol_account { - is_signer: false, - is_writable: true, // We will increase its balance. - }, - // Anker's reserve, where the stSOL move out of. - pub reserve_account { - is_signer: false, - is_writable: true, // We will decrease its balance. - }, - // Owner of Anker's reserve, a program-derived address. - pub reserve_authority { - is_signer: false, - is_writable: false, - }, - pub b_sol_mint { - is_signer: false, - is_writable: true, // Burning bSOL changes the supply, which is stored in the mint. - }, - const spl_token = spl_token::id(), - } -} - -pub fn withdraw( - program_id: &Pubkey, - accounts: &WithdrawAccountsMeta, - amount: BLamports, -) -> Instruction { - let data = AnkerInstruction::Withdraw { amount }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - FetchPoolPriceAccountsMeta, FetchPoolPriceAccountsInfo { - pub anker { - is_signer: false, - // Writable because we store the price in the instance. - is_writable: true, - }, - pub solido { - is_signer: false, - is_writable: false, - }, - - // Accounts for token swap. - pub token_swap_pool { - is_signer: false, - is_writable: false, - }, - pub pool_st_sol_account { - is_signer: false, - is_writable: false, - }, - pub pool_ust_account { - is_signer: false, - is_writable: false, - }, - - const sysvar_clock = sysvar::clock::id(), - } -} - -pub fn fetch_pool_price(program_id: &Pubkey, accounts: &FetchPoolPriceAccountsMeta) -> Instruction { - let data = AnkerInstruction::FetchPoolPrice; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - SellRewardsAccountsMeta, SellRewardsAccountsInfo { - pub anker { - is_signer: false, - is_writable: true, // Needed to update metrics. - }, - pub solido { - is_signer: false, - is_writable: false, - }, - // Needs to be writable so we can sell stSOL. - pub st_sol_reserve_account { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub b_sol_mint { - is_signer: false, - is_writable: false, - }, - - // Accounts for token swap - pub token_swap_pool { - is_signer: false, - is_writable: false, - }, - pub pool_st_sol_account { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub pool_ust_account { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub ust_reserve_account { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub pool_mint { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub st_sol_mint { - is_signer: false, - is_writable: false, - }, - pub ust_mint { - is_signer: false, - is_writable: false, - }, - pub pool_fee_account { - is_signer: false, - is_writable: true, // Needed to swap tokens. - }, - pub token_swap_authority { - is_signer: false, - is_writable: false, - }, - pub reserve_authority { - is_signer: false, - is_writable: false, - }, - pub token_swap_program_id { - is_signer: false, - is_writable: false, - }, - const spl_token = spl_token::id(), - const sysvar_clock = sysvar::clock::id(), - } -} - -pub fn sell_rewards(program_id: &Pubkey, accounts: &SellRewardsAccountsMeta) -> Instruction { - let data = AnkerInstruction::SellRewards; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - ChangeTerraRewardsDestinationAccountsMeta, ChangeTerraRewardsDestinationAccountsInfo { - // Needs to be writtable in order to save new Terra address. - pub anker { - is_signer: false, - is_writable: true, - }, - pub solido { - is_signer: false, - is_writable: false, - }, - pub manager { - is_signer: true, - is_writable: false, - }, - } -} - -pub fn change_terra_rewards_destination( - program_id: &Pubkey, - accounts: &ChangeTerraRewardsDestinationAccountsMeta, - terra_rewards_destination: TerraAddress, -) -> Instruction { - let data = AnkerInstruction::ChangeTerraRewardsDestination { - terra_rewards_destination, - }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - ChangeTokenSwapPoolAccountsMeta, ChangeTokenSwapPoolAccountsInfo { - // Needs to be writable in order to save new Token Pool address. - pub anker { - is_signer: false, - is_writable: true, - }, - pub solido { - is_signer: false, - is_writable: false, - }, - pub manager { - is_signer: true, - is_writable: false, - }, - pub current_token_swap_pool { - is_signer: false, - is_writable: false, - }, - pub new_token_swap_pool { - is_signer: false, - is_writable: false, - }, - } -} - -pub fn change_token_swap_pool( - program_id: &Pubkey, - accounts: &ChangeTokenSwapPoolAccountsMeta, -) -> Instruction { - let data = AnkerInstruction::ChangeTokenSwapPool; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - ChangeSellRewardsMinOutBpsAccountsMeta, ChangeSellRewardsMinOutBpsAccountsInfo { - // Needs to be writable in order to save new `sell_rewards_min_out_bps`. - pub anker { - is_signer: false, - is_writable: true, - }, - pub solido { - is_signer: false, - is_writable: false, - }, - pub manager { - is_signer: true, - is_writable: false, - }, - } -} - -pub fn change_sell_rewards_min_out_bps( - program_id: &Pubkey, - accounts: &ChangeSellRewardsMinOutBpsAccountsMeta, - sell_rewards_min_out_bps: u64, -) -> Instruction { - let data = AnkerInstruction::ChangeSellRewardsMinOutBps { - sell_rewards_min_out_bps, - }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} - -accounts_struct! { - // For the Wormhole accounts, see also - // https://github.com/certusone/wormhole/blob/537d56b37aa041a585f2c90515fa3a7ffa5898b5/solana/modules/token_bridge/program/src/instructions.rs#L328-L390. - SendRewardsAccountsMeta, SendRewardsAccountsInfo { - pub anker { - is_signer: false, - is_writable: false, - }, - pub solido { - is_signer: false, - is_writable: false, - }, - pub reserve_authority { - is_signer: false, - is_writable: false, - }, - // Accounts for Wormhole swap. - pub wormhole_token_bridge_program_id { - is_signer: false, - is_writable: false, - }, - pub wormhole_core_bridge_program_id { - is_signer: false, - is_writable: false, - }, - pub payer { - is_signer: true, - is_writable: true, - }, - pub config_key { - is_signer: false, - is_writable: false, - }, - pub ust_reserve_account { - is_signer: false, - is_writable: true, - }, - pub ust_mint { - is_signer: false, - is_writable: true, - }, - // Program-derived address derived from the mint address. - pub wrapped_meta_key { - is_signer: false, - is_writable: true, - }, - // Wormhole program-derived account that will sign the SPL - // token transfer out of the source account. This means we will need - // to call spl_token::approve before we can send. - pub authority_signer_key { - is_signer: false, - is_writable: false, - }, - pub bridge_config { - is_signer: false, - is_writable: true, - }, - // The message account needs to be a new, uninitialized account, and then - // calling Wormhole will initialize it. (This is why it needs to be a - // signer.) - pub message { - is_signer: true, - is_writable: true, - }, - pub emitter_key { - is_signer: false, - is_writable: false, - }, - pub sequence_key { - is_signer: false, - is_writable: true, - }, - // To make a Wormhole transfer, we need to pay a transaction fee (on top - // of the Solana transaction fee). The Wormhole program enforces this by - // transferring some SOL from the payer account to the fee collector. - pub fee_collector_key { - is_signer: false, - is_writable: true, - }, - const sysvar_clock = sysvar::clock::id(), - const sysvar_rent = sysvar::rent::id(), - const system_program = system_program::id(), - const spl_token = spl_token::id(), - } -} - -pub fn send_rewards( - program_id: &Pubkey, - accounts: &SendRewardsAccountsMeta, - wormhole_nonce: u32, -) -> Instruction { - let data = AnkerInstruction::SendRewards { wormhole_nonce }; - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: data.to_vec(), - } -} diff --git a/anker/src/lib.rs b/anker/src/lib.rs deleted file mode 100644 index c3c3576ff..000000000 --- a/anker/src/lib.rs +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use solana_program::pubkey::Pubkey; - -#[cfg(not(feature = "no-entrypoint"))] -pub mod entrypoint; -pub mod error; -pub mod instruction; -pub mod logic; -pub mod metrics; -pub mod processor; -pub mod state; -pub mod token; -pub mod wormhole; - -/// Mint authority, mints bSOL. -pub const ANKER_MINT_AUTHORITY: &[u8] = b"mint_authority"; - -/// Anker's authority that will control the reserve account. -pub const ANKER_RESERVE_AUTHORITY: &[u8] = b"reserve_authority"; - -/// Anker's reserve account. Holds StSOL. -pub const ANKER_STSOL_RESERVE_ACCOUNT: &[u8] = b"st_sol_reserve_account"; - -/// Anker's UST reserve account. Holds UST. -pub const ANKER_UST_RESERVE_ACCOUNT: &[u8] = b"ust_reserve_account"; - -/// Address of Orca.so's mainnet deployment of their token swap program. -pub mod orca_token_swap_v2 { - use solana_program::declare_id; - // The Solana macro generates a function that returns the pubkey, always - // named "id", not a constant that we can assign to a const Pubkey, so we - // have to put it in a mod instead :/ - declare_id!("9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP"); -} - -/// A different address that will also contain a deployment of the token swap program. -/// -/// This is used to test that attackers who upload their own (modified) copy of -/// the token swap program would not be able to call it. -pub mod orca_token_swap_v2_fake { - use solana_program::declare_id; - declare_id!("hackRN3Era2mH2ByBLzGo1EqiGEXFTCAnrJTNxhzU6i"); -} - -/// Return the address at which the Anker instance should live that belongs to -/// the given Solido instance. -pub fn find_instance_address(anker_program_id: &Pubkey, solido_instance: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[solido_instance.as_ref()], anker_program_id) -} - -/// Return the owner of the stSOL and UST reserve account, and bump seed. -pub fn find_reserve_authority(anker_program_id: &Pubkey, anker_instance: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[anker_instance.as_ref(), ANKER_RESERVE_AUTHORITY], - anker_program_id, - ) -} - -/// Return the address of the stSOL reserve account, and bump seed. -pub fn find_st_sol_reserve_account( - anker_program_id: &Pubkey, - anker_instance: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[anker_instance.as_ref(), ANKER_STSOL_RESERVE_ACCOUNT], - anker_program_id, - ) -} - -/// Return the mint authority for bSOL, and bump seed. -pub fn find_mint_authority(anker_program_id: &Pubkey, anker_instance: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[anker_instance.as_ref(), ANKER_MINT_AUTHORITY], - anker_program_id, - ) -} - -/// Return the UST reserve account, has the same authority as the stSOL account. -pub fn find_ust_reserve_account( - anker_program_id: &Pubkey, - anker_instance: &Pubkey, -) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[anker_instance.as_ref(), ANKER_UST_RESERVE_ACCOUNT], - anker_program_id, - ) -} diff --git a/anker/src/logic.rs b/anker/src/logic.rs deleted file mode 100644 index 1a4c4c61a..000000000 --- a/anker/src/logic.rs +++ /dev/null @@ -1,350 +0,0 @@ -use crate::{ - error::AnkerError, - token::{BLamports, MicroUst}, - ANKER_MINT_AUTHORITY, ANKER_RESERVE_AUTHORITY, -}; -use lido::{ - state::Lido, - token::{ArithmeticError, Lamports, StLamports}, -}; -use solana_program::{ - account_info::AccountInfo, - borsh::try_from_slice_unchecked, - entrypoint::ProgramResult, - msg, - program::{invoke, invoke_signed}, - program_error::ProgramError, - pubkey::Pubkey, - rent::Rent, - system_instruction, -}; -use spl_token_swap::{ - curve::calculator::{CurveCalculator, TradeDirection}, - instruction::Swap, -}; -use std::convert::TryFrom; - -use crate::{ - instruction::{DepositAccountsInfo, InitializeAccountsInfo, SellRewardsAccountsInfo}, - state::Anker, -}; - -/// Deserialize the Solido and Anker state. -/// -/// Check the following things for consistency: -/// * The Solido state should be owned by the Solido program stored in Anker. -/// * The Solido state should live at the address stored in Anker. -/// * The reserve should live at the address derived from Anker. -/// * The reserve should be valid stSOL account. -/// -/// The following things are not checked, because these accounts are not always -/// needed: -/// * The mint address should match the address stored in Anker. -/// * The mint authority should match the address derived from Anker. -/// * The StSOL/UST reserve address should match the address derived from Anker. -/// * The reserve authority should match the address derived from Anker. -/// -/// Note, the address of the Anker instance is a program-derived address that -/// derived from the Anker program address, and the Solido instance address of -/// the Solido instance that this Anker instance belongs to. This ensures that -/// for a given deployment of the Anker program, there exists a unique Anker -/// instance address per Solido instance. -pub fn deserialize_anker( - anker_program_id: &Pubkey, - anker_account: &AccountInfo, - solido_account: &AccountInfo, -) -> Result<(Lido, Anker), ProgramError> { - if anker_account.owner != anker_program_id { - msg!( - "Anker state is owned by {}, but should be owned by the Anker program ({}).", - anker_account.owner, - anker_program_id - ); - return Err(AnkerError::InvalidOwner.into()); - } - - let anker = try_from_slice_unchecked::(&anker_account.data.borrow())?; - - anker.check_self_address(anker_program_id, anker_account)?; - - if *solido_account.owner != anker.solido_program_id { - msg!( - "Anker state is associated with Solido program at {}, but Solido state is owned by {}.", - anker.solido_program_id, - solido_account.owner, - ); - return Err(AnkerError::InvalidOwner.into()); - } - - if *solido_account.key != anker.solido { - msg!( - "Anker state is associated with Solido instance at {}, but found {}.", - anker.solido, - solido_account.owner, - ); - return Err(AnkerError::InvalidSolidoInstance.into()); - } - - let solido = Lido::deserialize_lido(&anker.solido_program_id, solido_account)?; - - Ok((solido, anker)) -} - -/// Mint the given amount of bSOL and put it in the recipient's account. -pub fn mint_b_sol_to( - anker_program_id: &Pubkey, - anker: &Anker, - accounts: &DepositAccountsInfo, - amount: BLamports, -) -> ProgramResult { - // Check if the mint account is the same as the one stored in Anker. - anker.check_mint(accounts.b_sol_mint)?; - anker.check_mint_authority( - anker_program_id, - accounts.anker.key, - accounts.b_sol_mint_authority, - )?; - - anker.check_is_b_sol_account(accounts.b_sol_user_account)?; - - let authority_signature_seeds = [ - &accounts.anker.key.to_bytes(), - ANKER_MINT_AUTHORITY, - &[anker.mint_authority_bump_seed], - ]; - let signers = [&authority_signature_seeds[..]]; - - // The SPL token program supports multisig-managed mints, but we do not - // use those. - let mint_to_signers = []; - let instruction = spl_token::instruction::mint_to( - accounts.spl_token.key, - accounts.b_sol_mint.key, - accounts.b_sol_user_account.key, - accounts.b_sol_mint_authority.key, - &mint_to_signers, - amount.0, - )?; - - invoke_signed( - &instruction, - &[ - accounts.b_sol_mint.clone(), - accounts.b_sol_user_account.clone(), - accounts.b_sol_mint_authority.clone(), - accounts.spl_token.clone(), - ], - &signers, - ) -} - -/// Burn -pub fn burn_b_sol<'a>( - anker: &Anker, - spl_token_program: &AccountInfo<'a>, - b_sol_mint: &AccountInfo<'a>, - burn_from: &AccountInfo<'a>, - burn_from_authority: &AccountInfo<'a>, - amount: BLamports, -) -> ProgramResult { - anker.check_mint(b_sol_mint)?; - anker.check_is_b_sol_account(burn_from)?; - - // The SPL token program supports multisig-managed mints, but we do not use those. - let burn_signers = []; - let instruction = spl_token::instruction::burn( - spl_token_program.key, - burn_from.key, - b_sol_mint.key, - burn_from_authority.key, - &burn_signers, - amount.0, - )?; - - invoke( - &instruction, - &[ - burn_from.clone(), - b_sol_mint.clone(), - burn_from_authority.clone(), - spl_token_program.clone(), - ], - ) -} - -pub fn create_account<'a, 'b>( - owner: &Pubkey, - accounts: &InitializeAccountsInfo<'a, 'b>, - new_account: &'a AccountInfo<'b>, - sysvar_rent: &Rent, - data_len: usize, - seeds: &[&[u8]], -) -> ProgramResult { - let rent_lamports = sysvar_rent.minimum_balance(data_len); - let instr_create = system_instruction::create_account( - accounts.fund_rent_from.key, - new_account.key, - rent_lamports, - data_len as u64, - owner, - ); - msg!( - "Creating account at {}, funded with {} from {}.", - new_account.key, - Lamports(rent_lamports), - accounts.fund_rent_from.key, - ); - invoke_signed( - &instr_create, - &[ - accounts.fund_rent_from.clone(), - new_account.clone(), - accounts.system_program.clone(), - ], - &[seeds], - ) -} - -/// Initialize an SPL account with the owner set as the reserve authority. -pub fn initialize_spl_account<'a, 'b>( - accounts: &InitializeAccountsInfo<'a, 'b>, - seeds: &[&[u8]], - account: &'a AccountInfo<'b>, - mint: &'a AccountInfo<'b>, -) -> ProgramResult { - // Initialize the reserve account. - invoke_signed( - &spl_token::instruction::initialize_account( - &spl_token::id(), - account.key, - mint.key, - accounts.reserve_authority.key, - )?, - &[ - account.clone(), - mint.clone(), - accounts.reserve_authority.clone(), - accounts.sysvar_rent.clone(), - ], - &[seeds], - ) -} - -/// Swap the `amount` from StSOL to UST -/// -/// Sends the UST to the `accounts.ust_reserve` -pub fn swap_rewards( - program_id: &Pubkey, - amount: StLamports, - anker: &Anker, - accounts: &SellRewardsAccountsInfo, - minimum_ust_out: MicroUst, -) -> ProgramResult { - if amount == StLamports(0) { - msg!("Anker rewards must be greater than zero to be claimable."); - return Err(AnkerError::ZeroRewardsToClaim.into()); - } - anker.check_token_swap_before_sell(program_id, accounts)?; - - let swap_instruction = spl_token_swap::instruction::swap( - accounts.token_swap_program_id.key, - accounts.spl_token.key, - accounts.token_swap_pool.key, - accounts.token_swap_authority.key, - accounts.reserve_authority.key, - accounts.st_sol_reserve_account.key, - accounts.pool_st_sol_account.key, - accounts.pool_ust_account.key, - accounts.ust_reserve_account.key, - accounts.pool_mint.key, - accounts.pool_fee_account.key, - None, - Swap { - amount_in: amount.0, - minimum_amount_out: minimum_ust_out.0, - }, - )?; - - let authority_signature_seeds = [ - &accounts.anker.key.to_bytes(), - ANKER_RESERVE_AUTHORITY, - &[anker.reserve_authority_bump_seed], - ]; - let signers = [&authority_signature_seeds[..]]; - - invoke_signed( - &swap_instruction, - &[ - accounts.token_swap_pool.clone(), - accounts.token_swap_authority.clone(), - accounts.reserve_authority.clone(), - accounts.st_sol_reserve_account.clone(), - accounts.pool_st_sol_account.clone(), - accounts.pool_ust_account.clone(), - accounts.ust_reserve_account.clone(), - accounts.pool_mint.clone(), - accounts.pool_fee_account.clone(), - accounts.spl_token.clone(), - accounts.token_swap_program_id.clone(), - ], - &signers, - ) -} - -/// Get the price for selling 1 stSOL in MicroUst in the token swap pool. -pub fn get_one_st_sol_for_ust_price_from_pool( - curve_calculator: &dyn CurveCalculator, - swap_pool_token_a: &Pubkey, - pool_ust_address: &Pubkey, - pool_st_sol_balance: StLamports, - pool_ust_balance: MicroUst, -) -> Result { - // To sample the price, we go from stSOL to UST. - let trade_direction = if swap_pool_token_a == pool_ust_address { - TradeDirection::BtoA - } else { - TradeDirection::AtoB - }; - - // Check how much UST we get out, if we put in 1 stSOL. With a constant-product - // pool, the amount we get out depends not only on the state of the pool, but - // also on the amount we put in. We pick 1 stSOL here because it should be - // large enough that we don't lose precision in the output, but small enough - // to not move the price by a lot if we did swap that amount. - let one_st_sol = StLamports(1_000_000_000); - let swap_result = curve_calculator - .swap_without_fees( - one_st_sol.0 as u128, - pool_st_sol_balance.0 as u128, - pool_ust_balance.0 as u128, - trade_direction, - ) - .ok_or(AnkerError::PoolPriceUndefined)?; - Ok(MicroUst( - u64::try_from(swap_result.destination_amount_swapped).map_err(|_| ArithmeticError)?, - )) -} - -#[cfg(test)] -mod test { - use super::*; - use spl_token_swap::curve::constant_product::ConstantProductCurve; - - #[test] - fn test_less_than_one_st_sol_for_ust() { - // Previously, we had one assert that stated we sold exactly one stSOL, - // sometimes due to precision errors this assertion might fail. We - // removed it and put this test that sells `Lamports(999_999_998)`. - let curve = ConstantProductCurve::default(); - let swap_pool_token_a = Pubkey::new_unique(); - let pool_ust_address = Pubkey::new_unique(); - let result = get_one_st_sol_for_ust_price_from_pool( - &curve, - &swap_pool_token_a, - &pool_ust_address, - StLamports(500_000_000), - MicroUst(1_000_000_000), - ); - assert_eq!(result, Ok(MicroUst(666_666_666))); - } -} diff --git a/anker/src/metrics.rs b/anker/src/metrics.rs deleted file mode 100644 index e417a035c..000000000 --- a/anker/src/metrics.rs +++ /dev/null @@ -1,91 +0,0 @@ -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use lido::token::StLamports; -use serde::Serialize; - -use crate::token::{self, BLamports, MicroUst}; - -#[repr(C)] -#[derive( - Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct Metrics { - /// Total swapped amount of StSOL to UST. - #[serde(rename = "swaped_rewards_total_st_lamports")] - pub swapped_rewards_st_sol_total: StLamports, - - /// Total amount of UST received through swaps. - #[serde(rename = "swapped_rewards_ust_total_microust")] - pub swapped_rewards_ust_total: MicroUst, - - /// Metric for deposits. - pub deposit_metric: DepositWithdrawMetric, - - /// Metrics for withdrawals. - pub withdraw_metric: DepositWithdrawMetric, -} - -#[repr(C)] -#[derive( - Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct DepositWithdrawMetric { - /// Total amount of StSOL. - pub st_sol_total: StLamports, - - /// Total amount of bSol. - pub b_sol_total: BLamports, - - /// Total number of times the metric was called. - pub count: u64, -} - -impl Metrics { - pub fn new() -> Self { - let empty_metric = DepositWithdrawMetric { - st_sol_total: StLamports(0), - b_sol_total: BLamports(0), - count: 0, - }; - Metrics { - swapped_rewards_st_sol_total: StLamports(0), - swapped_rewards_ust_total: MicroUst(0), - deposit_metric: empty_metric.clone(), - withdraw_metric: empty_metric, - } - } - - pub fn observe_token_swap( - &mut self, - st_sol_amount: StLamports, - ust_amount: MicroUst, - ) -> token::Result<()> { - self.swapped_rewards_st_sol_total = (self.swapped_rewards_st_sol_total + st_sol_amount)?; - self.swapped_rewards_ust_total = (self.swapped_rewards_ust_total + ust_amount)?; - - Ok(()) - } - - pub fn observe_deposit( - &mut self, - st_sol_amount: StLamports, - b_sol_amount: BLamports, - ) -> token::Result<()> { - self.deposit_metric.st_sol_total = (self.deposit_metric.st_sol_total + st_sol_amount)?; - self.deposit_metric.b_sol_total = (self.deposit_metric.b_sol_total + b_sol_amount)?; - self.deposit_metric.count += 1; - - Ok(()) - } - - pub fn observe_withdraw( - &mut self, - st_sol_amount: StLamports, - b_sol_amount: BLamports, - ) -> token::Result<()> { - self.withdraw_metric.st_sol_total = (self.withdraw_metric.st_sol_total + st_sol_amount)?; - self.withdraw_metric.b_sol_total = (self.withdraw_metric.b_sol_total + b_sol_amount)?; - self.withdraw_metric.count += 1; - - Ok(()) - } -} diff --git a/anker/src/processor.rs b/anker/src/processor.rs deleted file mode 100644 index fa278090e..000000000 --- a/anker/src/processor.rs +++ /dev/null @@ -1,696 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use borsh::BorshDeserialize; -use lido::token::Lamports; -use solana_program::{ - account_info::AccountInfo, - clock::Clock, - entrypoint::ProgramResult, - msg, - program::{invoke, invoke_signed}, - program_error::ProgramError, - program_option::COption, - program_pack::Pack, - pubkey::Pubkey, - rent::Rent, - sysvar::Sysvar, -}; - -use lido::{state::Lido, token::StLamports}; - -use crate::{ - error::AnkerError, - find_instance_address, find_mint_authority, find_reserve_authority, - find_st_sol_reserve_account, - instruction::{ - AnkerInstruction, ChangeTerraRewardsDestinationAccountsInfo, - ChangeTokenSwapPoolAccountsInfo, DepositAccountsInfo, FetchPoolPriceAccountsInfo, - InitializeAccountsInfo, SellRewardsAccountsInfo, SendRewardsAccountsInfo, - WithdrawAccountsInfo, - }, - logic::{burn_b_sol, deserialize_anker, mint_b_sol_to}, - metrics::Metrics, - state::{Anker, WormholeParameters, ANKER_VERSION}, - token::{BLamports, MicroUst}, - wormhole::{get_wormhole_transfer_instruction, TerraAddress}, -}; -use crate::{find_ust_reserve_account, ANKER_STSOL_RESERVE_ACCOUNT, ANKER_UST_RESERVE_ACCOUNT}; -use crate::{ - instruction::ChangeSellRewardsMinOutBpsAccountsInfo, - logic::get_one_st_sol_for_ust_price_from_pool, - state::{HistoricalStSolPriceArray, POOL_PRICE_MAX_SAMPLE_AGE, POOL_PRICE_MIN_SAMPLE_DISTANCE}, -}; -use crate::{ - logic::{create_account, initialize_spl_account, swap_rewards}, - state::ExchangeRate, -}; -use crate::{state::ANKER_LEN, ANKER_RESERVE_AUTHORITY}; - -#[inline(never)] -fn process_initialize( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - terra_rewards_destination: TerraAddress, - sell_rewards_min_out_bps: u64, -) -> ProgramResult { - let accounts = InitializeAccountsInfo::try_from_slice(accounts_raw)?; - let rent = Rent::from_account_info(accounts.sysvar_rent)?; - - let (anker_address, anker_bump_seed) = find_instance_address(program_id, accounts.solido.key); - - if anker_address != *accounts.anker.key { - msg!( - "Expected to initialize instance at {}, but {} was provided.", - anker_address, - accounts.anker.key, - ); - return Err(AnkerError::InvalidDerivedAccount.into()); - } - if sell_rewards_min_out_bps > 10_000 { - return Err(AnkerError::InvalidSellRewardsMinOutBps.into()); - } - - let solido = Lido::deserialize_lido(accounts.solido_program.key, accounts.solido)?; - - // We generate these addresses here, and then at the end after constructing - // the Anker instance, we check that these addresses match the provided ones. - // This way we can re-use the existing checks. - let (mint_authority, mint_bump_seed) = find_mint_authority(program_id, &anker_address); - let (_reserve_authority, reserve_authority_bump_seed) = - find_reserve_authority(program_id, &anker_address); - let (_reserve_account, st_sol_reserve_account_bump_seed) = - find_st_sol_reserve_account(program_id, &anker_address); - let (_ust_reserve_account, ust_reserve_account_bump_seed) = - find_ust_reserve_account(program_id, &anker_address); - - // Create an account for the Anker instance. - let anker_seeds = [accounts.solido.key.as_ref(), &[anker_bump_seed]]; - create_account( - program_id, - &accounts, - accounts.anker, - &rent, - // At the time of writing, Solana accounts cannot be resized. If we ever - // need to store more data in the future, we need to create the headroom - // for it now (or switch to a different account later). So add 128 bytes - // of headroom for future expansion, in case we need it. - ANKER_LEN + 128, - &anker_seeds, - )?; - - // Create and initialize an stSOL SPL token account for the reserve. - let st_sol_reserve_account_seeds = [ - anker_address.as_ref(), - ANKER_STSOL_RESERVE_ACCOUNT, - &[st_sol_reserve_account_bump_seed], - ]; - msg!("Allocating account for stSOL reserve ..."); - create_account( - &spl_token::ID, - &accounts, - accounts.st_sol_reserve_account, - &rent, - spl_token::state::Account::LEN, - &st_sol_reserve_account_seeds, - )?; - msg!("Initializing SPL token account for stSOL reserve ..."); - initialize_spl_account( - &accounts, - &st_sol_reserve_account_seeds, - accounts.st_sol_reserve_account, - accounts.st_sol_mint, - )?; - - // Create and initialize an UST SPL token account for the reserve - let ust_reserve_account_seeds = [ - anker_address.as_ref(), - ANKER_UST_RESERVE_ACCOUNT, - &[ust_reserve_account_bump_seed], - ]; - msg!("Allocating account for UST reserve ..."); - create_account( - &spl_token::ID, - &accounts, - accounts.ust_reserve_account, - &rent, - spl_token::state::Account::LEN, - &ust_reserve_account_seeds, - )?; - msg!("Initializing SPL token account for UST reserve ..."); - initialize_spl_account( - &accounts, - &ust_reserve_account_seeds, - accounts.ust_reserve_account, - accounts.ust_mint, - )?; - - let anker = Anker { - version: ANKER_VERSION, - b_sol_mint: *accounts.b_sol_mint.key, - solido_program_id: *accounts.solido_program.key, - solido: *accounts.solido.key, - token_swap_pool: *accounts.token_swap_pool.key, - terra_rewards_destination, - wormhole_parameters: WormholeParameters { - core_bridge_program_id: *accounts.wormhole_core_bridge_program_id.key, - token_bridge_program_id: *accounts.wormhole_token_bridge_program_id.key, - }, - sell_rewards_min_out_bps, - metrics: Metrics::new(), - // At initialization, we fill the historical prices with a dummy - // price of 1 UST per stSOL recorded at slot 0. Because we require - // these prices to be recent at `SellRewards` time, these dummy - // values are never used. - historical_st_sol_prices: HistoricalStSolPriceArray::new(), - self_bump_seed: anker_bump_seed, - mint_authority_bump_seed: mint_bump_seed, - reserve_authority_bump_seed, - st_sol_reserve_account_bump_seed, - ust_reserve_account_bump_seed, - }; - - anker.check_mint(accounts.b_sol_mint)?; - anker.check_st_sol_reserve_address( - program_id, - &anker_address, - accounts.st_sol_reserve_account, - )?; - anker.check_ust_reserve_address(program_id, &anker_address, accounts.ust_reserve_account)?; - anker.check_reserve_authority(program_id, &anker_address, accounts.reserve_authority)?; - anker.check_is_st_sol_account(&solido, accounts.st_sol_reserve_account)?; - - match spl_token::state::Mint::unpack_from_slice(&accounts.b_sol_mint.data.borrow()) { - Ok(mint) if mint.mint_authority == COption::Some(mint_authority) => { - // Ok, we control this mint. - } - _ => { - msg!( - "Mint authority of bSOL mint {} is not the expected {}.", - accounts.b_sol_mint.key, - mint_authority, - ); - return Err(AnkerError::InvalidTokenMint.into()); - } - } - - anker.save(accounts.anker) -} - -/// Deposit an amount of StLamports and get bSol in return. -#[inline(never)] -fn process_deposit( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - amount: StLamports, -) -> ProgramResult { - let accounts = DepositAccountsInfo::try_from_slice(accounts_raw)?; - - if amount == StLamports(0) { - msg!("Amount must be greater than zero"); - return Err(ProgramError::InvalidArgument); - } - - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - anker.check_st_sol_reserve_address( - program_id, - accounts.anker.key, - accounts.to_reserve_account, - )?; - anker.check_is_st_sol_account(&solido, accounts.to_reserve_account)?; - - // Transfer `amount` StLamports to the reserve. - invoke( - &spl_token::instruction::transfer( - &spl_token::id(), - accounts.from_account.key, - accounts.to_reserve_account.key, - accounts.user_authority.key, - &[], - amount.0, - )?, - &[ - accounts.from_account.clone(), - accounts.to_reserve_account.clone(), - accounts.user_authority.clone(), - accounts.spl_token.clone(), - ], - )?; - - // Use Lido's exchange rate (`sol_balance / sol_supply`) to compute the - // amount of BLamports to mint. - let exchange_rate = ExchangeRate::from_solido_pegged(&solido); - let b_sol_amount = exchange_rate.exchange_st_sol(amount)?; - - mint_b_sol_to(program_id, &anker, &accounts, b_sol_amount)?; - - msg!( - "Anker: Deposited {}, minted {} in return.", - amount, - b_sol_amount, - ); - anker.metrics.observe_deposit(amount, b_sol_amount)?; - - anker.save(accounts.anker) -} - -/// Sample the current pool price, used later to limit slippage in `sell_rewards`. -#[inline(never)] -fn process_fetch_pool_price(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { - let accounts = FetchPoolPriceAccountsInfo::try_from_slice(accounts_raw)?; - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - - // Check that the accounts passed to this instruction are the same as those - // stored in the pool. That alone would still enable swapping the stSOL and - // UST accounts though, so also confirm the stSOL mint on one. - anker.check_token_swap_before_fetch_price(&accounts)?; - anker.check_is_st_sol_account(&solido, accounts.pool_st_sol_account)?; - - let token_swap_program_id = accounts.token_swap_pool.owner; - let swap_pool = - anker.get_token_swap_instance(accounts.token_swap_pool, token_swap_program_id)?; - let pool_ust_balance = MicroUst(Anker::get_token_amount(accounts.pool_ust_account)?); - let pool_st_sol_balance = StLamports(Anker::get_token_amount(accounts.pool_st_sol_account)?); - - let clock = Clock::from_account_info(accounts.sysvar_clock)?; - - // The price samples must be spaced at least some distance apart. - let most_recent_sample = anker.historical_st_sol_prices.last(); - let slots_elapsed = clock.slot.saturating_sub(most_recent_sample.slot); - if slots_elapsed < POOL_PRICE_MIN_SAMPLE_DISTANCE { - msg!( - "The previous stSOL/UST price was sampled at slot {}. \ - A new sample cannot be added until slot {}.", - most_recent_sample.slot, - most_recent_sample.slot + POOL_PRICE_MIN_SAMPLE_DISTANCE, - ); - return Err(AnkerError::FetchPoolPriceTooEarly.into()); - } - - let st_sol_price_in_ust = get_one_st_sol_for_ust_price_from_pool( - &*swap_pool.swap_curve.calculator, - &swap_pool.token_a, - accounts.pool_ust_account.key, - pool_st_sol_balance, - pool_ust_balance, - )?; - - anker - .historical_st_sol_prices - .insert_and_rotate(clock.slot, st_sol_price_in_ust); - anker.save(accounts.anker) -} - -/// Sell Anker rewards. -#[inline(never)] -fn process_sell_rewards(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { - let accounts = SellRewardsAccountsInfo::try_from_slice(accounts_raw)?; - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - anker.check_st_sol_reserve_address( - program_id, - accounts.anker.key, - accounts.st_sol_reserve_account, - )?; - - let clock = Clock::from_account_info(accounts.sysvar_clock)?; - let oldest_sample = anker.historical_st_sol_prices.first(); - let slots_elapsed = clock.slot.saturating_sub(oldest_sample.slot); - if slots_elapsed > POOL_PRICE_MAX_SAMPLE_AGE { - msg!( - "The oldest stSOL/UST price was sampled at slot {}. \ - It must have been sampled more recently.", - oldest_sample.slot, - ); - return Err(AnkerError::FetchPoolPriceNotCalledRecently.into()); - } - - // The youngest sample must not be too recent, so an adversarial cranker can - // not sandwich the `FetchPoolPrice` and `SellRewards` in the same transaction. - // But if we demand the same distance between the sale and fetching the price, - // as between price updates, then one could spam `FetchPoolPrice` transactions - // and hold off the `SellRewards` for a bit. To avoid this, we allow the - // `SellRewards` to happen earlier than the price fetch, but still late enough - // that no single validator should control that entire span of slots. - let youngest_sample = anker.historical_st_sol_prices.last(); - let slots_elapsed = clock.slot.saturating_sub(youngest_sample.slot); - if slots_elapsed < POOL_PRICE_MIN_SAMPLE_DISTANCE / 2 { - msg!( - "The youngest stSOL/UST price was sampled at slot {}. \ - Wait at least {} slots until selling the rewards..", - youngest_sample.slot, - POOL_PRICE_MIN_SAMPLE_DISTANCE / 2, - ); - return Err(AnkerError::SellRewardsTooEarly.into()); - } - - anker.check_is_st_sol_account(&solido, accounts.st_sol_reserve_account)?; - anker.check_mint(accounts.b_sol_mint)?; - - let token_mint_state = - spl_token::state::Mint::unpack_from_slice(&accounts.b_sol_mint.data.borrow())?; - let b_sol_supply = token_mint_state.supply; - - let reserve_st_sol_before = - StLamports(Anker::get_token_amount(accounts.st_sol_reserve_account)?); - - // Get StLamports corresponding to the amount of b_sol minted. - let b_sol_supply_value_in_st_sol = solido.exchange_rate.exchange_sol(Lamports(b_sol_supply))?; - - // If this underflows, something went wrong, and we abort the transaction. - let rewards = (reserve_st_sol_before - b_sol_supply_value_in_st_sol)?; - - // Get minimum amount we are willing to pay for the rewards in UST. - let minimum_ust_out = anker - .historical_st_sol_prices - .minimum_ust_swap_amount(rewards, anker.sell_rewards_min_out_bps)?; - - // Get the amount of UST that we had. - let ust_before = MicroUst(Anker::get_token_amount(accounts.ust_reserve_account)?); - swap_rewards(program_id, rewards, &anker, &accounts, minimum_ust_out)?; - // Get new UST amount. - let ust_after = MicroUst(Anker::get_token_amount(accounts.ust_reserve_account)?); - let reserve_st_sol_after = - StLamports(Anker::get_token_amount(accounts.st_sol_reserve_account)?); - let swapped_ust = (ust_after - ust_before)?; - let swapped_st_sol = (reserve_st_sol_before - reserve_st_sol_after)?; - - // The token swap program should not take more stSOL than we told it to swap. - // As an extra line of defense, confirm this after the swap is done, and abort - // if some stSOL went missing. - if swapped_st_sol > rewards { - msg!( - "Called the token swap program to swap {}, but {} was removed from the reserve!", - rewards, - swapped_st_sol, - ); - return Err(AnkerError::TokenSwapAmountInvalid.into()); - } - - msg!("Swapped {} for {}.", swapped_st_sol, swapped_ust); - - anker - .metrics - .observe_token_swap(swapped_st_sol, swapped_ust)?; - anker.save(accounts.anker) -} - -/// Return some bSOL and get back the underlying stSOL. -#[inline(never)] -fn process_withdraw( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - amount: BLamports, -) -> ProgramResult { - let accounts = WithdrawAccountsInfo::try_from_slice(accounts_raw)?; - - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - anker.check_is_st_sol_account(&solido, accounts.reserve_account)?; - anker.check_mint(accounts.b_sol_mint)?; - - anker.check_mint(accounts.b_sol_mint)?; - anker.check_reserve_authority(program_id, accounts.anker.key, accounts.reserve_authority)?; - - let mint = match spl_token::state::Mint::unpack_from_slice(&accounts.b_sol_mint.data.borrow()) { - Ok(mint) => mint, - _ => { - msg!("Failed to read the bSOL mint."); - return Err(AnkerError::InvalidTokenMint.into()); - } - }; - - let reserve = - match spl_token::state::Account::unpack_from_slice(&accounts.reserve_account.data.borrow()) - { - Ok(reserve) => reserve, - _ => { - msg!("Failed to read the reserve stSOL account."); - return Err(AnkerError::InvalidReserveAccount.into()); - } - }; - - let b_sol_supply = BLamports(mint.supply); - let reserve_balance = StLamports(reserve.amount); - - // We have two ways of computing the exchange rate: - // - // 1. The inverse exchange rate of what Solido uses. - // 2. Based on the bSOL supply and stSOL reserve. - // - // Option 1 enforces a 1 bSOL = 1 SOL peg, but if for some reason the value - // of stSOL drops (which is impossible at the time of writing because there - // is no slashing on Solana, but Solana might introduce this in the future - // when we are in no position to upgrade this program quickly, so we want to - // be prepared), then there may not be enough stSOL in the reserve to cover - // all existing bSOL at a 1 bSOL = 1 SOL rate. This is where the Anker - // exchange rate comes in: we treat 1 bSOL as a share of 1/supply of the - // reserve. This ensures that all stSOL can be withdrawn, and it socializes - // the loss among withdrawers until the 1 bSOL = 1 SOL peg is restored. - let exchange_rate_solido = ExchangeRate::from_solido_pegged(&solido); - let exchange_rate_anker = ExchangeRate::from_anker_unpegged(b_sol_supply, reserve_balance); - let st_sol_solido = exchange_rate_solido.exchange_b_sol(amount)?; - let st_sol_anker = exchange_rate_anker.exchange_b_sol(amount)?; - let st_sol_amount = std::cmp::min(st_sol_solido, st_sol_anker); - - // Transfer the stSOL back to the user. - let reserve_seeds = [ - accounts.anker.key.as_ref(), - ANKER_RESERVE_AUTHORITY, - &[anker.reserve_authority_bump_seed], - ]; - invoke_signed( - &spl_token::instruction::transfer( - &spl_token::id(), - accounts.reserve_account.key, - accounts.to_st_sol_account.key, - accounts.reserve_authority.key, - &[], - st_sol_amount.0, - )?, - &[ - accounts.reserve_account.clone(), - accounts.to_st_sol_account.clone(), - accounts.reserve_authority.clone(), - accounts.spl_token.clone(), - ], - &[&reserve_seeds[..]], - )?; - - burn_b_sol( - &anker, - accounts.spl_token, - accounts.b_sol_mint, - accounts.from_b_sol_account, - accounts.from_b_sol_authority, - amount, - )?; - - msg!("Anker: Withdrew {} for {}.", amount, st_sol_amount,); - anker.metrics.observe_withdraw(st_sol_amount, amount)?; - - anker.save(accounts.anker) -} - -/// Change the Terra rewards destination. -/// Solido's manager needs to sign the transaction. -#[inline(never)] -fn process_change_terra_rewards_destination( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - terra_rewards_destination: TerraAddress, -) -> ProgramResult { - let accounts = ChangeTerraRewardsDestinationAccountsInfo::try_from_slice(accounts_raw)?; - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - solido.check_manager(accounts.manager)?; - - anker.terra_rewards_destination = terra_rewards_destination; - anker.save(accounts.anker) -} - -/// Change the Token Pool instance. -/// Solido's manager needs to sign the transaction. -#[inline(never)] -fn process_change_token_swap_pool( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], -) -> ProgramResult { - let accounts = ChangeTokenSwapPoolAccountsInfo::try_from_slice(accounts_raw)?; - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - solido.check_manager(accounts.manager)?; - - let current_token_swap_program_id = accounts.current_token_swap_pool.owner; - let current_token_swap = anker.get_token_swap_instance( - accounts.current_token_swap_pool, - current_token_swap_program_id, - )?; - - // `get_token_swap_instance` compares the account to the one stored in - // `anker.token_swap_pool`. We assign first so we have the correct value to - // compare. If the check fails, the transaction will revert. - anker.token_swap_pool = *accounts.new_token_swap_pool.key; - let new_token_swap_program_id = accounts.new_token_swap_pool.owner; - let new_token_swap = - anker.get_token_swap_instance(accounts.new_token_swap_pool, new_token_swap_program_id)?; - - anker.check_change_token_swap_pool(&solido, current_token_swap, new_token_swap)?; - anker.save(accounts.anker) -} - -/// Change Anker's `sell_rewards_min_out_bps`. -/// Solido's manager needs to sign the transaction. -#[inline(never)] -fn process_change_sell_rewards_min_out_bps( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - sell_rewards_min_out_bps: u64, -) -> ProgramResult { - let accounts = ChangeSellRewardsMinOutBpsAccountsInfo::try_from_slice(accounts_raw)?; - let (solido, mut anker) = deserialize_anker(program_id, accounts.anker, accounts.solido)?; - solido.check_manager(accounts.manager)?; - - // Cannot be greater than 100%. - if sell_rewards_min_out_bps > 10_000 { - return Err(AnkerError::InvalidSellRewardsMinOutBps.into()); - } - - anker.sell_rewards_min_out_bps = sell_rewards_min_out_bps; - anker.save(accounts.anker) -} - -/// Send rewards via Wormhole from the UST reserve address to Terra. -#[inline(never)] -fn process_send_rewards( - program_id: &Pubkey, - accounts_raw: &[AccountInfo], - wormhole_nonce: u32, -) -> ProgramResult { - let accounts = Box::new(SendRewardsAccountsInfo::try_from_slice(accounts_raw)?); - let anker = deserialize_anker(program_id, accounts.anker, accounts.solido)?.1; - anker.check_ust_reserve_address( - program_id, - accounts.anker.key, - accounts.ust_reserve_account, - )?; - let wormhole_transfer_args = anker.check_send_rewards(&accounts)?; - - // We put the temporaries in a scope here to make sure they are popped from - // the stack before we continue the function, because this function is scarce - // on stack space. - let reserve_ust_amount = { - let ust_reserve_state = spl_token::state::Account::unpack_from_slice( - &accounts.ust_reserve_account.data.borrow(), - )?; - - // Check UST mint. - if &ust_reserve_state.mint != accounts.ust_mint.key { - return Err(AnkerError::InvalidTokenMint.into()); - } - MicroUst(ust_reserve_state.amount) - }; - - let reserve_seeds = [ - accounts.anker.key.as_ref(), - ANKER_RESERVE_AUTHORITY, - &[anker.reserve_authority_bump_seed], - ]; - - // Stack space is scarce in this function, so we put as many things as we can - // in a scope to make sure the stack space of the temporaries is reclaimed. - { - // Wormhole signs the SPL token transfer with its "authority signer key", - // which means we need to authorize that key to modify our UST reserve. - let instr = Box::new(spl_token::instruction::approve( - accounts.spl_token.key, - accounts.ust_reserve_account.key, - accounts.authority_signer_key.key, - accounts.reserve_authority.key, - // The next argument is "signers", which is only relevant for this SPL - // token multisig feature, which we do not use. - &[], - reserve_ust_amount.0, - )?); - - invoke_signed( - &instr, - // This vec is not useless, we want the data to go on the heap, not on the stack! - #[allow(clippy::useless_vec)] - &vec![ - accounts.ust_reserve_account.clone(), - accounts.authority_signer_key.clone(), - accounts.reserve_authority.clone(), - ], - &[&reserve_seeds[..]], - )?; - } - - let payload = Box::new(crate::wormhole::Payload::new( - wormhole_nonce, - reserve_ust_amount, - anker.terra_rewards_destination.to_foreign(), - )); - - // For the order and meaning of the accounts, see also - // https://github.com/certusone/wormhole/blob/537d56b37aa041a585f2c90515fa3a7ffa5898b5/solana/modules/token_bridge/program/src/instructions.rs#L328-L390. - let instr = Box::new(get_wormhole_transfer_instruction( - &payload, - &wormhole_transfer_args, - )); - let accounts = vec![ - accounts.payer.clone(), - accounts.config_key.clone(), - accounts.ust_reserve_account.clone(), - accounts.reserve_authority.clone(), - accounts.ust_mint.clone(), - accounts.wrapped_meta_key.clone(), - accounts.authority_signer_key.clone(), - accounts.bridge_config.clone(), - accounts.message.clone(), - accounts.emitter_key.clone(), - accounts.sequence_key.clone(), - accounts.fee_collector_key.clone(), - accounts.sysvar_clock.clone(), - accounts.sysvar_rent.clone(), - accounts.system_program.clone(), - accounts.wormhole_core_bridge_program_id.clone(), - accounts.spl_token.clone(), - ]; - // Send UST tokens via Wormhole 🤞. - invoke_signed(&instr, &accounts[..], &[&reserve_seeds[..]]) -} - -/// Processes [Instruction](enum.Instruction.html). -pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { - let instruction = AnkerInstruction::try_from_slice(input)?; - match instruction { - AnkerInstruction::Initialize { - terra_rewards_destination, - sell_rewards_min_out_bps, - } => process_initialize( - program_id, - accounts, - terra_rewards_destination, - sell_rewards_min_out_bps, - ), - AnkerInstruction::Deposit { amount } => process_deposit(program_id, accounts, amount), - AnkerInstruction::Withdraw { amount } => process_withdraw(program_id, accounts, amount), - AnkerInstruction::FetchPoolPrice => process_fetch_pool_price(program_id, accounts), - AnkerInstruction::SellRewards => process_sell_rewards(program_id, accounts), - AnkerInstruction::ChangeTerraRewardsDestination { - terra_rewards_destination, - } => process_change_terra_rewards_destination( - program_id, - accounts, - terra_rewards_destination, - ), - AnkerInstruction::ChangeTokenSwapPool => { - process_change_token_swap_pool(program_id, accounts) - } - AnkerInstruction::SendRewards { wormhole_nonce } => { - process_send_rewards(program_id, accounts, wormhole_nonce) - } - AnkerInstruction::ChangeSellRewardsMinOutBps { - sell_rewards_min_out_bps, - } => { - process_change_sell_rewards_min_out_bps(program_id, accounts, sell_rewards_min_out_bps) - } - } -} diff --git a/anker/src/state.rs b/anker/src/state.rs deleted file mode 100644 index 0e2bdcf1d..000000000 --- a/anker/src/state.rs +++ /dev/null @@ -1,988 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use crate::instruction::{ - FetchPoolPriceAccountsInfo, SellRewardsAccountsInfo, SendRewardsAccountsInfo, -}; -use crate::metrics::Metrics; -use crate::wormhole::{check_wormhole_account, TerraAddress, WormholeTransferArgs}; -use crate::{ - error::AnkerError, ANKER_MINT_AUTHORITY, ANKER_RESERVE_AUTHORITY, ANKER_STSOL_RESERVE_ACCOUNT, - ANKER_UST_RESERVE_ACCOUNT, -}; -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use lido::state::Lido; -use lido::token::{ArithmeticError, Lamports, Rational, StLamports}; -use lido::util::serialize_b58; -use serde::Serialize; -use solana_program::program_error::ProgramError; -use solana_program::{ - account_info::AccountInfo, clock::Slot, entrypoint::ProgramResult, msg, program_pack::Pack, - pubkey::Pubkey, -}; -use spl_token_swap::state::SwapV1; - -use crate::token::{self, BLamports, MicroUst}; - -/// Size of the serialized [`Anker`] struct, in bytes. -pub const ANKER_LEN: usize = 370; -pub const ANKER_VERSION: u8 = 0; - -// Next are three constants related to stored stSOL/UST prices. Because Anker is -// permissionless, everybody can call `SellRewards` if there are rewards to sell. -// This means that the caller could sandwich the `SellRewards` between two -// instructions that swap against the same stSOL/UST pool that Anker uses, to -// give us a bad price, and take the difference. To mitigate this risk, we set a -// `min_out` on the swap instruction, but in order to do so, we need a "fair" -// price. For that, we sample 5 past prices, at least some number of slots apart -// (enough that they are produced by different leaders), but also not too old, -// to make sure the price is still fresh. Then we take the median of that as a -// "fair" price and set `min_out` based on that. Now if anybody is trying to -// sandwich us, they would also have to sandwich 3 of those 5 times where we sample -// the price (and they pay swap fees), and they are competing with our honest -// maintenance bot for that (and possibly with others). Also, having a recent -// price ensures that we don't sell rewards at times of extreme volatility. - -/// The number of historical stSOL/UST exchange rates we store. -pub const POOL_PRICE_NUM_SAMPLES: usize = 5; - -/// The minimum number of slots that must elapse after the most recent stSOL/UST price sample, -/// before we can store a new sample. -pub const POOL_PRICE_MIN_SAMPLE_DISTANCE: Slot = 100; - -/// The maximum age of the oldest stSOL/UST price sample where we still allow `SellRewards`. -/// -/// This value should be larger than `POOL_PRICE_NUM_SAMPLES * POOL_PRICE_MIN_SAMPLE_DISTANCE`. -/// -/// At ~550 ms per slot, 1000 slots is roughly 9 minutes. -pub const POOL_PRICE_MAX_SAMPLE_AGE: Slot = 1000; - -#[repr(C)] -#[derive( - Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct WormholeParameters { - /// The Wormhole program associated with this instance. - pub core_bridge_program_id: Pubkey, - /// The Wormhole program for token transfers associated with this instance. - pub token_bridge_program_id: Pubkey, -} - -/// The price of 1 stSOL expressed in UST, as observed from the pool in a particular slot. -#[repr(C)] -#[derive( - Clone, - Copy, - Debug, - Default, - BorshDeserialize, - BorshSerialize, - BorshSchema, - Eq, - PartialEq, - Serialize, -)] -pub struct HistoricalStSolPrice { - /// The slot in which this price was observed. - pub slot: Slot, - - /// The price of 1 stSOL (1e9 stLamports). - #[serde(rename = "st_sol_price_in_micro_ust")] - pub st_sol_price_in_ust: MicroUst, -} - -#[repr(C)] -#[derive( - Clone, - Copy, - Debug, - Default, - BorshDeserialize, - BorshSerialize, - BorshSchema, - Eq, - PartialEq, - Serialize, -)] -pub struct HistoricalStSolPriceArray(pub [HistoricalStSolPrice; POOL_PRICE_NUM_SAMPLES]); - -impl HistoricalStSolPriceArray { - /// Create new `HistorialStSolPriceArray` with slot 0 and 1 UST in each - /// position of the array. - pub fn new() -> Self { - HistoricalStSolPriceArray( - [HistoricalStSolPrice { - slot: 0, - st_sol_price_in_ust: MicroUst(1_000_000), - }; 5], - ) - } - - /// Get last price from the array. - pub fn last(&self) -> HistoricalStSolPrice { - self.0[POOL_PRICE_NUM_SAMPLES - 1] - } - - /// Get first price from the array. - pub fn first(&self) -> HistoricalStSolPrice { - self.0[0] - } - - /// Insert `st_sol_price_in_ust` at the end of the array and rotate it. - pub fn insert_and_rotate(&mut self, slot: Slot, st_sol_price_in_ust: MicroUst) { - // Maintain the invariant that samples are sorted by ascending slot number. - // The sample at index 0 is the oldest, so we remove it (well, move it to the - // end to be overwritten), and move everything else closer to the beginning - // of the array. Then we overwrite the last element with the current price - // and slot number, and we confirmed above that that slot number is larger - // than the slot number of the sample before it. - self.0.rotate_left(1); - self.0[POOL_PRICE_NUM_SAMPLES - 1].slot = slot; - self.0[POOL_PRICE_NUM_SAMPLES - 1].st_sol_price_in_ust = st_sol_price_in_ust; - assert!(self.0[POOL_PRICE_NUM_SAMPLES - 1].slot >= self.0[POOL_PRICE_NUM_SAMPLES - 2].slot); - } - - /// Calculate the minimum amount we are willing to pay for the `StLamports` - /// rewards based on the median price from the historical price information. - pub fn minimum_ust_swap_amount( - &self, - rewards: StLamports, - sell_rewards_min_out_bps: u64, - ) -> Result { - let mut sorted_arr = self.0; - sorted_arr.sort_by_key(|x| x.st_sol_price_in_ust); - // Get median historical price. - let median_price = sorted_arr[POOL_PRICE_NUM_SAMPLES / 2]; - let minimum_ust_per_st_sol = (median_price.st_sol_price_in_ust - * Rational { - numerator: sell_rewards_min_out_bps, - denominator: 10_000, - })?; - let minimum_price = (rewards - * Rational { - numerator: minimum_ust_per_st_sol.0, - denominator: 1_000_000_000, - })?; - Ok(MicroUst(minimum_price.0)) - } -} - -#[repr(C)] -#[derive( - Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct Anker { - /// Version number for Anker. - pub version: u8, - - /// The Solido program that owns the `solido` instance. - #[serde(serialize_with = "serialize_b58")] - pub solido_program_id: Pubkey, - - /// The associated Solido instance address. - #[serde(serialize_with = "serialize_b58")] - pub solido: Pubkey, - - /// The SPL Token mint address for bSOL. - #[serde(serialize_with = "serialize_b58")] - pub b_sol_mint: Pubkey, - - /// Token swap data. Used to swap stSOL for UST. - #[serde(serialize_with = "serialize_b58")] - pub token_swap_pool: Pubkey, - - /// Destination of the rewards on Terra, paid in UST. - pub terra_rewards_destination: TerraAddress, - - /// Wormhole parameters associated with this instance. - pub wormhole_parameters: WormholeParameters, - - /// When we sell rewards, we set the minimum out to stSOL amount times the - /// median of the recent price samples times a factor alpha. In other words, - /// this factor alpha is `1 - max_slippage`. Alpha is defined as - /// `sell_rewards_min_out_bps / 1e4`. The `bps` here means "basis points". - /// A basis point is 0.01% = 1e-4. - pub sell_rewards_min_out_bps: u64, - - /// Metrics for informational purposes. - pub metrics: Metrics, - - /// Historical stSOL prices, used to prevent sandwiching when we sell rewards. - /// - /// Invariant: entries are sorted by ascending slot number (so the oldest - /// entry is at index 0). - pub historical_st_sol_prices: HistoricalStSolPriceArray, - - /// Bump seed for the derived address that this Anker instance should live at. - pub self_bump_seed: u8, - - /// Bump seed for the mint authority derived address. - pub mint_authority_bump_seed: u8, - - /// Bump seed for the reserve authority (owner of the reserve account) derived address. - pub reserve_authority_bump_seed: u8, - - /// Bump seed for the reserve account (SPL token account that holds stSOL). - pub st_sol_reserve_account_bump_seed: u8, - - /// Bump seed for the UST reserve account. - pub ust_reserve_account_bump_seed: u8, -} - -impl Anker { - pub fn save(&self, account: &AccountInfo) -> ProgramResult { - // NOTE: If you ended up here because the tests are failing because the - // runtime complained that an account's size was modified by a program - // that wasn't its owner, double check that the name passed to - // ProgramTest matches the name of the crate. - BorshSerialize::serialize(self, &mut *account.data.borrow_mut())?; - Ok(()) - } - - /// Confirm that the account address is the derived address where the Anker instance should live. - pub fn check_self_address( - &self, - anker_program_id: &Pubkey, - account_info: &AccountInfo, - ) -> ProgramResult { - let address = Pubkey::create_program_address( - &[self.solido.as_ref(), &[self.self_bump_seed]], - anker_program_id, - ) - .expect("Depends only on Anker-controlled values, should not fail."); - - if *account_info.key != address { - msg!( - "Expected Anker instance for Solido instance {} to be {}, but found {} instead.", - self.solido, - address, - account_info.key, - ); - return Err(AnkerError::InvalidDerivedAccount.into()); - } - Ok(()) - } - - /// Confirm that the derived account address matches the `account_info` adddress. - fn check_derived_account_address( - &self, - name: &'static str, - seed: &'static [u8], - bump_seed: u8, - anker_program_id: &Pubkey, - anker_instance: &Pubkey, - account_info: &AccountInfo, - ) -> ProgramResult { - let address = Pubkey::create_program_address( - &[anker_instance.as_ref(), seed, &[bump_seed]], - anker_program_id, - ) - .expect("Depends only on Anker-controlled values, should not fail."); - - if *account_info.key != address { - msg!( - "Expected {} to be {}, but found {} instead.", - name, - address, - account_info.key, - ); - return Err(AnkerError::InvalidDerivedAccount.into()); - } - Ok(()) - } - - /// Confirm that the provided stSOL reserve accounts is the one that - /// belongs to this instance. - /// - /// This does not check that the stSOL reserve is an stSOL account. - pub fn check_st_sol_reserve_address( - &self, - anker_program_id: &Pubkey, - anker_instance: &Pubkey, - st_sol_reserve_account_info: &AccountInfo, - ) -> ProgramResult { - self.check_derived_account_address( - "the stSOL reserve account", - ANKER_STSOL_RESERVE_ACCOUNT, - self.st_sol_reserve_account_bump_seed, - anker_program_id, - anker_instance, - st_sol_reserve_account_info, - ) - } - - /// Confirm that the provided UST reserve accounts is the one that - /// belongs to this instance. - /// - /// This does not check that the UST reserve is an UST account. - pub fn check_ust_reserve_address( - &self, - anker_program_id: &Pubkey, - anker_instance: &Pubkey, - ust_reserve_account_info: &AccountInfo, - ) -> ProgramResult { - self.check_derived_account_address( - "the UST reserve account", - ANKER_UST_RESERVE_ACCOUNT, - self.ust_reserve_account_bump_seed, - anker_program_id, - anker_instance, - ust_reserve_account_info, - ) - } - - /// Confirm that the provided reserve authority is the one that belongs to this instance. - pub fn check_reserve_authority( - &self, - anker_program_id: &Pubkey, - anker_instance: &Pubkey, - reserve_authority_info: &AccountInfo, - ) -> ProgramResult { - self.check_derived_account_address( - "the reserve authority", - ANKER_RESERVE_AUTHORITY, - self.reserve_authority_bump_seed, - anker_program_id, - anker_instance, - reserve_authority_info, - ) - } - - /// Confirm that the provided bSOL mint authority is the one that belongs to this instance. - pub fn check_mint_authority( - &self, - anker_program_id: &Pubkey, - anker_instance: &Pubkey, - mint_authority_info: &AccountInfo, - ) -> ProgramResult { - self.check_derived_account_address( - "the bSOL mint authority", - ANKER_MINT_AUTHORITY, - self.mint_authority_bump_seed, - anker_program_id, - anker_instance, - mint_authority_info, - ) - } - - /// Confirm that the provided mint account is the one stored in this instance. - pub fn check_mint(&self, provided_mint: &AccountInfo) -> ProgramResult { - if *provided_mint.owner != spl_token::id() { - msg!( - "Expected bSOL mint to be owned by the SPL token program ({}), but found {}.", - spl_token::id(), - provided_mint.owner, - ); - return Err(AnkerError::InvalidTokenMint.into()); - } - - if self.b_sol_mint != *provided_mint.key { - msg!( - "Invalid mint account, expected {}, but found {}.", - self.b_sol_mint, - provided_mint.key, - ); - return Err(AnkerError::InvalidTokenMint.into()); - } - Ok(()) - } - - fn check_is_spl_token_account( - mint_name: &'static str, - mint_address: &Pubkey, - token_account_info: &AccountInfo, - ) -> ProgramResult { - if token_account_info.owner != &spl_token::id() { - msg!( - "Expected SPL token account to be owned by {}, but it's owned by {} instead.", - spl_token::id(), - token_account_info.owner - ); - return Err(AnkerError::InvalidTokenAccountOwner.into()); - } - - let token_account = - match spl_token::state::Account::unpack_from_slice(&token_account_info.data.borrow()) { - Ok(account) => account, - Err(..) => { - msg!( - "Expected an SPL token account at {}.", - token_account_info.key - ); - return Err(AnkerError::InvalidTokenAccount.into()); - } - }; - - if token_account.mint != *mint_address { - msg!( - "Expected mint of {} to be {} mint ({}), but found {}.", - token_account_info.key, - mint_name, - mint_address, - token_account.mint, - ); - return Err(AnkerError::InvalidTokenMint.into()); - } - - Ok(()) - } - - /// Confirm that the account is an SPL token account that holds bSOL. - pub fn check_is_b_sol_account(&self, token_account_info: &AccountInfo) -> ProgramResult { - Anker::check_is_spl_token_account("our bSOL", &self.b_sol_mint, token_account_info) - } - - /// Confirm that the account is an SPL token account that holds stSOL. - pub fn check_is_st_sol_account( - &self, - solido: &Lido, - token_account_info: &AccountInfo, - ) -> ProgramResult { - Anker::check_is_spl_token_account("Solido's stSOL", &solido.st_sol_mint, token_account_info) - } - - /// Get an instance of the Token Swap V1 from the provided account info. - pub fn get_token_swap_instance( - &self, - token_swap_account: &AccountInfo, - token_swap_program_id: &Pubkey, - ) -> Result { - self.check_token_swap_pool(token_swap_account)?; - - // We do not check the owner of the `token_swap_account`. Since we store - // this address in Anker's state, and we also trust the manager that changes - // this address, we don't verify the account's owner. This also allows us to - // test different token swap programs ids on different clusters. - // However, we *should* check that the program we are going to call later to - // do the token swap, is actually the intended token swap program. - if token_swap_account.owner != token_swap_program_id { - msg!( - "Encountered wrong token swap program; expected {} but found {}.", - token_swap_account.owner, - token_swap_program_id, - ); - return Err(AnkerError::WrongSplTokenSwap.into()); - } - - // Check that version byte corresponds to V1 version byte. - if token_swap_account.data.borrow().len() != spl_token_swap::state::SwapVersion::LATEST_LEN - { - msg!( - "Length of the Token Swap is invalid, expected {}, found {}", - spl_token_swap::state::SwapVersion::LATEST_LEN, - token_swap_account.data.borrow().len() - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - if token_swap_account.data.borrow()[0] != 1u8 { - msg!( - "Token Swap instance version is different from what we expect, expected 1, found {}", - token_swap_account.data.borrow()[0] - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - // We should ignore the version 1st byte for the unpack. - spl_token_swap::state::SwapV1::unpack(&token_swap_account.data.borrow()[1..]) - } - - /// Check if we can change the token swap account. - pub fn check_change_token_swap_pool( - &self, - solido: &Lido, - current_token_swap: SwapV1, - new_token_swap: SwapV1, - ) -> ProgramResult { - // We don't check that the old pool's owner is the same as the new - // pool's owner. It's the manager's responsibility to replace the token - // pool swap with a valid one. This also allows us to change the pool - // program, if necessary. - // Check if the token swap account is the same one as the stored in the instance. - - // Get stSOL and UST mint. We trust that the UST mint stored in the current instance is right. - let (st_sol_mint, ust_mint) = if current_token_swap.token_a_mint == solido.st_sol_mint { - ( - current_token_swap.token_a_mint, - current_token_swap.token_b_mint, - ) - } else { - ( - current_token_swap.token_b_mint, - current_token_swap.token_a_mint, - ) - }; - - // Get the stSOL and UST pool token, and verify that the minters are right. - if new_token_swap.token_a_mint == st_sol_mint { - // token_a_mint is stSOL mint. - if new_token_swap.token_b_mint != ust_mint { - // token_b_mint should be ust_mint. - msg!( - "token_b_mint is expected to be the UST mint ({}), but is {}", - ust_mint, - new_token_swap.token_b_mint - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - } else if new_token_swap.token_a_mint == ust_mint { - // token_a is UST. - if new_token_swap.token_b_mint != st_sol_mint { - // token_b_mint should be ust_mint. - msg!( - "token_b_mint is expected to be the stSOL mint ({}), but is {}", - st_sol_mint, - new_token_swap.token_b_mint - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - } else { - // token_a_mint is wrong. - msg!( - "token_a_mint is expected to be either stSOL mint ({}), or UST mint ({}) but is {}", - st_sol_mint, - ust_mint, - new_token_swap.token_a_mint - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - }; - - Ok(()) - } - - fn check_token_swap_pool(&self, token_swap_account: &AccountInfo) -> ProgramResult { - if &self.token_swap_pool != token_swap_account.key { - msg!( - "Invalid Token Swap instance, expected {}, found {}", - self.token_swap_pool, - token_swap_account.key - ); - return Err(AnkerError::WrongSplTokenSwap.into()); - } - Ok(()) - } - - /// Confirm that the passed accounts match those stored in the pool. - pub fn check_token_swap_before_fetch_price( - &self, - accounts: &FetchPoolPriceAccountsInfo, - ) -> ProgramResult { - // Check if the token swap account is the same one as the stored in the instance. - let token_swap_program_id = accounts.token_swap_pool.owner; - let token_swap = - self.get_token_swap_instance(accounts.token_swap_pool, token_swap_program_id)?; - - // Check that the pool still has token - let (pool_st_sol_account, pool_ust_account) = if &token_swap.token_a - == accounts.pool_st_sol_account.key - { - Ok((token_swap.token_a, token_swap.token_b)) - } else if &token_swap.token_a == accounts.pool_ust_account.key { - Ok((token_swap.token_b, token_swap.token_a)) - } else { - msg!( - "Could not find a match for token swap account {}, candidates were the stSol account {} or UST account {}", - token_swap.token_a, - accounts.pool_st_sol_account.key, - accounts.pool_ust_account.key - ); - Err(AnkerError::WrongSplTokenSwapParameters) - }?; - - if &pool_st_sol_account != accounts.pool_st_sol_account.key { - msg!( - "Token swap stSol token is different from what is stored in the instance, expected {}, found {}", - pool_st_sol_account, - accounts.pool_st_sol_account.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - if &pool_ust_account != accounts.pool_ust_account.key { - msg!( - "Token swap UST token is different from what is stored in the instance, expected {}, found {}", - pool_ust_account, - accounts.pool_ust_account.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - - Ok(()) - } - - /// Check the if the token swap program is the same as the one stored in the - /// instance. - /// - /// Check all the token swap associated accounts. - /// Check if the rewards destination is the same as the one stored in Anker. - pub fn check_token_swap_before_sell( - &self, - anker_program_id: &Pubkey, - accounts: &SellRewardsAccountsInfo, - ) -> ProgramResult { - // Check if the token swap account is the same one as the stored in the instance. - let token_swap = self.get_token_swap_instance( - accounts.token_swap_pool, - accounts.token_swap_program_id.key, - )?; - - // Check token swap instance parameters. - // Check UST token accounts. - self.check_ust_reserve_address( - anker_program_id, - accounts.anker.key, - accounts.ust_reserve_account, - )?; - - // Pool stSOL and UST token could be swapped. - let (pool_st_sol_account, pool_st_sol_mint, pool_ust_account, pool_ust_mint) = - if &token_swap.token_a == accounts.pool_st_sol_account.key { - Ok(( - token_swap.token_a, - token_swap.token_a_mint, - token_swap.token_b, - token_swap.token_b_mint, - )) - } else if &token_swap.token_a == accounts.pool_ust_account.key { - Ok(( - token_swap.token_b, - token_swap.token_b_mint, - token_swap.token_a, - token_swap.token_a_mint, - )) - } else { - msg!( - "Could not find a match for token swap account {}, candidates were the StSol account {} or UST account {}", - token_swap.token_a, - accounts.pool_st_sol_account.key, - accounts.pool_ust_account.key - ); - Err(AnkerError::WrongSplTokenSwapParameters) - }?; - - // Check stSOL token. - if &pool_st_sol_account != accounts.pool_st_sol_account.key { - msg!( - "Token Swap StSol token is different from what is stored in the instance, expected {}, found {}", - pool_st_sol_account, - accounts.pool_st_sol_account.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - // Check UST token. - if &pool_ust_account != accounts.pool_ust_account.key { - msg!( - "Token Swap UST token is different from what is stored in the instance, expected {}, found {}", - pool_ust_account, - accounts.pool_ust_account.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - // Check pool mint. - if &token_swap.pool_mint != accounts.pool_mint.key { - msg!( - "Token Swap mint is different from what is stored in the instance, expected {}, found {}", - token_swap.pool_mint, - accounts.pool_mint.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - - // Check stSOL mint. - if &pool_st_sol_mint != accounts.st_sol_mint.key { - msg!( - "Token Swap StSol mint is different from what is stored in the instance, expected {}, found {}", - pool_st_sol_mint, - accounts.st_sol_mint.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - // Check UST mint. - if &pool_ust_mint != accounts.ust_mint.key { - msg!( - "Token Swap UST mint is different from what is stored in the instance, expected {}, found {}", - pool_ust_mint, - accounts.ust_mint.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - // Check pool fee. - if &token_swap.pool_fee_account != accounts.pool_fee_account.key { - msg!( - "Token Swap fee account is different from what is stored in the instance, expected {}, found {}", - token_swap.pool_fee_account, - accounts.pool_fee_account.key - ); - return Err(AnkerError::WrongSplTokenSwapParameters.into()); - } - - Ok(()) - } - - pub fn check_send_rewards( - &self, - accounts: &SendRewardsAccountsInfo, - ) -> Result, ProgramError> { - check_wormhole_account( - "token bridge program", - &self.wormhole_parameters.token_bridge_program_id, - accounts.wormhole_token_bridge_program_id.key, - )?; - check_wormhole_account( - "core bridge program", - &self.wormhole_parameters.core_bridge_program_id, - accounts.wormhole_core_bridge_program_id.key, - )?; - - let wormhole_transfer_args = WormholeTransferArgs::new( - self.wormhole_parameters.token_bridge_program_id, - self.wormhole_parameters.core_bridge_program_id, - *accounts.ust_mint.key, - *accounts.payer.key, - *accounts.ust_reserve_account.key, - *accounts.reserve_authority.key, - *accounts.message.key, - ); - - check_wormhole_account( - "config key", - &wormhole_transfer_args.config_key, - accounts.config_key.key, - )?; - check_wormhole_account( - "wrapped meta key", - &wormhole_transfer_args.wrapped_meta_key, - accounts.wrapped_meta_key.key, - )?; - check_wormhole_account( - "authority signer key", - &wormhole_transfer_args.authority_signer_key, - accounts.authority_signer_key.key, - )?; - check_wormhole_account( - "bridge config", - &wormhole_transfer_args.bridge_config, - accounts.bridge_config.key, - )?; - check_wormhole_account( - "emitter key", - &wormhole_transfer_args.emitter_key, - accounts.emitter_key.key, - )?; - check_wormhole_account( - "sequence key", - &wormhole_transfer_args.sequence_key, - accounts.sequence_key.key, - )?; - check_wormhole_account( - "fee collector key", - &wormhole_transfer_args.fee_collector_key, - accounts.fee_collector_key.key, - )?; - Ok(Box::new(wormhole_transfer_args)) - } - - /// Get the `amount` of tokens from the SPL account defined by `account`. - /// Does not perform any checks, fails if not able to decode an SPL account. - pub fn get_token_amount(account: &AccountInfo) -> Result { - if account.owner != &spl_token::id() { - msg!( - "Token accounts should be owned by {}, it's owned by {}", - spl_token::id(), - account.owner - ); - return Err(AnkerError::InvalidOwner.into()); - } - let account_state = spl_token::state::Account::unpack_from_slice(&account.data.borrow())?; - Ok(account_state.amount) - } -} - -/// Exchange rate from bSOL to stSOL. -/// -/// This can be computed in different ways, but -pub struct ExchangeRate { - /// Amount of stSOL that is equal in value to `b_sol_amount`. - pub st_sol_amount: StLamports, - - /// Amount of bSOL that is equal in value to `st_sol_amount`. - pub b_sol_amount: BLamports, -} - -impl ExchangeRate { - /// Return the bSOL/stSOL rate that ensures that 1 bSOL = 1 SOL. - pub fn from_solido_pegged(solido: &Lido) -> ExchangeRate { - // On mainnet, the Solido instance exists for a while already, and its - // stSOL supply and SOL balance are nonzero. But for local testing, in - // the first epoch, the exchange rate stored in the Solido instance is - // 0 stSOL = 0 SOL. To still enable Anker deposits during that first - // epoch, we define the initial exchange rate to be 1 stSOL = 1 bSOL, - // because Solido initially uses 1 SOL = 1 stSOL if the balance is zero. - if solido.exchange_rate.st_sol_supply == StLamports(0) - && solido.exchange_rate.sol_balance == Lamports(0) - { - ExchangeRate { - st_sol_amount: StLamports(1), - b_sol_amount: BLamports(1), - } - } else { - ExchangeRate { - st_sol_amount: solido.exchange_rate.st_sol_supply, - // By definition here, we set 1 bSOL equal to 1 SOL. - b_sol_amount: BLamports(solido.exchange_rate.sol_balance.0), - } - } - } - - /// Return the bSOL/stSOL rate assuming 1 bSOL is a fraction 1/supply of the reserve. - pub fn from_anker_unpegged( - b_sol_supply: BLamports, - reserve_balance: StLamports, - ) -> ExchangeRate { - ExchangeRate { - st_sol_amount: reserve_balance, - b_sol_amount: b_sol_supply, - } - } - - pub fn exchange_st_sol(&self, amount: StLamports) -> token::Result { - // This swap is only used when depositing, so we should use the exchange - // rate based on the Solido instance. It should have a non-zero amount - // of assets under management, so the exchange rate is well-defined. - assert!(self.b_sol_amount > BLamports(0)); - assert!(self.st_sol_amount > StLamports(0)); - - let rate = Rational { - numerator: self.b_sol_amount.0, - denominator: self.st_sol_amount.0, - }; - - // The result is in StLamports, because the type system considers Rational - // dimensionless, but in this case `rate` has dimensions bSOL/stSOL, so - // we need to re-wrap the result in the right type. - (amount * rate).map(|x| BLamports(x.0)) - } - - pub fn exchange_b_sol(&self, amount: BLamports) -> token::Result { - // We can get the exchange rate either from Solido, or from the reserve + supply. - // But in either case, neither of the values should be zero when we exchange bSOL - // back to stSOL: in Solido neither the SOL balance nor the stSOL supply should - // ever become zero, because we deposited some SOL in it that we do not plan to - // ever withdraw. And for Anker, if you have bSOL to exchange, the only way in - // which it could have been created is by locking some stSOL in Anker, so there - // is a nonzero bSOL supply and nonzero reserve. - assert!(self.b_sol_amount > BLamports(0)); - assert!(self.st_sol_amount > StLamports(0)); - - let rate = Rational { - numerator: self.st_sol_amount.0, - denominator: self.b_sol_amount.0, - }; - - // The result is in BLamports, because the type system considers Rational - // dimensionless, but in this case `rate` has dimensions stSOL/bSOL, so - // we need to re-wrap the result in the right type. - (amount * rate).map(|x| StLamports(x.0)) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_anker_len() { - let instance = Anker::default(); - let mut writer = Vec::new(); - BorshSerialize::serialize(&instance, &mut writer).unwrap(); - assert_eq!(writer.len(), ANKER_LEN); - } - - #[test] - fn test_version_serialise() { - use solana_sdk::borsh::try_from_slice_unchecked; - - for i in 0..=255 { - let anker = Anker { - version: i, - ..Anker::default() - }; - let mut res: Vec = Vec::new(); - BorshSerialize::serialize(&anker, &mut res).unwrap(); - - assert_eq!(res[0], i); - - let anker_recovered = try_from_slice_unchecked(&res[..]).unwrap(); - assert_eq!(anker, anker_recovered); - } - } - - #[test] - fn test_historical_price_array_minimum() { - let mut price_array = HistoricalStSolPriceArray::new(); - // 100 UST for each StSol. - for slot in 0..POOL_PRICE_NUM_SAMPLES as u64 { - price_array.insert_and_rotate(slot, MicroUst(100_000_000)); - } - - // 1 StSol rewards and 1% slippage. - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(1_000_000_000), 9900) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(99_000_000)); - - // 1 StSol rewards and 2% slippage. - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(1_000_000_000), 9800) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(98_000_000)); - - // 80 StSol rewards and 5% slippage - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(80_000_000_000), 9500) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(7_600_000_000)); - - // 331 StSol rewards and 50% slippage - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(331_000_000_000), 5000) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(16_550_000_000)); - } - - #[test] - fn test_different_prices() { - let mut price_array = HistoricalStSolPriceArray::new(); - // Prices in USD per Sol [100, 90, 95, 105, 101], median: 100 - for (slot, price) in [100, 90, 95, 105, 101].iter().enumerate() { - price_array.insert_and_rotate(slot as Slot, MicroUst(price * 1_000_000)); - } - - price_array.insert_and_rotate(4, MicroUst(80_000_000)); - // prices: [90, 95, 105, 101, 80], median: 95 - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(331_000_000_000), 5000) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(15_722_500_000)); - - price_array.insert_and_rotate(5, MicroUst(70_000_000)); - price_array.insert_and_rotate(6, MicroUst(85_000_000)); - // prices: [105, 101, 80, 70, 85], median: 85 - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(100_000_000_000), 9800) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(8_330_000_000)); - } - - #[test] - fn test_historical_price_array_limits() { - let mut price_array = HistoricalStSolPriceArray::new(); - // 100 UST for each StSol. - for slot in 0..POOL_PRICE_NUM_SAMPLES as u64 { - price_array.insert_and_rotate(slot, MicroUst(100_000_000)); - } - - // 100 StLamports rewards and 1% slippage. - let minimum_ust = price_array - .minimum_ust_swap_amount(StLamports(100), 9900) - .unwrap(); - assert_eq!(minimum_ust, MicroUst(9)); - } -} diff --git a/anker/src/token.rs b/anker/src/token.rs deleted file mode 100644 index c3af2c548..000000000 --- a/anker/src/token.rs +++ /dev/null @@ -1,15 +0,0 @@ -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use lido::impl_token; -use lido::token::{ArithmeticError, Rational}; -use serde::Serialize; -use std::{ - convert::TryFrom, - fmt, - iter::Sum, - ops::{Add, Div, Mul, Sub}, -}; - -pub use lido::token::Result; - -impl_token!(BLamports, "bSOL", decimals = 9); -impl_token!(MicroUst, "UST", decimals = 6); diff --git a/anker/src/wormhole.rs b/anker/src/wormhole.rs deleted file mode 100644 index 2fc09e8c5..000000000 --- a/anker/src/wormhole.rs +++ /dev/null @@ -1,345 +0,0 @@ -use std::fmt; -use std::fmt::Formatter; -use std::str::FromStr; - -use bech32::{FromBase32, ToBase32}; -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use serde::Serialize; -use solana_program::{ - entrypoint::ProgramResult, - instruction::{AccountMeta, Instruction}, - msg, - pubkey::Pubkey, -}; - -use crate::{error::AnkerError, token::MicroUst}; - -/// Wormhole's Terra chain id. -pub const WORMHOLE_CHAIN_ID_TERRA: u16 = 3; - -/// The constant is 4, because it is the instruction at index 4, starting from 0. -/// https://github.com/certusone/wormhole/blob/94695ee125399f67c3a62f26ebd807cf532567c4/solana/modules/token_bridge/program/src/lib.rs#L80 -const WORMHOLE_WRAPPED_TRANSFER_CODE: u8 = 4; - -#[repr(C)] -#[derive( - Clone, Default, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct ForeignAddress([u8; 32]); - -#[derive(Debug, Eq, PartialEq)] -pub enum AddressError { - /// Bech32 decoding failed. - Bech32(bech32::Error), - - /// The human-readable part of the address is not "terra". - HumanReadablePartIsNotTerra, - - /// The address is either too long or too short. - LengthNot20Bytes, - - /// The variant is not the classic BIP-0173 bech32. - VariantIsNotBech32, -} - -impl fmt::Display for AddressError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - AddressError::Bech32(err) => write!(f, "Invalid bech32 format: {}", err), - AddressError::HumanReadablePartIsNotTerra => { - write!(f, "Address does not start with 'terra'.") - } - AddressError::LengthNot20Bytes => write!(f, "The address is not 20 bytes long."), - AddressError::VariantIsNotBech32 => { - write!(f, "The address variant is not the classic BIP-0173 bech32.") - } - } - } -} - -impl std::error::Error for AddressError {} - -#[repr(C)] -#[derive( - Clone, Default, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Eq, PartialEq, Serialize, -)] -pub struct TerraAddress([u8; 20]); - -impl TerraAddress { - pub fn to_foreign(&self) -> ForeignAddress { - // Wormhole treats all addresses as bytestrings of length 32. If the - // address is shorter, it must be left-padded with zeros. - let mut foreign = [0_u8; 32]; - foreign[12..].copy_from_slice(&self.0[..]); - ForeignAddress(foreign) - } -} - -impl FromStr for TerraAddress { - type Err = AddressError; - - fn from_str(s: &str) -> Result { - let (hrp, data_u5, variant) = bech32::decode(s).map_err(AddressError::Bech32)?; - if hrp != "terra" { - return Err(AddressError::HumanReadablePartIsNotTerra); - } - if variant != bech32::Variant::Bech32 { - return Err(AddressError::VariantIsNotBech32); - } - - let data_bytes = Vec::::from_base32(&data_u5).map_err(AddressError::Bech32)?; - if data_bytes.len() != 20 { - return Err(AddressError::LengthNot20Bytes); - } - - let mut address = [0; 20]; - address.copy_from_slice(&data_bytes); - - Ok(TerraAddress(address)) - } -} - -impl fmt::Display for TerraAddress { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - bech32::encode_to_fmt(f, "terra", self.0.to_base32(), bech32::Variant::Bech32) - .expect("The HRP is hard-coded and known to be fine, it should not fail.") - } -} - -/// Payload copied and modified from the Wormhole project. -#[repr(C)] -#[derive(BorshSerialize, BorshSchema)] -pub struct Payload { - pub nonce: u32, - pub amount: MicroUst, - pub fee: u64, - pub foreign_address: ForeignAddress, - pub target_chain: u16, -} - -impl Payload { - pub fn new(nonce: u32, amount: MicroUst, foreign_address: ForeignAddress) -> Payload { - Payload { - nonce, - amount, - fee: 0, - foreign_address, - target_chain: WORMHOLE_CHAIN_ID_TERRA, - } - } -} - -pub fn check_wormhole_account( - msg: &'static str, - expected: &Pubkey, - provided: &Pubkey, -) -> ProgramResult { - if expected != provided { - msg!( - "Wrong Wormhole {}. Expected {}, but found {}", - msg, - expected, - provided - ); - return Err(AnkerError::InvalidSendRewardsParameters.into()); - } - Ok(()) -} - -pub struct WormholeTransferArgs { - pub payer: Pubkey, - pub config_key: Pubkey, - pub from: Pubkey, - pub from_owner: Pubkey, - pub wrapped_mint_key: Pubkey, - pub wrapped_meta_key: Pubkey, - pub authority_signer_key: Pubkey, - pub bridge_config: Pubkey, - pub message: Pubkey, - pub emitter_key: Pubkey, - pub sequence_key: Pubkey, - pub fee_collector_key: Pubkey, - pub core_bridge_program_id: Pubkey, - pub token_bridge_program_id: Pubkey, -} - -impl WormholeTransferArgs { - pub fn new( - token_bridge_program_id: Pubkey, - core_bridge_program_id: Pubkey, - wrapped_mint_key: Pubkey, - payer: Pubkey, - from: Pubkey, - from_owner: Pubkey, - message: Pubkey, - ) -> Self { - let (config_key, _) = Pubkey::find_program_address(&[b"config"], &token_bridge_program_id); - let (wrapped_meta_key, _) = Pubkey::find_program_address( - &[b"meta", &wrapped_mint_key.to_bytes()], - &token_bridge_program_id, - ); - let (authority_signer_key, _) = - Pubkey::find_program_address(&[b"authority_signer"], &token_bridge_program_id); - let (bridge_config, _) = - Pubkey::find_program_address(&[b"Bridge"], &core_bridge_program_id); - let (emitter_key, _) = - Pubkey::find_program_address(&[b"emitter"], &token_bridge_program_id); - let (sequence_key, _) = Pubkey::find_program_address( - &[b"Sequence", &emitter_key.to_bytes()], - &core_bridge_program_id, - ); - let (fee_collector_key, _) = - Pubkey::find_program_address(&[b"fee_collector"], &core_bridge_program_id); - - WormholeTransferArgs { - payer, - config_key, - from, - from_owner, - wrapped_mint_key, - wrapped_meta_key, - authority_signer_key, - bridge_config, - message, - emitter_key, - sequence_key, - fee_collector_key, - core_bridge_program_id, - token_bridge_program_id, - } - } -} - -/// Get Wormhole transfer instruction. -pub fn get_wormhole_transfer_instruction( - payload: &Payload, - wormhole_transfer_args: &WormholeTransferArgs, -) -> Instruction { - Instruction { - program_id: wormhole_transfer_args.token_bridge_program_id, - accounts: vec![ - AccountMeta::new(wormhole_transfer_args.payer, true), - AccountMeta::new_readonly(wormhole_transfer_args.config_key, false), - AccountMeta::new(wormhole_transfer_args.from, false), - AccountMeta::new_readonly(wormhole_transfer_args.from_owner, true), - AccountMeta::new(wormhole_transfer_args.wrapped_mint_key, false), - AccountMeta::new_readonly(wormhole_transfer_args.wrapped_meta_key, false), - AccountMeta::new_readonly(wormhole_transfer_args.authority_signer_key, false), - AccountMeta::new(wormhole_transfer_args.bridge_config, false), - AccountMeta::new(wormhole_transfer_args.message, true), - AccountMeta::new_readonly(wormhole_transfer_args.emitter_key, false), - AccountMeta::new(wormhole_transfer_args.sequence_key, false), - AccountMeta::new(wormhole_transfer_args.fee_collector_key, false), - AccountMeta::new_readonly(solana_program::sysvar::clock::id(), false), - // Dependencies - AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false), - AccountMeta::new_readonly(solana_program::system_program::id(), false), - // Program - AccountMeta::new_readonly(wormhole_transfer_args.core_bridge_program_id, false), - AccountMeta::new_readonly(spl_token::id(), false), - ], - data: (WORMHOLE_WRAPPED_TRANSFER_CODE, payload) - .try_to_vec() - .unwrap(), - } -} - -/// Test transaction that transfers UST on Solana to Terra. -/// -/// Based on this transaction: . -#[test] -fn test_get_wormhole_instruction() { - let terra_addr = - TerraAddress::from_str("terra1z7529lza7elcleyhzj2sfq62uk7rtjgnrqeuxr").unwrap(); - let foreign_addr = terra_addr.to_foreign(); - - let payload = Payload::new(0x28fb, MicroUst(1_000_000), foreign_addr); - let payer = Pubkey::from_str("GUVfssWwwu6oXfKyVQUjKcYxgKDJEPhaEwh16kccZkSq").unwrap(); - let from = Pubkey::from_str("3gHYGmunh7mBWHGQ5YjqgKjy44krwenxNZ5cadZ85DtT").unwrap(); - let from_owner = payer; - let wrapped_mint_key = - Pubkey::from_str("5Dmmc5CC6ZpKif8iN5DSY9qNYrWJvEKcX2JrxGESqRMu").unwrap(); - let message = Pubkey::from_str("9yvM539kKjfrowv5yjuJBpTyouuD76X3J8JidobENV9s").unwrap(); - - // Testnet addresses: https://docs.wormholenetwork.com/wormhole/contracts#core-bridge-1. - let token_bridge_id = Pubkey::from_str("DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe").unwrap(); - let core_bridge_id = Pubkey::from_str("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5").unwrap(); - - let wormhole_transfer_args = WormholeTransferArgs::new( - token_bridge_id, - core_bridge_id, - wrapped_mint_key, - payer, - from, - from_owner, - message, - ); - let instruction = get_wormhole_transfer_instruction(&payload, &wormhole_transfer_args); - - let expected_accounts = vec![ - payer, - Pubkey::from_str("8PFZNjn19BBYVHNp4H31bEW7eAmu78Yf2RKV8EeA461K").unwrap(), - from, - from_owner, - wrapped_mint_key, - Pubkey::from_str("GUvmRrbZcB6TkDZDYJ5zbZ1bNdRj9QGfuZQDgkCNhgyA").unwrap(), - Pubkey::from_str("3VFdJkFuzrcwCwdxhKRETGxrDtUVAipNmYcLvRBDcQeH").unwrap(), - Pubkey::from_str("6bi4JGDoRwUs9TYBuvoA7dUVyikTJDrJsJU1ew6KVLiu").unwrap(), - message, - Pubkey::from_str("4yttKWzRoNYS2HekxDfcZYmfQqnVWpKiJ8eydYRuFRgs").unwrap(), - Pubkey::from_str("9QzqZZvhxoHzXbNY9y2hyAUfJUzDwyDb7fbDs9RXwH3").unwrap(), - Pubkey::from_str("7s3a1ycs16d6SNDumaRtjcoyMaTDZPavzgsmS3uUZYWX").unwrap(), - Pubkey::from_str("SysvarC1ock11111111111111111111111111111111").unwrap(), - Pubkey::from_str("SysvarRent111111111111111111111111111111111").unwrap(), - Pubkey::from_str("11111111111111111111111111111111").unwrap(), - core_bridge_id, - Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap(), - ]; - let expected_data = hex::decode("04fb28000040420f0000000000000000000000000000000000000000000000000017a8a2fc5df67f8fe497149504834ae5bc35c9130300").unwrap(); - let accounts: Vec = instruction.accounts.iter().map(|acc| acc.pubkey).collect(); - assert_eq!(expected_accounts, accounts); - assert_eq!(expected_data, instruction.data); -} - -#[test] -fn test_terra_address_from_string() { - // This is the address from the test transaction: - // https://github.com/ChorusOne/solido/issues/445#issuecomment-988002302. - assert_eq!( - TerraAddress::from_str("terra1z7529lza7elcleyhzj2sfq62uk7rtjgnrqeuxr"), - Ok(TerraAddress([ - 0x17, 0xa8, 0xa2, 0xfc, 0x5d, 0xf6, 0x7f, 0x8f, 0xe4, 0x97, 0x14, 0x95, 0x04, 0x83, - 0x4a, 0xe5, 0xbc, 0x35, 0xc9, 0x13 - ])), - ); -} - -#[test] -fn test_terra_address_to_string() { - // This is the address from the test transaction: - // https://github.com/ChorusOne/solido/issues/445#issuecomment-988002302. - assert_eq!( - TerraAddress([ - 0x17, 0xa8, 0xa2, 0xfc, 0x5d, 0xf6, 0x7f, 0x8f, 0xe4, 0x97, 0x14, 0x95, 0x04, 0x83, - 0x4a, 0xe5, 0xbc, 0x35, 0xc9, 0x13 - ]) - .to_string(), - "terra1z7529lza7elcleyhzj2sfq62uk7rtjgnrqeuxr", - ); -} - -#[test] -fn terra_address_to_foreign_left_pads_with_zeros() { - assert_eq!( - TerraAddress([ - 0x17, 0xa8, 0xa2, 0xfc, 0x5d, 0xf6, 0x7f, 0x8f, 0xe4, 0x97, 0x14, 0x95, 0x04, 0x83, - 0x4a, 0xe5, 0xbc, 0x35, 0xc9, 0x13 - ]) - .to_foreign(), - ForeignAddress([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0xa8, - 0xa2, 0xfc, 0x5d, 0xf6, 0x7f, 0x8f, 0xe4, 0x97, 0x14, 0x95, 0x04, 0x83, 0x4a, 0xe5, - 0xbc, 0x35, 0xc9, 0x13 - ]) - ); -} diff --git a/anker/tests/mod.rs b/anker/tests/mod.rs deleted file mode 100644 index 5dabb00e4..000000000 --- a/anker/tests/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -// The actual tests all live as modules in the `tests` directory. -// Without this, `cargo test-bpf` tries to build every top-level -// file as a separate binary, which then causes -// -// * Every build error in a shared file to be reported once per file that uses it. -// * Unused function warnings for the helpers that do not get used in *every* module. -// * Rather verbose test output, with one section per binary. -// -// By putting everything in a single module, we sidestep this problem. -pub mod tests; diff --git a/anker/tests/tests/amm.rs b/anker/tests/tests/amm.rs deleted file mode 100644 index 1858e5ee7..000000000 --- a/anker/tests/tests/amm.rs +++ /dev/null @@ -1,39 +0,0 @@ -use anker::token::MicroUst; -use lido::token::{Lamports, StLamports}; -use solana_program_test::tokio; -use solana_sdk::signer::Signer; -use testlib::anker_context::Context; - -#[tokio::test] -async fn test_successful_token_swap() { - let mut context = Context::new().await; - context - .token_pool_context - .initialize_token_pool(&mut context.solido_context) - .await; - let (st_sol_keypair, st_sol_token) = context - .solido_context - .deposit(Lamports(10_000_000_000)) - .await; - - let ust_address = context - .create_ust_token_account(st_sol_keypair.pubkey()) - .await; - - let amount_in = StLamports(1_000_000_000); - let min_amount_out = MicroUst(0); - context - .swap_st_sol_for_ust( - &st_sol_token, - &ust_address, - &st_sol_keypair, - amount_in, - min_amount_out, - ) - .await; - - let ust_balance = context.get_ust_balance(ust_address).await; - // For the constant product AMM: - // 10000 - (10*10000 / 11) = 909.0909090909099 - assert_eq!(ust_balance, MicroUst(909_090_909)); -} diff --git a/anker/tests/tests/deposit.rs b/anker/tests/tests/deposit.rs deleted file mode 100644 index 19b725c06..000000000 --- a/anker/tests/tests/deposit.rs +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use anker::error::AnkerError; -use anker::token::BLamports; -use lido::token::{Lamports, StLamports}; -use solana_program_test::tokio; -use solana_sdk::account::WritableAccount; -use solana_sdk::signer::Signer; -use testlib::anker_context::Context; -use testlib::assert_solido_error; - -const TEST_DEPOSIT_AMOUNT: StLamports = StLamports(1_000_000_000); - -#[tokio::test] -async fn test_successful_deposit_during_first_epoch() { - let mut context = Context::new_with_undefined_exchange_rate().await; - let (_owner, recipient) = context.deposit(Lamports(TEST_DEPOSIT_AMOUNT.0)).await; - - let reserve_balance = context - .solido_context - .get_st_sol_balance(context.st_sol_reserve) - .await; - let recipient_balance = context.get_b_sol_balance(recipient).await; - - // If there is no deposit yet, the exchange rate is defined to be 1:1, - // so the amounts in SOL, stSOL, and bSOL are all equal. - assert_eq!(reserve_balance, TEST_DEPOSIT_AMOUNT); - assert_eq!(recipient_balance, BLamports(TEST_DEPOSIT_AMOUNT.0)); - let anker = context.get_anker().await; - assert_eq!( - anker.metrics.deposit_metric.st_sol_total, - TEST_DEPOSIT_AMOUNT - ); - assert_eq!( - anker.metrics.deposit_metric.b_sol_total, - BLamports(TEST_DEPOSIT_AMOUNT.0) - ); - assert_eq!(anker.metrics.deposit_metric.count, 1); -} - -#[tokio::test] -async fn test_successful_deposit_after_first_epoch() { - let mut context = Context::new().await; - let (_owner, recipient) = context.deposit(Lamports(TEST_DEPOSIT_AMOUNT.0)).await; - - let reserve_balance = context - .solido_context - .get_st_sol_balance(context.st_sol_reserve) - .await; - let recipient_balance = context.get_b_sol_balance(recipient).await; - - // The context starts Solido with 1:1 exchange rate. - assert_eq!(reserve_balance, TEST_DEPOSIT_AMOUNT); - assert_eq!(recipient_balance, BLamports(TEST_DEPOSIT_AMOUNT.0)); -} - -#[tokio::test] -async fn test_successful_deposit_different_exchange_rate() { - let mut context = Context::new_different_exchange_rate(Lamports(1_000_000_000)).await; - let (_owner, recipient) = context.deposit(Lamports(TEST_DEPOSIT_AMOUNT.0)).await; - let reserve_balance = context - .solido_context - .get_st_sol_balance(context.st_sol_reserve) - .await; - let recipient_balance = context.get_b_sol_balance(recipient).await; - - // The exchange rate is now 1:2. - assert_eq!(reserve_balance, StLamports(500_000_000)); - assert_eq!(recipient_balance, BLamports(TEST_DEPOSIT_AMOUNT.0)); -} - -#[tokio::test] -async fn test_deposit_fails_with_wrong_reserve() { - let mut context = Context::new().await; - - let fake_reserve = context.solido_context.deterministic_keypair.new_keypair(); - context.st_sol_reserve = fake_reserve.pubkey(); - - // The program should confirm that the reserve we use is the reserve of the - // instance, and fail the transaction if it's a different account. Otherwise - // we could pass in a reserve controlled by us (where we are an attacker), and - // get bSOL while also retaining the stSOL. - let result = context.try_deposit(Lamports(TEST_DEPOSIT_AMOUNT.0)).await; - assert_solido_error!(result, AnkerError::InvalidDerivedAccount); -} - -#[tokio::test] -async fn test_deposit_fails_with_wrong_instance_address() { - let mut context = Context::new().await; - - let real_account = context.solido_context.get_account(context.anker).await; - - // Make a copy of the Anker instance, but put it at a different address. - let fake_addr = context.solido_context.deterministic_keypair.new_keypair(); - let mut fake_account_shared = solana_sdk::account::AccountSharedData::new( - real_account.lamports, - real_account.data.len(), - &real_account.owner, - ); - fake_account_shared.set_rent_epoch(real_account.rent_epoch); - fake_account_shared.set_data(real_account.data.clone()); - context - .solido_context - .context - .set_account(&fake_addr.pubkey(), &fake_account_shared); - - // Confirm that we succeeded to make a copy. Only the addresses should differ. - let fake_account = context.solido_context.get_account(fake_addr.pubkey()).await; - assert_eq!(real_account, fake_account); - - // Then poison our context to make it pass the wrong instance. - context.anker = fake_addr.pubkey(); - - // Depositing should now fail, because the instance does not live at the - // right address. - let result = context.try_deposit(Lamports(TEST_DEPOSIT_AMOUNT.0)).await; - assert_solido_error!(result, AnkerError::InvalidDerivedAccount); -} diff --git a/anker/tests/tests/fetch_pool_price.rs b/anker/tests/tests/fetch_pool_price.rs deleted file mode 100644 index b2633a6f1..000000000 --- a/anker/tests/tests/fetch_pool_price.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use anker::{ - error::AnkerError, - state::{HistoricalStSolPrice, POOL_PRICE_MIN_SAMPLE_DISTANCE, POOL_PRICE_NUM_SAMPLES}, - token::MicroUst, -}; -use lido::token::{Lamports, StLamports}; -use solana_program::clock::DEFAULT_SLOTS_PER_EPOCH; -use solana_program_test::tokio; -use solana_sdk::signer::Signer; -use testlib::{anker_context::Context, assert_solido_error}; - -const DEPOSIT_AMOUNT: u64 = 1_000_000_000; // 1e9 units - -#[tokio::test] -async fn test_successful_fetch_pool_price() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - for epoch in 2..2 + POOL_PRICE_NUM_SAMPLES as u64 { - context.solido_context.advance_to_normal_epoch(epoch); - context.fetch_pool_price().await; - } - let anker = context.get_anker().await; - - // Initially, there are 10 stSOL and 10_000 UST in the pool. - // for maintaining the constant product k = 10 * 10_000 = 100_000. - // When selling 1 StSOL we should maintain the equality: - // (10 + 1) * (10_000 - x) = k, x = 909.0909090909091 - let current_ust_price = MicroUst(909_090_909); - - let mut expected_historical_st_sol_prices = (0..5) - .map(|i| HistoricalStSolPrice { - slot: 1388256 + i * DEFAULT_SLOTS_PER_EPOCH, - st_sol_price_in_ust: current_ust_price, - }) - .collect::>(); - - assert_eq!( - anker.historical_st_sol_prices.0[..], - expected_historical_st_sol_prices - ); - - expected_historical_st_sol_prices.rotate_left(1); - expected_historical_st_sol_prices[POOL_PRICE_NUM_SAMPLES - 1] = HistoricalStSolPrice { - slot: 3548256, - st_sol_price_in_ust: MicroUst(909090909), - }; - context - .solido_context - .advance_to_normal_epoch(2 + POOL_PRICE_NUM_SAMPLES as u64); - context.fetch_pool_price().await; - let anker = context.get_anker().await; - assert_eq!( - anker.historical_st_sol_prices.0[..], - expected_historical_st_sol_prices - ); -} - -#[tokio::test] -async fn test_fetch_pool_price_when_price_changed() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - - // Deposit some tokens so we have StSol - let (st_sol_keypair, st_sol_token) = context - .solido_context - .deposit(Lamports(10_000_000_000)) - .await; - let ust_address = context - .create_ust_token_account(st_sol_keypair.pubkey()) - .await; - - context.solido_context.advance_to_normal_epoch(2); - context.fetch_pool_price().await; - - let amount_in = StLamports(1_000_000_000); - let min_amount_out = MicroUst(0); - context - .swap_st_sol_for_ust( - &st_sol_token, - &ust_address, - &st_sol_keypair, - amount_in, - min_amount_out, - ) - .await; - - context.solido_context.advance_to_normal_epoch(3); - context.fetch_pool_price().await; - - let anker = context.get_anker().await; - assert_eq!( - anker.historical_st_sol_prices.0[POOL_PRICE_NUM_SAMPLES - 2], - HistoricalStSolPrice { - slot: 1388256, - st_sol_price_in_ust: MicroUst(909_090_909) - } - ); - - // There are 11 stSOL and 9090_909_091 UST in the pool. - // for maintaining the constant product k = 11 * 9090.909_091 = 100_000. - // When selling 1 StSOL we should maintain the equality: - // (11 + 1) * (9090.909091 - x) = k, x = 757.5757576666656 - - assert_eq!( - anker.historical_st_sol_prices.0[POOL_PRICE_NUM_SAMPLES - 1], - HistoricalStSolPrice { - slot: 1820256, - st_sol_price_in_ust: MicroUst(757_575_757) - } - ); -} - -#[tokio::test] -async fn test_fail_fetch_pool_price_too_early() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - context.fetch_pool_price().await; - context - .solido_context - .context - .warp_to_slot(956256 + POOL_PRICE_MIN_SAMPLE_DISTANCE - 1) - .expect("Failed to warp to slot"); - let result = context.try_fetch_pool_price().await; - assert_solido_error!(result, AnkerError::FetchPoolPriceTooEarly); - context - .solido_context - .context - .warp_to_slot(956256 + POOL_PRICE_MIN_SAMPLE_DISTANCE + 1) - .expect("Failed to warp to slot"); - context.fetch_pool_price().await; -} diff --git a/anker/tests/tests/manager.rs b/anker/tests/tests/manager.rs deleted file mode 100644 index 856f67873..000000000 --- a/anker/tests/tests/manager.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::str::FromStr; - -use anker::{error::AnkerError, wormhole::TerraAddress}; -use lido::{error::LidoError, token::Lamports}; -use solana_program::pubkey::Pubkey; -use solana_program_test::tokio; -use solana_sdk::{signature::Keypair, signer::Signer}; -use testlib::{ - anker_context::{setup_token_pool, Context}, - assert_solido_error, -}; - -const DEPOSIT_AMOUNT: Lamports = Lamports(1_000_000_000); - -#[tokio::test] -async fn test_successful_change_token_swap_pool() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(DEPOSIT_AMOUNT) - .await; - let mut new_token_pool = setup_token_pool(&mut context.solido_context).await; - - // Copy UST token info from original Token Swap pool. - new_token_pool.ust_mint_address = context.token_pool_context.ust_mint_address; - let ust_mint_authority = - Keypair::from_bytes(&context.token_pool_context.ust_mint_authority.to_bytes()).unwrap(); - new_token_pool.ust_mint_authority = ust_mint_authority; - new_token_pool.token_a = context - .solido_context - .create_spl_token_account( - new_token_pool.ust_mint_address, - new_token_pool.get_authority(), - ) - .await; - - new_token_pool - .initialize_token_pool(&mut context.solido_context) - .await; - let new_token_pool_address = new_token_pool.swap_account.pubkey(); - let result = context - .try_change_token_swap_pool(new_token_pool_address) - .await; - assert!(result.is_ok()); - let anker = context.get_anker().await; - assert_eq!(anker.token_swap_pool, new_token_pool_address); -} - -#[tokio::test] -async fn test_change_token_swap_pool_invalid_pool() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(DEPOSIT_AMOUNT) - .await; - let new_token_swap = Pubkey::new_unique(); - let result = context.try_change_token_swap_pool(new_token_swap).await; - assert_solido_error!(result, AnkerError::WrongSplTokenSwapParameters); -} - -#[tokio::test] -async fn test_change_token_swap_pool_different_minters() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(DEPOSIT_AMOUNT) - .await; - let mut new_token_pool = setup_token_pool(&mut context.solido_context).await; - new_token_pool - .initialize_token_pool(&mut context.solido_context) - .await; - let new_token_pool_address = new_token_pool.swap_account.pubkey(); - - let result = context - .try_change_token_swap_pool(new_token_pool_address) - .await; - assert_solido_error!(result, AnkerError::WrongSplTokenSwapParameters); -} - -#[tokio::test] -async fn test_change_token_swap_pool_different_manager() { - let mut context = Context::new().await; - // Token pool doesn't matter, and can be left uninitialized/invalid, as the - // manager is evaluated earlier. - let new_token_swap = Pubkey::new_unique(); - context.solido_context.manager = context.solido_context.deterministic_keypair.new_keypair(); - let anker = context.get_anker().await; - let result = context.try_change_token_swap_pool(new_token_swap).await; - assert_solido_error!(result, LidoError::InvalidManager); - let new_anker = context.get_anker().await; - assert_eq!(anker.token_swap_pool, new_anker.token_swap_pool); -} - -#[tokio::test] -async fn test_successful_change_terra_rewards_destination() { - let mut context = Context::new().await; - let new_terra_rewards_address = - TerraAddress::from_str("terra1fex9f78reuwhfsnc8sun6mz8rl9zwqh03fhwf3").unwrap(); - let manager = Keypair::from_bytes(&context.solido_context.manager.to_bytes()).unwrap(); - let result = context - .try_change_terra_rewards_destination(&manager, new_terra_rewards_address.clone()) - .await; - assert!(result.is_ok()); - let anker = context.get_anker().await; - assert_eq!(anker.terra_rewards_destination, new_terra_rewards_address); -} - -#[tokio::test] -async fn test_change_terra_rewards_destination_different_manager() { - let mut context = Context::new().await; - let new_terra_rewards_address = - TerraAddress::from_str("terra1fex9f78reuwhfsnc8sun6mz8rl9zwqh03fhwf3").unwrap(); - let wrong_manager = context.solido_context.deterministic_keypair.new_keypair(); - let anker = context.get_anker().await; - let result = context - .try_change_terra_rewards_destination(&wrong_manager, new_terra_rewards_address) - .await; - assert_solido_error!(result, LidoError::InvalidManager); - let new_anker = context.get_anker().await; - assert_eq!( - anker.terra_rewards_destination, - new_anker.terra_rewards_destination - ); -} - -#[tokio::test] -async fn test_successful_change_sell_rewards_min_out_bps() { - let mut context = Context::new().await; - let sell_rewards_min_out_bps = 10; - let manager = Keypair::from_bytes(&context.solido_context.manager.to_bytes()).unwrap(); - let result = context - .try_change_sell_rewards_min_out_bps(&manager, sell_rewards_min_out_bps) - .await; - assert!(result.is_ok()); - let anker = context.get_anker().await; - assert_eq!(anker.sell_rewards_min_out_bps, sell_rewards_min_out_bps); -} - -#[tokio::test] -async fn test_change_sell_rewards_min_out_bps_more_than_100_percent() { - let mut context = Context::new().await; - let sell_rewards_min_out_bps = 10_001; - let manager = Keypair::from_bytes(&context.solido_context.manager.to_bytes()).unwrap(); - let result = context - .try_change_sell_rewards_min_out_bps(&manager, sell_rewards_min_out_bps) - .await; - assert_solido_error!(result, AnkerError::InvalidSellRewardsMinOutBps); -} - -#[tokio::test] -async fn test_change_sell_rewards_fake_manager() { - let mut context = Context::new().await; - let sell_rewards_min_out_bps = 10_001; - let manager = context.solido_context.deterministic_keypair.new_keypair(); - let result = context - .try_change_sell_rewards_min_out_bps(&manager, sell_rewards_min_out_bps) - .await; - assert_solido_error!(result, LidoError::InvalidManager); -} diff --git a/anker/tests/tests/mod.rs b/anker/tests/tests/mod.rs deleted file mode 100644 index ec0b03126..000000000 --- a/anker/tests/tests/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -#![cfg(feature = "test-bpf")] - -pub mod amm; -pub mod deposit; -pub mod fetch_pool_price; -pub mod manager; -pub mod sell_rewards; -pub mod send_rewards; -pub mod withdraw; diff --git a/anker/tests/tests/sell_rewards.rs b/anker/tests/tests/sell_rewards.rs deleted file mode 100644 index 7fe3a9498..000000000 --- a/anker/tests/tests/sell_rewards.rs +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use anker::{ - error::AnkerError, - state::{POOL_PRICE_MIN_SAMPLE_DISTANCE, POOL_PRICE_NUM_SAMPLES}, - token::MicroUst, -}; -use lido::token::{Lamports, StLamports}; -use solana_program::pubkey::Pubkey; -use solana_program_test::tokio; -use std::mem; -use testlib::{anker_context::Context, assert_solido_error}; - -const DEPOSIT_AMOUNT: u64 = 1_000_000_000; // 1e9 units - -#[tokio::test] -async fn test_fails_sell_rewards_if_not_enough_fetch_pool_price_calls() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - - // Test calling `fetch_pool_price` not enough times. - for _ in 0..POOL_PRICE_NUM_SAMPLES - 1 { - let current_slot = context.solido_context.get_clock().await.slot; - context.fetch_pool_price().await; - context - .solido_context - .context - .warp_to_slot(current_slot + POOL_PRICE_MIN_SAMPLE_DISTANCE) - .unwrap(); - let result = context.try_sell_rewards().await; - assert_solido_error!(result, AnkerError::FetchPoolPriceNotCalledRecently); - } - - context.fetch_pool_price().await; - - // After the final fetch, we still shouldn't be able to _immediately_ - // sell the rewards, but after MIN_SAMPLE_DISTANCE / 2, we should. - let current_slot = context.solido_context.get_clock().await.slot; - let result = context.try_sell_rewards().await; - assert_solido_error!(result, AnkerError::SellRewardsTooEarly); - - context - .solido_context - .context - .warp_to_slot(current_slot + POOL_PRICE_MIN_SAMPLE_DISTANCE / 2) - .unwrap(); - - // At this point, it should not yet be possible to fetch the price again, - // that is only allowed one slot later. But this slot we can sell the rewards. - let result = context.try_fetch_pool_price().await; - assert_solido_error!(result, AnkerError::FetchPoolPriceTooEarly); - - context.sell_rewards().await; -} - -#[tokio::test] -async fn test_successful_sell_rewards() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - - let anker_before = context.get_anker().await; - - context.fill_historical_st_sol_price_array().await; - context.sell_rewards().await; - - let anker_after = context.get_anker().await; - assert_eq!( - anker_after.metrics.swapped_rewards_st_sol_total - - anker_before.metrics.swapped_rewards_st_sol_total, - // Solido got initialized with 1 SOL. Then it got 10 stLamports for - // initializing the swap pool. Then we deposited 1 more SOL for use with - // Anker, and we donated 1 SOL to change the exchange rate. - // That would mean we have 13 SOL = 12 stSOL, and that would mean Anker's - // excess value is 1/12 = 0.0833 SOL. Converting that to stSOL yields - // 1/12 * 12/13 = 1/13 = 0.0769 stSOL. - Ok(StLamports(76_923_077)) - ); - assert_eq!( - anker_after.metrics.swapped_rewards_ust_total - - anker_before.metrics.swapped_rewards_ust_total, - // We started out the constant product pool with 10 stSOL = 10 UST, - // and we swapped a relatively small amount, so the amount we got out - // here in UST should be close to the amount in stSOL above. - Ok(MicroUst(76_335_877)) - ); - - let ust_balance = context.get_ust_balance(context.ust_reserve).await; - // Exchange rate is 12 stSol : 13 Sol - // We have 1 stSOL, our rewards were 1 - (1 * 12/13) = 0.076923077 - // Initially there are 10 StSol and 10_000 UST in the AMM - // We should get 10000 - (10*10000 / 10.076923077) = 76.33587793834886 UST - assert_eq!(ust_balance, MicroUst(76_335_877)); - // Test claiming the reward again fails. - let result = context.try_sell_rewards().await; - assert_solido_error!(result, AnkerError::ZeroRewardsToClaim); -} - -// Create a token pool where the token a and b are swapped (what matters is that -// they are stSOL and UST), the order shouldn't make a difference. -#[tokio::test] -async fn test_successful_sell_rewards_pool_a_b_token_swapped() { - let mut context = Context::new().await; - // Swap the tokens a and b on Token Swap creation. - mem::swap( - &mut context.token_pool_context.token_a, - &mut context.token_pool_context.token_b, - ); - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - context.fill_historical_st_sol_price_array().await; - context.sell_rewards().await; - - let ust_balance = context.get_ust_balance(context.ust_reserve).await; - assert_eq!(ust_balance, MicroUst(76_335_877)); -} - -#[tokio::test] -async fn test_sell_rewards_fails_with_different_reserve() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - context.fill_historical_st_sol_price_array().await; - - context.ust_reserve = context.create_ust_token_account(Pubkey::new_unique()).await; - - let result = context.try_sell_rewards().await; - assert_solido_error!(result, AnkerError::InvalidDerivedAccount); -} - -#[tokio::test] -async fn test_sell_rewards_fails_with_different_token_swap_program() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - context.fill_historical_st_sol_price_array().await; - - // If we try to call `SellRewards`, but the swap program is not the owner of - // the pool, that should fail. - context.token_swap_program_id = anker::orca_token_swap_v2_fake::id(); - let result = context.try_sell_rewards().await; - - assert_solido_error!(result, AnkerError::WrongSplTokenSwap); -} diff --git a/anker/tests/tests/send_rewards.rs b/anker/tests/tests/send_rewards.rs deleted file mode 100644 index b67a1fba8..000000000 --- a/anker/tests/tests/send_rewards.rs +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use solana_program::instruction::InstructionError; -use solana_program_test::tokio; -use solana_sdk::transaction::TransactionError; -use solana_sdk::transport::TransportError; - -use lido::token::Lamports; -use testlib::anker_context::Context; - -const DEPOSIT_AMOUNT: u64 = 1_000_000_000; // 1e9 units - -#[tokio::test] -async fn test_send_rewards_does_not_overflow_stack() { - let mut context = Context::new().await; - context - .initialize_token_pool_and_deposit(Lamports(DEPOSIT_AMOUNT)) - .await; - context.fill_historical_st_sol_price_array().await; - context.sell_rewards().await; - - let result = context.try_send_rewards().await; - - match result { - // An access violation caused by a stack overflow results in this error. - Err(TransportError::TransactionError(TransactionError::InstructionError( - 0, - InstructionError::ProgramFailedToComplete, - ))) => { - panic!("Did the program overflow the stack?") - } - Err(TransportError::TransactionError(TransactionError::InstructionError( - 0, - InstructionError::AccountNotExecutable, - ))) => { - // This error is expected, we try to call a dummy address where the - // Wormhole program is supposed to live, but that dummy address is - // not executable. If we get here, it means we executed the entire - // `SendRewards` instruction aside from the final call to the - // Wormhole progrma. In particular we know that we didn't overflow - // the stack. - } - Ok(()) => panic!("This should not have passed without the Wormhole program present."), - Err(err) => { - panic!("Unexpected error: {:?}", err); - } - } -} diff --git a/anker/tests/tests/withdraw.rs b/anker/tests/tests/withdraw.rs deleted file mode 100644 index 9c8e6bc49..000000000 --- a/anker/tests/tests/withdraw.rs +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use anker::error::AnkerError; -use anker::token::BLamports; -use borsh::BorshSerialize; -use lido::state::Lido; -use lido::token::{Lamports, StLamports}; -use solana_program::borsh::try_from_slice_unchecked; -use solana_program_test::tokio; -use solana_sdk::account::WritableAccount; -use solana_sdk::signer::Signer; -use testlib::anker_context::Context; -use testlib::assert_solido_error; - -const TEST_DEPOSIT_AMOUNT: Lamports = Lamports(1_000_000_000); - -#[tokio::test] -async fn test_withdraw_single_epoch() { - let mut context = Context::new().await; - - // Deposit some SOL into Solido, then put that in Anker. - let (owner, b_sol_recipient) = context.deposit(TEST_DEPOSIT_AMOUNT).await; - let b_sol_balance = context.get_b_sol_balance(b_sol_recipient).await; - - // We now own all bSOL in existence. - let b_sol_supply = context.get_b_sol_supply().await; - assert_eq!(b_sol_balance, b_sol_supply); - - // Withdraw the full amount from Anker again. - let st_sol_recipient = context - .withdraw(&owner, b_sol_recipient, b_sol_balance) - .await; - - // After withdrawing, no bSOL exists any more. - let b_sol_supply = context.get_b_sol_supply().await; - assert_eq!(b_sol_supply, BLamports(0)); - - // The SOL value of that stSOL should be the same as what we put in. - let st_sol_balance = context - .solido_context - .get_st_sol_balance(st_sol_recipient) - .await; - let sol_value = context.exchange_st_sol(st_sol_balance).await; - assert_eq!(sol_value, TEST_DEPOSIT_AMOUNT); - - // The reserve should now be empty, we withdrew everything. - let reserve_st_sol = context - .solido_context - .get_st_sol_balance(context.st_sol_reserve) - .await; - assert_eq!(reserve_st_sol, StLamports(0)); - let anker = context.get_anker().await; - assert_eq!(anker.metrics.withdraw_metric.st_sol_total, st_sol_balance); - assert_eq!(anker.metrics.withdraw_metric.b_sol_total, b_sol_balance); - assert_eq!(anker.metrics.withdraw_metric.count, 1); -} - -#[tokio::test] -async fn test_withdraw_after_st_sol_price_increase() { - let mut context = Context::new().await; - - // Deposit some SOL into Solido, then put that in Anker. - let (owner, b_sol_recipient) = context.deposit(TEST_DEPOSIT_AMOUNT).await; - let b_sol_balance = context.get_b_sol_balance(b_sol_recipient).await; - - // Donate some SOL to Solido to simulate rewards, and warp to the next epoch - // to pick up the new exchange rate. - let donation = Lamports(1_000_000_000); - context - .solido_context - .fund(context.solido_context.reserve_address, donation) - .await; - context.solido_context.advance_to_normal_epoch(1); - context.solido_context.update_exchange_rate().await; - - // We now own all bSOL in existence. - let b_sol_supply = context.get_b_sol_supply().await; - assert_eq!(b_sol_balance, b_sol_supply); - - // Withdraw the full amount from Anker again. - let st_sol_recipient = context - .withdraw(&owner, b_sol_recipient, b_sol_balance) - .await; - - // After withdrawing, no bSOL exists any more. - let b_sol_supply = context.get_b_sol_supply().await; - assert_eq!(b_sol_supply, BLamports(0)); - - // The SOL value of that stSOL should be the same as what we put in. - // One lamport is lost due to rounding errors though. - let st_sol_balance = context - .solido_context - .get_st_sol_balance(st_sol_recipient) - .await; - let sol_value = context.exchange_st_sol(st_sol_balance).await; - assert_eq!(sol_value, (TEST_DEPOSIT_AMOUNT - Lamports(1)).unwrap()); - - // Some stSOL should be left in the reserve: these are the staking rewards - // that the bSOL holder renounced by converting their stSOL into bSOL. Half - // of the stSOL is held by the initial Solido depositor set up by the test - // context, the other half of the stSOL was held by Anker at the time of the - // donation. So now the value of the reserve should be half the deposit. - // (Plus one lamport rounding error.) - let reserve_st_sol = context - .solido_context - .get_st_sol_balance(context.st_sol_reserve) - .await; - let reserve_sol = context.exchange_st_sol(reserve_st_sol).await; - assert_eq!(reserve_sol, Lamports(500_000_001)); -} - -#[tokio::test] -async fn test_withdraw_wrong_token_mint() { - let mut context = Context::new().await; - - let (owner, st_sol_account) = context - .solido_context - .deposit(Lamports(1_000_000_000)) - .await; - let b_sol_account = context - .try_deposit_st_sol(&owner, st_sol_account, StLamports(500_000_000)) - .await - .unwrap(); - - // Withdrawing with the wrong type of account should fail. We need to put in - // a bSOL account, not an stSOL account to withdraw from. - let result = context - .try_withdraw(&owner, st_sol_account, BLamports(250_000_000)) - .await; - assert_solido_error!(result, AnkerError::InvalidTokenMint); - - // With the right type of account, it should succeed. - context - .withdraw(&owner, b_sol_account, BLamports(250_000_000)) - .await; -} - -#[tokio::test] -async fn test_withdraw_after_st_sol_price_decrease() { - let mut context = Context::new().await; - - // Deposit some SOL into Solido, then put that in Anker. - let (owner, b_sol_recipient) = context.deposit(TEST_DEPOSIT_AMOUNT).await; - let b_sol_balance = context.get_b_sol_balance(b_sol_recipient).await; - - // Mutate the Solido instance and sabotage its exchange rate to make the - // value of stSOL go down. Normally this cannot happen, but if Solana would - // introduce slashing in the future, then it might. - context.solido_context.advance_to_normal_epoch(1); - context.solido_context.update_exchange_rate().await; - let mut solido_account = context - .solido_context - .get_account(context.solido_context.solido.pubkey()) - .await; - let mut solido = try_from_slice_unchecked::(solido_account.data.as_slice()).unwrap(); - // Set 1 stSOL = 0.5 SOL. - solido.exchange_rate.sol_balance = Lamports(1_000_000_000); - solido.exchange_rate.st_sol_supply = StLamports(2_000_000_000); - solido_account.data = BorshSerialize::try_to_vec(&solido).unwrap(); - let mut solido_account_shared = solana_sdk::account::AccountSharedData::new( - solido_account.lamports, - solido_account.data.len(), - &solido_account.owner, - ); - solido_account_shared.set_rent_epoch(solido_account.rent_epoch); - solido_account_shared.set_data(solido_account.data); - context.solido_context.context.set_account( - &context.solido_context.solido.pubkey(), - &solido_account_shared, - ); - - assert_eq!(b_sol_balance, BLamports(1_000_000_000)); - - // Withdraw 0.1 bSOL. - let st_sol_recipient = context - .withdraw(&owner, b_sol_recipient, BLamports(100_000_000)) - .await; - - // We put in 1 SOL, converted it to stSOL, then to bSOL. - // Then the value of stSOL went down by 50%. This breaks the peg, even though - // we have 1 bSOL, we can at best withdraw 0.5 SOL now. To make the test - // more interesting, if we tried to withdraw the full 1 bSOL and we forgot - // to use the right exchange rate, there is not enough stSOL in existence - // and the transaction would fail, but if we only withdraw 0.1 bSOL, then - // if we used the wrong exchange rate, we would get 0.2 stSOL, which we have. - let st_sol_balance = context - .solido_context - .get_st_sol_balance(st_sol_recipient) - .await; - // The SOL value of our withdraw is half of the bSOL amount, because the peg - // is broken. - let sol_value = context.exchange_st_sol(st_sol_balance).await; - assert_eq!(sol_value, Lamports(50_000_000)); -} diff --git a/audit/2022-10-13-neodyme.pdf b/audit/2022-10-13-neodyme.pdf new file mode 100644 index 000000000..a53ac0397 Binary files /dev/null and b/audit/2022-10-13-neodyme.pdf differ diff --git a/buildimage.sh b/buildimage.sh index 287e57b9b..b29a28c88 100755 --- a/buildimage.sh +++ b/buildimage.sh @@ -5,10 +5,9 @@ # 1. Get last commit hash VERSION=$(git rev-parse --short HEAD) -TAG="chorusone/solido:$VERSION" -BASETAG="chorusone/solido-base" -SOLIPATH="/root/.local/share/solana/install/releases/1.8.16/solana-release/bin/solido" - +TAG="guyos/solido:$VERSION" +BASETAG="guyos/solido-base" +SOLIPATH="/root/.local/share/solana/install/releases/1.9.28/solana-release/bin/solido" # 2. Build container image echo "Building container image $TAG" @@ -32,7 +31,7 @@ echo "Running container id is=$CON_ID" #6. Copy artefacts locally ## a. on-chain -programs=("lido" "serum_multisig" "anker") +programs=("lido" "serum_multisig") for i in "${programs[@]}" do echo "Copying $i program and hash" diff --git a/cli/common/Cargo.toml b/cli/common/Cargo.toml index bde233eb8..3d6f7ebb0 100644 --- a/cli/common/Cargo.toml +++ b/cli/common/Cargo.toml @@ -4,11 +4,10 @@ description = "Solido common client implementation" license = "GPL-3.0" edition = "2018" name = "solido-cli-common" -version = "1.3.3" +version = "1.3.6" [dependencies] anchor-lang = "0.13.0" -anker = { path = "../../anker", features = ["no-entrypoint"] } bincode = "1.3" lido = { path = "../../program", features = ["no-entrypoint"] } num-traits = "0.2" @@ -25,3 +24,4 @@ solana-vote-program = "1.9.28" solana-transaction-status = "1.9.28" spl-token = "3.1.1" rusqlite = "0.26.3" +borsh = "0.9.3" diff --git a/cli/common/src/error.rs b/cli/common/src/error.rs index 0f5b78ea4..44459235b 100644 --- a/cli/common/src/error.rs +++ b/cli/common/src/error.rs @@ -15,7 +15,7 @@ use solana_sdk::signer::presigner::PresignerError; use solana_sdk::signer::SignerError; use solana_sdk::transaction::TransactionError; -use anker::error::AnkerError; +use crate::snapshot::SnapshotError; use lido::error::LidoError; /// Return whether the transaction may have executed despite the client error. @@ -369,10 +369,6 @@ pub fn print_pretty_error_code(error_code: u32) { println!(" Error {} is not a known Multisig error.", error_code); } } - match AnkerError::from_u32(error_code) { - Some(err) => println!(" Anker error {} is {:?}", error_code, err), - None => println!(" Error {} is not a known Anker error.", error_code), - } } impl AsPrettyError for ProgramError { @@ -504,6 +500,19 @@ impl AsPrettyError for ParseHashError { } } +impl AsPrettyError for SnapshotError { + fn print_pretty(&self) { + print_red("Lido snapshot error:"); + match self { + Self::MissingAccount => println!(" Missing account"), + Self::MissingValidatorIdentity(pubkey) => { + println!(" Missing validator identity for {}", pubkey) + } + Self::OtherError(err) => err.print_pretty(), + }; + } +} + impl AsPrettyError for Box { fn print_pretty(&self) { (**self).print_pretty() diff --git a/cli/common/src/prometheus.rs b/cli/common/src/prometheus.rs index ea35e13ce..3b4ead516 100644 --- a/cli/common/src/prometheus.rs +++ b/cli/common/src/prometheus.rs @@ -5,7 +5,6 @@ //! //! See also . -use anker::token::{BLamports, MicroUst}; use lido::metrics::{LamportsHistogram, Metrics}; use lido::token::{Lamports, StLamports}; use std::io; @@ -91,18 +90,6 @@ impl<'a> Metric<'a> { Metric::new(MetricValue::Nano(amount.0)) } - /// Construct a metric that measures an amount of UST. - pub fn new_ust(amount: MicroUst) -> Metric<'a> { - // One microUst is 1e-6 UST, so we use micro here. - Metric::new(MetricValue::Micro(amount.0)) - } - - /// Construct a metric that measures an amount of bSOL. - pub fn new_b_sol(amount: BLamports) -> Metric<'a> { - // One bLamports is 1e-9 bSOL, so we use nano here. - Metric::new(MetricValue::Nano(amount.0)) - } - /// Set the timestamp. pub fn at(mut self, at: SystemTime) -> Metric<'a> { self.timestamp = Some(at); @@ -326,91 +313,6 @@ pub fn write_solido_metrics_as_prometheus( Ok(()) } -pub fn write_anker_metrics_as_prometheus( - metrics: &anker::metrics::Metrics, - at: SystemTime, - out: &mut W, -) -> io::Result<()> { - write_metric( - out, - &MetricFamily { - name: "anker_swapped_rewards_st_sol_total", - help: "Total amount of stSOL rewards swapped by our Anker instance.", - type_: "gauge", - metrics: vec![Metric::new_st_sol(metrics.swapped_rewards_st_sol_total).at(at)], - }, - )?; - write_metric( - out, - &MetricFamily { - name: "anker_swapped_rewards_ust_total", - help: "Total amount of UST rewards swapped by our Anker instance.", - type_: "gauge", - metrics: vec![Metric::new_ust(metrics.swapped_rewards_ust_total).at(at)], - }, - )?; - - // Deposit metrics - write_metric( - out, - &MetricFamily { - name: "anker_deposit_st_sol_total", - help: "Total amount stSOL deposited into Anker", - type_: "gauge", - metrics: vec![Metric::new_st_sol(metrics.deposit_metric.st_sol_total).at(at)], - }, - )?; - write_metric( - out, - &MetricFamily { - name: "anker_deposit_b_sol_total", - help: "Total amount bSOL minted in response to a deposit into Anker", - type_: "gauge", - metrics: vec![Metric::new_b_sol(metrics.deposit_metric.b_sol_total).at(at)], - }, - )?; - write_metric( - out, - &MetricFamily { - name: "anker_deposit_count_total", - help: "Total number of deposits made by users on Anker.", - type_: "gauge", - metrics: vec![Metric::new(metrics.deposit_metric.count).at(at)], - }, - )?; - - // Withdraw metrics - write_metric( - out, - &MetricFamily { - name: "anker_withdraw_st_sol_total", - help: "Total amount of stSOL withdrawn in response from burning bSOL from Anker", - type_: "gauge", - metrics: vec![Metric::new_st_sol(metrics.withdraw_metric.st_sol_total).at(at)], - }, - )?; - write_metric( - out, - &MetricFamily { - name: "anker_withdraw_b_sol_total", - help: "Total amount of bSOL burned in response from withdrawing stSOL from Anker", - type_: "gauge", - metrics: vec![Metric::new_b_sol(metrics.withdraw_metric.b_sol_total).at(at)], - }, - )?; - write_metric( - out, - &MetricFamily { - name: "anker_withdraw_count_total", - help: "Total number of withdrawals made by users on Anker.", - type_: "gauge", - metrics: vec![Metric::new(metrics.withdraw_metric.count).at(at)], - }, - )?; - - Ok(()) -} - #[cfg(test)] mod test { use std::str; diff --git a/cli/common/src/snapshot.rs b/cli/common/src/snapshot.rs index f1e723a0d..574726622 100644 --- a/cli/common/src/snapshot.rs +++ b/cli/common/src/snapshot.rs @@ -46,8 +46,7 @@ use solana_sdk::transaction::Transaction; use solana_transaction_status::{TransactionDetails, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteState; -use anker::state::Anker; -use lido::state::Lido; +use lido::state::{AccountList, Lido, ListEntry}; use lido::token::Lamports; use spl_token::solana_program::hash::Hash; @@ -217,6 +216,16 @@ impl<'a> Snapshot<'a> { } } + /// Get list of accounts of type T from Solido + pub fn get_account_list( + &mut self, + address: &Pubkey, + ) -> crate::Result> { + let list_account = self.get_account(address)?; + let mut data = list_account.data.to_vec(); + AccountList::from(&mut data).map_err(|e| e.into()) + } + /// Read an account and immediately bincode-deserialize it. pub fn get_bincode(&mut self, address: &Pubkey) -> crate::Result { let account = self.get_account(address)?; @@ -358,25 +367,6 @@ impl<'a> Snapshot<'a> { } } - /// Read the account and deserialize the Anker struct. - pub fn get_anker(&mut self, anker_address: &Pubkey) -> crate::Result { - let account = self.get_account(anker_address)?; - match try_from_slice_unchecked::(&account.data) { - Ok(anker) => Ok(anker), - Err(err) => { - let error: Error = Box::new(SerializationError { - cause: Some(err.into()), - address: *anker_address, - context: format!( - "Failed to deserialize Anker struct, data length is {} bytes.", - account.data.len() - ), - }); - Err(error.into()) - } - } - } - /// Return the amount in an SPL token account. pub fn get_spl_token_balance(&mut self, address: &Pubkey) -> crate::Result { let account: spl_token::state::Account = self.get_unpack(address)?; @@ -495,7 +485,11 @@ pub struct SnapshotClient { /// on their validator. At the time of writing, it defaults to 100. fn is_too_many_inputs_error(error: &ClientError) -> bool { match error.kind() { - ClientErrorKind::RpcError(RpcError::RpcRequestError(message)) => { + ClientErrorKind::RpcError(RpcError::RpcResponseError { + code: _, + message, + data: _, + }) => { // Unfortunately, there is no way to get a structured error; all we // get is a string that looks like this: // diff --git a/cli/listener/Cargo.toml b/cli/listener/Cargo.toml index 542a3e435..e71fba3a1 100644 --- a/cli/listener/Cargo.toml +++ b/cli/listener/Cargo.toml @@ -4,7 +4,7 @@ description = "Solido utility for indexing price data" license = "GPL-3.0" edition = "2018" name = "listener" -version = "1.3.3" +version = "1.3.6" [dependencies] rusqlite = "0.26.3" diff --git a/cli/listener/fuzz/Cargo.lock b/cli/listener/fuzz/Cargo.lock index ab6ba5832..c13ebb2be 100644 --- a/cli/listener/fuzz/Cargo.lock +++ b/cli/listener/fuzz/Cargo.lock @@ -164,7 +164,7 @@ dependencies = [ "anchor-attribute-state", "anchor-derive-accounts", "base64 0.13.0", - "borsh 0.9.3", + "borsh", "bytemuck", "solana-program", "thiserror", @@ -188,21 +188,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "anker" -version = "1.3.3" -dependencies = [ - "bech32", - "borsh 0.9.3", - "lido", - "num-derive", - "num-traits", - "serde", - "solana-program", - "spl-token", - "spl-token-swap", -] - [[package]] name = "ansi_term" version = "0.12.1" @@ -301,12 +286,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bech32" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" - [[package]] name = "bincode" version = "1.3.3" @@ -361,85 +340,29 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" -[[package]] -name = "borsh" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b13fa9bf62be34702e5ee4526aff22530ae22fe34a0c4290d30d5e4e782e6" -dependencies = [ - "borsh-derive 0.7.2", -] - [[package]] name = "borsh" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" dependencies = [ - "borsh-derive 0.9.3", + "borsh-derive", "hashbrown", ] -[[package]] -name = "borsh-derive" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6aaa45f8eec26e4bf71e7e5492cf53a91591af8f871f422d550e7cc43f6b927" -dependencies = [ - "borsh-derive-internal 0.7.2", - "borsh-schema-derive-internal 0.7.2", - "proc-macro2 1.0.39", - "syn 1.0.96", -] - -[[package]] -name = "borsh-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307f3740906bac2c118a8122fe22681232b244f1369273e45f1156b45c43d2dd" -dependencies = [ - "borsh-derive-internal 0.8.2", - "borsh-schema-derive-internal 0.8.2", - "proc-macro-crate 0.1.5", - "proc-macro2 1.0.39", - "syn 1.0.96", -] - [[package]] name = "borsh-derive" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" dependencies = [ - "borsh-derive-internal 0.9.3", - "borsh-schema-derive-internal 0.9.3", + "borsh-derive-internal", + "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", "proc-macro2 1.0.39", "syn 1.0.96", ] -[[package]] -name = "borsh-derive-internal" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61621b9d3cca65cc54e2583db84ef912d59ae60d2f04ba61bc0d7fc57556bda2" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2104c73179359431cc98e016998f2f23bc7a05bc53e79741bcba705f30047bc" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - [[package]] name = "borsh-derive-internal" version = "0.9.3" @@ -451,28 +374,6 @@ dependencies = [ "syn 1.0.96", ] -[[package]] -name = "borsh-schema-derive-internal" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b38abfda570837b0949c2c7ebd31417e15607861c23eacb2f668c69f6f3bf7" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae29eb8418fcd46f723f8691a2ac06857d31179d33d2f2d91eb13967de97c728" -dependencies = [ - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - [[package]] name = "borsh-schema-derive-internal" version = "0.9.3" @@ -1001,18 +902,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum_dispatch" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" -dependencies = [ - "once_cell", - "proc-macro2 1.0.39", - "quote 1.0.18", - "syn 1.0.96", -] - [[package]] name = "env_logger" version = "0.9.0" @@ -1671,9 +1560,10 @@ dependencies = [ [[package]] name = "lido" -version = "1.3.3" +version = "1.3.6" dependencies = [ - "borsh 0.9.3", + "arrayref", + "borsh", "num-derive", "num-traits", "serde", @@ -1690,7 +1580,7 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "listener" -version = "1.3.3" +version = "1.3.6" dependencies = [ "chrono", "clap 3.1.18", @@ -2361,12 +2251,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - [[package]] name = "rustc_version" version = "0.4.0" @@ -2905,8 +2789,8 @@ dependencies = [ "bincode", "bitflags", "blake3", - "borsh 0.9.3", - "borsh-derive 0.9.3", + "borsh", + "borsh-derive", "bs58 0.4.0", "bv", "bytemuck", @@ -3058,7 +2942,7 @@ dependencies = [ "base64 0.13.0", "bincode", "bitflags", - "borsh 0.9.3", + "borsh", "bs58 0.4.0", "bytemuck", "byteorder", @@ -3201,11 +3085,11 @@ dependencies = [ [[package]] name = "solido-cli-common" -version = "1.3.3" +version = "1.3.6" dependencies = [ "anchor-lang", - "anker", "bincode", + "borsh", "lido", "num-traits", "rusqlite", @@ -3239,21 +3123,6 @@ dependencies = [ "spl-token", ] -[[package]] -name = "spl-math" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ecdd22720b9e5ab578a862928f5010ca197419502bdace600ccd5d23dae9352" -dependencies = [ - "borsh 0.7.2", - "borsh-derive 0.8.2", - "num-derive", - "num-traits", - "solana-program", - "thiserror", - "uint", -] - [[package]] name = "spl-memo" version = "3.0.1" @@ -3277,34 +3146,12 @@ dependencies = [ "thiserror", ] -[[package]] -name = "spl-token-swap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c63b79be6174568e8724912b15e62d0c6b0424ac98397e9a5a867ac2881553af" -dependencies = [ - "arrayref", - "enum_dispatch", - "num-derive", - "num-traits", - "solana-program", - "spl-math", - "spl-token", - "thiserror", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.8.0" @@ -3493,7 +3340,7 @@ dependencies = [ [[package]] name = "tiny_http" version = "0.11.0" -source = "git+https://github.com/ruuda/tiny-http?rev=3568e8880f995dd0348feff9e29645fce995b534#3568e8880f995dd0348feff9e29645fce995b534" +source = "git+https://github.com/tiny-http/tiny-http?rev=f0fce7ed0bdf7439b5bd0b2d15fa82944aac30f6#f0fce7ed0bdf7439b5bd0b2d15fa82944aac30f6" dependencies = [ "ascii", "chunked_transfer", @@ -3654,18 +3501,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "uint" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9db035e67dfaf7edd9aebfe8676afcd63eed53c8a4044fed514c8cccf1835177" -dependencies = [ - "byteorder", - "crunchy", - "rustc-hex", - "static_assertions", -] - [[package]] name = "unicode-bidi" version = "0.3.8" diff --git a/cli/listener/fuzz/Cargo.toml b/cli/listener/fuzz/Cargo.toml index f62ec3c06..1d6f1627b 100644 --- a/cli/listener/fuzz/Cargo.toml +++ b/cli/listener/fuzz/Cargo.toml @@ -21,7 +21,7 @@ path = ".." [patch.crates-io] # https://github.com/tiny-http/tiny-http/pull/225 -tiny_http = { git = "https://github.com/ruuda/tiny-http", rev = "3568e8880f995dd0348feff9e29645fce995b534" } +tiny_http = { git = "https://github.com/tiny-http/tiny-http", rev = "f0fce7ed0bdf7439b5bd0b2d15fa82944aac30f6" } anchor-lang = { git = "https://github.com/lidofinance/anchor", branch = "solana-v1.9.28" } # Prevent this from interfering with workspaces diff --git a/cli/maintainer/Cargo.toml b/cli/maintainer/Cargo.toml index 6fad4684d..793dc2e2e 100644 --- a/cli/maintainer/Cargo.toml +++ b/cli/maintainer/Cargo.toml @@ -4,15 +4,14 @@ description = "Solido Command-line Utility" license = "GPL-3.0" edition = "2018" name = "solido-cli" -version = "1.3.3" +version = "1.3.6" [dependencies] anchor-lang = "0.13.0" -anker = { path = "../../anker", features = ["no-entrypoint"] } bincode = "1.3" borsh = "0.9" bs58 = "0.4.0" -clap = { version = "3.1.18", features = ["derive"] } +clap = { version = "3.2.15", features = ["derive"] } derivation-path = "0.1.3" lido = {path = "../../program", features = ["no-entrypoint"]} num-traits = "0.2" diff --git a/cli/maintainer/src/anker_state.rs b/cli/maintainer/src/anker_state.rs deleted file mode 100644 index 3894f0236..000000000 --- a/cli/maintainer/src/anker_state.rs +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use anker::{ - find_instance_address, find_reserve_authority, find_st_sol_reserve_account, - find_ust_reserve_account, - state::Anker, - token::{BLamports, MicroUst}, -}; -use lido::{state::Lido, token::StLamports}; -use solana_program::{instruction::Instruction, program_pack::Pack}; -use solana_sdk::account::ReadableAccount; -use solana_sdk::pubkey::Pubkey; -use solido_cli_common::{ - error::{Error, SerializationError}, - snapshot::SnapshotConfig, -}; -use spl_token_swap::curve::{constant_product::ConstantProductCurve, fees}; - -#[derive(Default)] -pub struct AnkerState { - pub anker: Anker, - pub anker_program_id: Pubkey, - pub token_swap_program_id: Pubkey, - - pub b_sol_total_supply_amount: BLamports, - pub pool_st_sol_account: Pubkey, - pub pool_ust_account: Pubkey, - pub pool_st_sol_balance: StLamports, - pub pool_ust_balance: MicroUst, - - pub constant_product_calculator: ConstantProductCurve, - pub pool_fees: fees::Fees, - pub ust_mint: Pubkey, - pub pool_mint: Pubkey, - pub pool_fee_account: Pubkey, - pub ust_reserve_balance: MicroUst, - pub st_sol_reserve_balance: StLamports, -} - -impl AnkerState { - pub fn new( - config: &mut SnapshotConfig, - anker_program_id: &Pubkey, - anker_address: &Pubkey, - solido: &Lido, - ) -> solido_cli_common::Result { - let anker = config.client.get_anker(anker_address)?; - - let token_swap_account = config.client.get_account(&anker.token_swap_pool)?; - let token_swap_version = token_swap_account.data()[0]; - if token_swap_version != 1 { - let error: Error = Box::new(SerializationError { - context: "Expected the token swap version to be 1, but found something else." - .to_string(), - cause: None, - address: anker.token_swap_pool, - }); - return Err(error.into()); - } - let token_swap = spl_token_swap::state::SwapV1::unpack(&token_swap_account.data()[1..])?; - let token_swap_program_id = token_swap_account.owner; - - let (anker_ust_reserve, _anker_ust_reserve_bump_seed) = - find_ust_reserve_account(anker_program_id, anker_address); - let ust_reserve_balance = - MicroUst(config.client.get_spl_token_balance(&anker_ust_reserve)?); - let ust_account: spl_token::state::Account = - config.client.get_unpack(&anker_ust_reserve)?; - - let (anker_st_sol_reserve, _anker_st_sol_reserve_bump_seed) = - find_st_sol_reserve_account(anker_program_id, anker_address); - let st_sol_reserve_balance = - StLamports(config.client.get_spl_token_balance(&anker_st_sol_reserve)?); - - let b_sol_mint_account = config.client.get_spl_token_mint(&anker.b_sol_mint)?; - let b_sol_total_supply_amount = BLamports(b_sol_mint_account.supply); - - let (pool_ust_account, pool_st_sol_account) = - if token_swap.token_a_mint == solido.st_sol_mint { - (token_swap.token_b, token_swap.token_a) - } else { - (token_swap.token_a, token_swap.token_b) - }; - - let pool_st_sol_balance = - StLamports(config.client.get_spl_token_balance(&pool_st_sol_account)?); - let pool_ust_balance = MicroUst(config.client.get_spl_token_balance(&pool_ust_account)?); - - Ok(AnkerState { - anker_program_id: *anker_program_id, - anker, - b_sol_total_supply_amount, - pool_st_sol_account, - pool_ust_account, - pool_st_sol_balance, - pool_ust_balance, - constant_product_calculator: ConstantProductCurve::default(), - pool_fees: token_swap.fees, - ust_mint: ust_account.mint, - pool_mint: token_swap.pool_mint, - pool_fee_account: token_swap.pool_fee_account, - ust_reserve_balance, - st_sol_reserve_balance, - token_swap_program_id, - }) - } - - pub fn get_fetch_pool_price_instruction(&self, solido_address: Pubkey) -> Instruction { - let (anker_instance, _anker_bump_seed) = - find_instance_address(&self.anker_program_id, &solido_address); - - anker::instruction::fetch_pool_price( - &self.anker_program_id, - &anker::instruction::FetchPoolPriceAccountsMeta { - anker: anker_instance, - solido: solido_address, - token_swap_pool: self.anker.token_swap_pool, - pool_st_sol_account: self.pool_st_sol_account, - pool_ust_account: self.pool_ust_account, - }, - ) - } - - pub fn get_sell_rewards_instruction( - &self, - solido_address: Pubkey, - st_sol_mint: Pubkey, - ) -> Instruction { - let (anker_instance, _anker_bump_seed) = - find_instance_address(&self.anker_program_id, &solido_address); - let (anker_ust_reserve_account, _ust_reserve_bump_seed) = - find_ust_reserve_account(&self.anker_program_id, &anker_instance); - - let (st_sol_reserve_account, _st_sol_reserve_bump_seed) = - find_st_sol_reserve_account(&self.anker_program_id, &anker_instance); - - let (reserve_authority, _reserve_authority_bump_seed) = - find_reserve_authority(&self.anker_program_id, &anker_instance); - - let (token_swap_authority, _authority_bump_seed) = Pubkey::find_program_address( - &[&self.anker.token_swap_pool.to_bytes()[..]], - &self.token_swap_program_id, - ); - - anker::instruction::sell_rewards( - &self.anker_program_id, - &anker::instruction::SellRewardsAccountsMeta { - anker: anker_instance, - solido: solido_address, - st_sol_reserve_account, - b_sol_mint: self.anker.b_sol_mint, - token_swap_pool: self.anker.token_swap_pool, - pool_st_sol_account: self.pool_st_sol_account, - pool_ust_account: self.pool_ust_account, - ust_reserve_account: anker_ust_reserve_account, - pool_mint: self.pool_mint, - st_sol_mint, - ust_mint: self.ust_mint, - pool_fee_account: self.pool_fee_account, - token_swap_authority, - reserve_authority, - token_swap_program_id: self.token_swap_program_id, - }, - ) - } -} diff --git a/cli/maintainer/src/commands_anker.rs b/cli/maintainer/src/commands_anker.rs deleted file mode 100644 index eb368e86e..000000000 --- a/cli/maintainer/src/commands_anker.rs +++ /dev/null @@ -1,871 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use std::fmt; - -use clap::Parser; -use serde::Serialize; -use solana_program::pubkey::Pubkey; -use solana_program::system_instruction; -use solana_sdk::signature::Keypair; -use solana_sdk::signer::Signer; -use spl_token_swap::curve::base::{CurveType, SwapCurve}; -use spl_token_swap::curve::constant_product::ConstantProductCurve; - -use anker::state::HistoricalStSolPrice; -use anker::token::{BLamports, MicroUst}; -use anker::wormhole::TerraAddress; -use lido::token::{Lamports, StLamports}; -use lido::util::serialize_b58; -use solido_cli_common::error::Abort; -use solido_cli_common::snapshot::{SnapshotClientConfig, SnapshotConfig}; - -use crate::anker_state::AnkerState; -use crate::commands_multisig::{propose_instruction, ProposeInstructionOutput}; -use crate::config::{ - AnkerChangeSellRewardsMinOutBpsOpts, AnkerChangeTerraRewardsDestinationOpts, - AnkerChangeTokenSwapPoolOpts, AnkerDepositOpts, AnkerWithdrawOpts, ConfigFile, CreateAnkerOpts, - CreateTokenPoolOpts, ShowAnkerAuthoritiesOpts, ShowAnkerOpts, -}; -use crate::print_output; -use crate::serialization_utils::serialize_bech32; -use crate::spl_token_utils::{push_create_spl_token_account, push_create_spl_token_mint}; - -#[derive(Parser, Debug)] -enum SubCommand { - /// Create a new Anker instance. - Create(Box), - - /// Show Anker authorities. - ShowAuthorities(ShowAnkerAuthoritiesOpts), - - /// Display the details of an Anker instance. - Show(ShowAnkerOpts), - - /// Create an SPL token swap pool for testing purposes. - CreateTokenPool(CreateTokenPoolOpts), - - /// Deposit stSOL to Anker to obtain bSOL. - Deposit(AnkerDepositOpts), - - /// Return bSOL to Anker to redeem stSOL. - Withdraw(AnkerWithdrawOpts), - - /// Change Terra rewards destination. - ChangeTerraRewardsDestination(AnkerChangeTerraRewardsDestinationOpts), - - /// Change Token Swap pool. - ChangeTokenSwapPool(AnkerChangeTokenSwapPoolOpts), - - /// Change Anker's `sell_rewards_min_out_bps`. - ChangeSellRewardsMinOutBps(AnkerChangeSellRewardsMinOutBpsOpts), -} - -#[derive(Parser, Debug)] -pub struct AnkerOpts { - #[clap(subcommand)] - subcommand: SubCommand, -} - -impl AnkerOpts { - pub fn merge_with_config_and_environment(&mut self, config_file: Option<&ConfigFile>) { - match &mut self.subcommand { - SubCommand::Create(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::Show(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::CreateTokenPool(opts) => { - opts.merge_with_config_and_environment(config_file) - } - SubCommand::Deposit(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::Withdraw(opts) => opts.merge_with_config_and_environment(config_file), - SubCommand::ShowAuthorities(opts) => { - opts.merge_with_config_and_environment(config_file) - } - SubCommand::ChangeTerraRewardsDestination(opts) => { - opts.merge_with_config_and_environment(config_file) - } - SubCommand::ChangeTokenSwapPool(opts) => { - opts.merge_with_config_and_environment(config_file) - } - SubCommand::ChangeSellRewardsMinOutBps(opts) => { - opts.merge_with_config_and_environment(config_file) - } - } - } -} - -pub fn main(config: &mut SnapshotClientConfig, anker_opts: &AnkerOpts) { - match &anker_opts.subcommand { - SubCommand::Create(opts) => { - let result = config.with_snapshot(|config| command_create_anker(config, opts)); - let output = result.ok_or_abort_with("Failed to create Anker instance."); - print_output(config.output_mode, &output); - } - SubCommand::Show(opts) => { - let result = config.with_snapshot(|config| command_show_anker(config, opts)); - let output = result.ok_or_abort_with("Failed to show Anker instance."); - print_output(config.output_mode, &output); - } - SubCommand::CreateTokenPool(opts) => { - let result = config.with_snapshot(|config| command_create_token_pool(config, opts)); - let output = result.ok_or_abort_with("Failed to create Token Pool instance."); - print_output(config.output_mode, &output); - } - SubCommand::Deposit(opts) => { - let result = config.with_snapshot(|config| command_deposit(config, opts)); - let output = result.ok_or_abort_with("Failed to deposit into Anker."); - print_output(config.output_mode, &output); - } - SubCommand::Withdraw(opts) => { - let result = config.with_snapshot(|config| command_withdraw(config, opts)); - let output = result.ok_or_abort_with("Failed to withdraw from Anker."); - print_output(config.output_mode, &output); - } - SubCommand::ShowAuthorities(opts) => { - let result = config.with_snapshot(|_| command_show_anker_authorities(opts)); - let output = result.ok_or_abort_with("Failed to show Anker authorities."); - print_output(config.output_mode, &output); - } - SubCommand::ChangeTerraRewardsDestination(opts) => { - let result = config - .with_snapshot(|config| command_change_terra_rewards_destination(config, opts)); - let output = result - .ok_or_abort_with("Failed to change Anker Terra rewards destination address."); - print_output(config.output_mode, &output); - } - SubCommand::ChangeTokenSwapPool(opts) => { - let result = - config.with_snapshot(|config| command_change_token_swap_pool(config, opts)); - let output = result.ok_or_abort_with("Failed to change Anker token swap pool address."); - print_output(config.output_mode, &output); - } - SubCommand::ChangeSellRewardsMinOutBps(opts) => { - let result = config - .with_snapshot(|config| command_change_sell_rewards_min_out_bps(config, opts)); - let output = result.ok_or_abort_with("Failed to change Anker sell_rewards_min_bps."); - print_output(config.output_mode, &output); - } - } -} - -#[derive(Serialize)] -struct CreateAnkerOutput { - /// Account that stores the data for this Anker instance. - #[serde(serialize_with = "serialize_b58")] - pub anker_address: Pubkey, - - /// Manages the deposited stSOL. - #[serde(serialize_with = "serialize_b58")] - pub st_sol_reserve_account: Pubkey, - - /// Holds the UST proceeds until they are sent to Terra. - #[serde(serialize_with = "serialize_b58")] - pub ust_reserve_account: Pubkey, - - /// SPL token mint account for bSOL tokens. - #[serde(serialize_with = "serialize_b58")] - pub b_sol_mint_address: Pubkey, -} - -impl fmt::Display for CreateAnkerOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Anker details:")?; - writeln!(f, " Anker address: {}", self.anker_address)?; - writeln!( - f, - " Reserve account (stSOL): {}", - self.st_sol_reserve_account - )?; - writeln!(f, " Reserve account (UST): {}", self.ust_reserve_account)?; - writeln!(f, " bSOL mint: {}", self.b_sol_mint_address)?; - Ok(()) - } -} - -fn command_create_anker( - config: &mut SnapshotConfig, - opts: &CreateAnkerOpts, -) -> solido_cli_common::Result { - let solido = config.client.get_solido(opts.solido_address())?; - - let (anker_address, _bump_seed) = - anker::find_instance_address(opts.anker_program_id(), opts.solido_address()); - let (st_sol_reserve_account, _bump_seed) = - anker::find_st_sol_reserve_account(opts.anker_program_id(), &anker_address); - let (ust_reserve_account, _bump_seed) = - anker::find_ust_reserve_account(opts.anker_program_id(), &anker_address); - let (reserve_authority, _bump_seed) = - anker::find_reserve_authority(opts.anker_program_id(), &anker_address); - - let instructions = [anker::instruction::initialize( - opts.anker_program_id(), - &anker::instruction::InitializeAccountsMeta { - fund_rent_from: config.signer.pubkey(), - anker: anker_address, - solido: *opts.solido_address(), - solido_program: *opts.solido_program_id(), - st_sol_mint: solido.st_sol_mint, - b_sol_mint: *opts.b_sol_mint_address(), - st_sol_reserve_account, - ust_reserve_account, - reserve_authority, - wormhole_core_bridge_program_id: *opts.wormhole_core_bridge_program_id(), - wormhole_token_bridge_program_id: *opts.wormhole_token_bridge_program_id(), - ust_mint: *opts.ust_mint_address(), - token_swap_pool: *opts.token_swap_pool(), - }, - opts.terra_rewards_address().clone(), - *opts.sell_rewards_min_out_bps(), - )]; - - config.sign_and_send_transaction(&instructions[..], &[config.signer])?; - - let result = CreateAnkerOutput { - anker_address, - st_sol_reserve_account, - ust_reserve_account, - b_sol_mint_address: *opts.b_sol_mint_address(), - }; - - Ok(result) -} - -#[derive(Serialize)] -struct ShowAnkerOutput { - #[serde(serialize_with = "serialize_b58")] - anker_address: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - anker_program_id: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - solido_address: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - solido_program_id: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - b_sol_mint: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - b_sol_mint_authority: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - token_swap_pool: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - token_swap_pool_st_sol_account: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - token_swap_pool_ust_account: Pubkey, - - #[serde(serialize_with = "serialize_bech32")] - terra_rewards_destination: TerraAddress, - - sell_rewards_min_out_bps: u64, - - #[serde(serialize_with = "serialize_b58")] - reserve_authority: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - st_sol_reserve: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - ust_reserve: Pubkey, - - #[serde(rename = "ust_reserve_balance_micro_ust")] - ust_reserve_balance: MicroUst, - - #[serde(rename = "st_sol_reserve_balance_st_lamports")] - st_sol_reserve_balance: StLamports, - - #[serde(rename = "st_sol_reserve_value_lamports")] - st_sol_reserve_value: Option, - - #[serde(rename = "b_sol_supply_b_lamports")] - b_sol_supply: BLamports, - - historical_st_sol_price: Vec, -} - -impl fmt::Display for ShowAnkerOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Anker address: {}", self.anker_address)?; - writeln!(f, "Anker program id: {}", self.anker_program_id)?; - writeln!(f, "Solido address: {}", self.solido_address)?; - writeln!(f, "Solido program id: {}", self.solido_program_id)?; - writeln!( - f, - "Rewards destination: {}", - self.terra_rewards_destination - )?; - writeln!(f, "Token Swap Pool: {}", self.token_swap_pool)?; - writeln!( - f, - " - Pool stSOL account: {}", - self.token_swap_pool_st_sol_account - )?; - writeln!( - f, - " - Pool UST account: {}", - self.token_swap_pool_ust_account - )?; - if self.sell_rewards_min_out_bps <= 9999 { - writeln!(f, - "Sell rewards min out: {}.{:>02}% of the expected amount ({}.{:>02}% slippage + fees)", - self.sell_rewards_min_out_bps / 100, - self.sell_rewards_min_out_bps % 100, - (10000 - self.sell_rewards_min_out_bps) / 100, - (10000 - self.sell_rewards_min_out_bps) % 100, - )?; - } else { - writeln!( - f, - "Sell rewards min out: {}.{:>02}% of the expected amount \ - (Warning! Getting >100% out is unlikely to ever happen.)", - self.sell_rewards_min_out_bps / 100, - self.sell_rewards_min_out_bps % 100, - )?; - } - writeln!(f, "bSOL mint: {}", self.b_sol_mint)?; - writeln!(f, "bSOL mint authority: {}", self.b_sol_mint_authority)?; - writeln!(f, "bSOL supply: {}", self.b_sol_supply)?; - writeln!(f, "Reserve authority: {}", self.reserve_authority)?; - writeln!(f, "stSOL reserve address: {}", self.st_sol_reserve)?; - writeln!(f, "stSOL reserve balance: {}", self.st_sol_reserve_balance)?; - write!(f, "stSOL reserve value: ")?; - match self.st_sol_reserve_value { - Some(sol_value) => writeln!(f, "{}", sol_value), - None => writeln!(f, "Undefined; does Solido have nonzero deposits?"), - }?; - writeln!(f, "UST reserve address: {}", self.ust_reserve)?; - writeln!(f, "UST reserve balance: {}", self.ust_reserve_balance)?; - writeln!(f, "Historical stSOL price:")?; - for x in &self.historical_st_sol_price { - writeln!(f, " Slot {}: {} per stSOL", x.slot, x.st_sol_price_in_ust)?; - } - Ok(()) - } -} - -fn command_show_anker( - config: &mut SnapshotConfig, - opts: &ShowAnkerOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = client.get_solido(&anker.solido)?; - let anker_state = AnkerState::new(config, &anker_program_id, opts.anker_address(), &solido)?; - - let (mint_authority, _seed) = - anker::find_mint_authority(&anker_program_id, opts.anker_address()); - let (reserve_authority, _seed) = - anker::find_reserve_authority(&anker_program_id, opts.anker_address()); - let (st_sol_reserve, _seed) = - anker::find_st_sol_reserve_account(&anker_program_id, opts.anker_address()); - let (ust_reserve, _seed) = - anker::find_ust_reserve_account(&anker_program_id, opts.anker_address()); - - let st_sol_reserve_balance = anker_state.st_sol_reserve_balance; - let ust_reserve_balance = anker_state.ust_reserve_balance; - - let st_sol_reserve_value = solido - .exchange_rate - .exchange_st_sol(st_sol_reserve_balance) - .ok(); - let b_sol_supply = anker_state.b_sol_total_supply_amount; - - let result = ShowAnkerOutput { - anker_address: *opts.anker_address(), - anker_program_id, - - solido_address: anker.solido, - solido_program_id: anker.solido_program_id, - - token_swap_pool: anker.token_swap_pool, - token_swap_pool_st_sol_account: anker_state.pool_st_sol_account, - token_swap_pool_ust_account: anker_state.pool_ust_account, - - terra_rewards_destination: anker.terra_rewards_destination, - sell_rewards_min_out_bps: anker.sell_rewards_min_out_bps, - - b_sol_mint: anker.b_sol_mint, - b_sol_mint_authority: mint_authority, - reserve_authority, - st_sol_reserve, - ust_reserve, - - st_sol_reserve_balance, - st_sol_reserve_value, - - ust_reserve_balance, - b_sol_supply, - - historical_st_sol_price: anker.historical_st_sol_prices.0.to_vec(), - }; - - Ok(result) -} - -#[derive(Serialize)] -struct CreateTokenPoolOutput { - #[serde(serialize_with = "serialize_b58")] - pool_address: Pubkey, - #[serde(serialize_with = "serialize_b58")] - pool_authority: Pubkey, -} - -impl fmt::Display for CreateTokenPoolOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Pool address: {}", self.pool_address)?; - writeln!(f, "Pool authority: {}", self.pool_authority)?; - Ok(()) - } -} - -/// Create a Token Pool. Used for testing purposes only. -/// -/// The pool is created with 0 fees. -/// The pool is `ConstantProduct`, i.e., `token_a * token_b = C`, with C a -/// constant. -fn command_create_token_pool( - config: &mut SnapshotConfig, - opts: &CreateTokenPoolOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let mut instructions = Vec::new(); - - let token_pool_account = Keypair::new(); - let rent = client.get_rent()?; - - let rent_lamports = rent.minimum_balance(spl_token_swap::state::SwapVersion::LATEST_LEN); - instructions.push(system_instruction::create_account( - &config.signer.pubkey(), - &token_pool_account.pubkey(), - rent_lamports, - spl_token_swap::state::SwapVersion::LATEST_LEN as u64, - opts.token_swap_program_id(), - )); - - let (authority_pubkey, authority_bump_seed) = Pubkey::find_program_address( - &[&token_pool_account.pubkey().to_bytes()[..]], - opts.token_swap_program_id(), - ); - - let pool_mint_keypair = - push_create_spl_token_mint(config, &mut instructions, &authority_pubkey)?; - let pool_mint_pubkey = pool_mint_keypair.pubkey(); - let pool_fee_keypair = push_create_spl_token_account( - config, - &mut instructions, - &pool_mint_pubkey, - &config.signer.pubkey(), - )?; - let pool_token_keypair = push_create_spl_token_account( - config, - &mut instructions, - &pool_mint_pubkey, - &config.signer.pubkey(), - )?; - - // Change the token owner to the pool's authority. - instructions.push(spl_token::instruction::set_authority( - &spl_token::id(), - opts.st_sol_account(), - Some(&authority_pubkey), - spl_token::instruction::AuthorityType::AccountOwner, - &config.signer.pubkey(), - &[], - )?); - - // Change the token owner to the pool's authority. - instructions.push(spl_token::instruction::set_authority( - &spl_token::id(), - opts.ust_account(), - Some(&authority_pubkey), - spl_token::instruction::AuthorityType::AccountOwner, - &config.signer.pubkey(), - &[], - )?); - - let signers = vec![ - config.signer, - &token_pool_account, - &pool_mint_keypair, - &pool_fee_keypair, - &pool_token_keypair, - ]; - - let fees = spl_token_swap::curve::fees::Fees { - trade_fee_numerator: 0, - trade_fee_denominator: 10, - owner_trade_fee_numerator: 0, - owner_trade_fee_denominator: 10, - owner_withdraw_fee_numerator: 0, - owner_withdraw_fee_denominator: 10, - host_fee_numerator: 0, - host_fee_denominator: 10, - }; - - let swap_curve = SwapCurve { - curve_type: CurveType::ConstantProduct, - calculator: Box::new(ConstantProductCurve), - }; - - let initialize_pool_instruction = spl_token_swap::instruction::initialize( - opts.token_swap_program_id(), - &spl_token::id(), - &token_pool_account.pubkey(), - &authority_pubkey, - opts.st_sol_account(), - opts.ust_account(), - &pool_mint_pubkey, - &pool_fee_keypair.pubkey(), - &pool_token_keypair.pubkey(), - authority_bump_seed, - fees, - swap_curve, - ) - .expect("Failed to create token pool initialization instruction."); - instructions.push(initialize_pool_instruction); - - config.sign_and_send_transaction(&instructions[..], &signers)?; - - Ok(CreateTokenPoolOutput { - pool_address: token_pool_account.pubkey(), - pool_authority: authority_pubkey, - }) -} - -#[derive(Serialize)] -struct DepositOutput { - /// Recipient account that holds the bSOL. - #[serde(serialize_with = "serialize_b58")] - pub b_sol_account: Pubkey, - - /// Whether we had to create the associated bSOL account. False if one existed already. - pub created_associated_b_sol_account: bool, -} - -impl fmt::Display for DepositOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.created_associated_b_sol_account { - writeln!(f, "Created recipient bSOL account, it did not yet exist.")?; - } else { - writeln!(f, "Recipient bSOL account existed already before deposit.")?; - } - writeln!(f, "Recipient bSOL account: {}", self.b_sol_account)?; - Ok(()) - } -} - -fn command_deposit( - config: &mut SnapshotConfig, - opts: &AnkerDepositOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = client.get_solido(&anker.solido)?; - - let mut instructions = Vec::new(); - let mut created_recipient = false; - - // The user can pass in a particular SPL token account to send from, but if - // none is provided, we use the associated token account of the signer. - let sender = if opts.from_st_sol_address() == &Pubkey::default() { - spl_associated_token_account::get_associated_token_address( - &config.signer.pubkey(), - &solido.st_sol_mint, - ) - } else { - *opts.from_st_sol_address() - }; - - let recipient = spl_associated_token_account::get_associated_token_address( - &config.signer.pubkey(), - &anker.b_sol_mint, - ); - - if !config.client.account_exists(&recipient)? { - let instr = spl_associated_token_account::create_associated_token_account( - &config.signer.pubkey(), - &config.signer.pubkey(), - &anker.b_sol_mint, - ); - instructions.push(instr); - created_recipient = true; - } - - let (st_sol_reserve_account, _bump_seed) = - anker::find_st_sol_reserve_account(&anker_program_id, opts.anker_address()); - let (b_sol_mint_authority, _bump_seed) = - anker::find_mint_authority(&anker_program_id, opts.anker_address()); - - let instr = anker::instruction::deposit( - &anker_program_id, - &anker::instruction::DepositAccountsMeta { - anker: *opts.anker_address(), - solido: anker.solido, - from_account: sender, - user_authority: config.signer.pubkey(), - to_reserve_account: st_sol_reserve_account, - b_sol_user_account: recipient, - b_sol_mint: anker.b_sol_mint, - b_sol_mint_authority, - }, - *opts.amount_st_sol(), - ); - instructions.push(instr); - - config.sign_and_send_transaction(&instructions[..], &[config.signer])?; - - let result = DepositOutput { - created_associated_b_sol_account: created_recipient, - b_sol_account: recipient, - }; - Ok(result) -} - -#[derive(Serialize)] -struct WithdrawOutput { - /// Sender account whose bSOL balance was decreased. - #[serde(serialize_with = "serialize_b58")] - pub from_b_sol_account: Pubkey, - - /// Recipient account whose stSOL balance was increased. - #[serde(serialize_with = "serialize_b58")] - pub to_st_sol_account: Pubkey, - - /// Whether we had to create the associated stSOL account. False if one existed already. - pub created_associated_st_sol_account: bool, -} - -impl fmt::Display for WithdrawOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.created_associated_st_sol_account { - writeln!(f, "Created recipient stSOL account, it did not yet exist.")?; - } else { - writeln!( - f, - "Recipient stSOL account existed already before withdraw." - )?; - } - writeln!(f, "Sender bSOL account: {}", self.from_b_sol_account)?; - writeln!(f, "Recipient stSOL account: {}", self.to_st_sol_account)?; - Ok(()) - } -} - -fn command_withdraw( - config: &mut SnapshotConfig, - opts: &AnkerWithdrawOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = client.get_solido(&anker.solido)?; - - let mut instructions = Vec::new(); - let mut created_recipient = false; - - // The user can pass in a particular SPL token account to send from, but if - // none is provided, we use the associated token account of the signer. - let sender = if opts.from_b_sol_address() == &Pubkey::default() { - spl_associated_token_account::get_associated_token_address( - &config.signer.pubkey(), - &anker.b_sol_mint, - ) - } else { - *opts.from_b_sol_address() - }; - - // Also for the recipient, we use the associated token account of the signer - // if the user did not specify an account. - let recipient = if opts.to_st_sol_address() == &Pubkey::default() { - let recipient = spl_associated_token_account::get_associated_token_address( - &config.signer.pubkey(), - &solido.st_sol_mint, - ); - if !config.client.account_exists(&recipient)? { - let instr = spl_associated_token_account::create_associated_token_account( - &config.signer.pubkey(), - &config.signer.pubkey(), - &solido.st_sol_mint, - ); - instructions.push(instr); - created_recipient = true; - } - recipient - } else { - *opts.to_st_sol_address() - }; - - let (st_sol_reserve_account, _bump_seed) = - anker::find_st_sol_reserve_account(&anker_program_id, opts.anker_address()); - let (reserve_authority, _bump_seed) = - anker::find_reserve_authority(&anker_program_id, opts.anker_address()); - - let instr = anker::instruction::withdraw( - &anker_program_id, - &anker::instruction::WithdrawAccountsMeta { - anker: *opts.anker_address(), - solido: anker.solido, - from_b_sol_account: sender, - from_b_sol_authority: config.signer.pubkey(), - to_st_sol_account: recipient, - reserve_account: st_sol_reserve_account, - reserve_authority, - b_sol_mint: anker.b_sol_mint, - }, - *opts.amount_b_sol(), - ); - instructions.push(instr); - - config.sign_and_send_transaction(&instructions[..], &[config.signer])?; - - let result = WithdrawOutput { - created_associated_st_sol_account: created_recipient, - from_b_sol_account: sender, - to_st_sol_account: recipient, - }; - Ok(result) -} - -#[derive(Serialize)] -pub struct ShowAnkerAuthoritiesOutput { - #[serde(serialize_with = "serialize_b58")] - pub anker_address: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - pub b_sol_mint_authority: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - pub st_sol_reserve_account: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - pub ust_reserve_account: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - pub reserve_authority: Pubkey, -} - -impl fmt::Display for ShowAnkerAuthoritiesOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Anker address: {}", self.anker_address,)?; - writeln!(f, "bSOL Mint authority: {}", self.b_sol_mint_authority)?; - writeln!(f, "stSOL reserve account: {}", self.st_sol_reserve_account)?; - writeln!(f, "UST reserve account: {}", self.ust_reserve_account)?; - writeln!(f, "Reserve authority: {}", self.reserve_authority,)?; - Ok(()) - } -} - -/// Show Anker address derived for its Solido instance and Anker authorities. -pub fn command_show_anker_authorities( - opts: &ShowAnkerAuthoritiesOpts, -) -> solido_cli_common::Result { - let (anker_address, _) = - anker::find_instance_address(opts.anker_program_id(), opts.solido_address()); - let (b_sol_mint_authority, _) = - anker::find_mint_authority(opts.anker_program_id(), &anker_address); - let (reserve_authority, _) = - anker::find_reserve_authority(opts.anker_program_id(), &anker_address); - let (st_sol_reserve_account, _) = - anker::find_st_sol_reserve_account(opts.anker_program_id(), &anker_address); - let (ust_reserve_account, _) = - anker::find_ust_reserve_account(opts.anker_program_id(), &anker_address); - - Ok(ShowAnkerAuthoritiesOutput { - anker_address, - b_sol_mint_authority, - st_sol_reserve_account, - ust_reserve_account, - reserve_authority, - }) -} - -pub fn command_change_terra_rewards_destination( - config: &mut SnapshotConfig, - opts: &AnkerChangeTerraRewardsDestinationOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = config.client.get_solido(&anker.solido)?; - - let instruction = anker::instruction::change_terra_rewards_destination( - &anker_program_id, - &anker::instruction::ChangeTerraRewardsDestinationAccountsMeta { - anker: *opts.anker_address(), - solido: anker.solido, - manager: solido.manager, - }, - opts.terra_rewards_destination().clone(), - ); - propose_instruction( - config, - opts.multisig_program_id(), - *opts.multisig_address(), - instruction, - ) -} - -pub fn command_change_token_swap_pool( - config: &mut SnapshotConfig, - opts: &AnkerChangeTokenSwapPoolOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = config.client.get_solido(&anker.solido)?; - - let instruction = anker::instruction::change_token_swap_pool( - &anker_program_id, - &anker::instruction::ChangeTokenSwapPoolAccountsMeta { - anker: *opts.anker_address(), - solido: anker.solido, - manager: solido.manager, - current_token_swap_pool: anker.token_swap_pool, - new_token_swap_pool: *opts.token_swap_pool(), - }, - ); - propose_instruction( - config, - opts.multisig_program_id(), - *opts.multisig_address(), - instruction, - ) -} - -pub fn command_change_sell_rewards_min_out_bps( - config: &mut SnapshotConfig, - opts: &AnkerChangeSellRewardsMinOutBpsOpts, -) -> solido_cli_common::Result { - let client = &mut config.client; - let anker_account = client.get_account(opts.anker_address())?; - let anker_program_id = anker_account.owner; - let anker = client.get_anker(opts.anker_address())?; - let solido = config.client.get_solido(&anker.solido)?; - - let instruction = anker::instruction::change_sell_rewards_min_out_bps( - &anker_program_id, - &anker::instruction::ChangeSellRewardsMinOutBpsAccountsMeta { - anker: *opts.anker_address(), - solido: anker.solido, - manager: solido.manager, - }, - *opts.sell_rewards_min_out_bps(), - ); - propose_instruction( - config, - opts.multisig_program_id(), - *opts.multisig_address(), - instruction, - ) -} diff --git a/cli/maintainer/src/commands_multisig.rs b/cli/maintainer/src/commands_multisig.rs index 7689f2a57..40d310ce8 100644 --- a/cli/maintainer/src/commands_multisig.rs +++ b/cli/maintainer/src/commands_multisig.rs @@ -1,17 +1,13 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 +use itertools::Itertools; use std::collections::HashSet; use std::fmt; use std::str::FromStr; use anchor_lang::prelude::{AccountMeta, ToAccountMetas}; use anchor_lang::{Discriminator, InstructionData}; -use anker::instruction::{ - ChangeSellRewardsMinOutBpsAccountsMeta, ChangeTerraRewardsDestinationAccountsMeta, - ChangeTokenSwapPoolAccountsMeta, -}; -use anker::wormhole::TerraAddress; use borsh::de::BorshDeserialize; use borsh::ser::BorshSerialize; use clap::Parser; @@ -28,8 +24,8 @@ use solana_sdk::sysvar; use lido::{ instruction::{ - AddMaintainerMeta, AddValidatorMetaV2, ChangeRewardDistributionMeta, - DeactivateValidatorMeta, LidoInstruction, RemoveMaintainerMeta, + AddMaintainerMetaV2, AddValidatorMetaV2, ChangeRewardDistributionMeta, + DeactivateValidatorMetaV2, LidoInstruction, MigrateStateToV2Meta, RemoveMaintainerMetaV2, SetMaxValidationCommissionMeta, }, state::{FeeRecipients, Lido, RewardDistribution}, @@ -128,17 +124,11 @@ pub fn main(config: &mut SnapshotClientConfig, multisig_opts: MultisigOpts) { } SubCommand::ShowTransaction(cmd_opts) => { let result = config.with_snapshot(|config| { - let anker_program_id = if cmd_opts.anker_program_id() == &Pubkey::default() { - None - } else { - Some(*cmd_opts.anker_program_id()) - }; show_transaction( config, cmd_opts.transaction_address(), cmd_opts.multisig_program_id(), cmd_opts.solido_program_id(), - anker_program_id, ) }); let output = result.ok_or_abort_with("Failed to read multisig."); @@ -157,7 +147,7 @@ pub fn main(config: &mut SnapshotClientConfig, multisig_opts: MultisigOpts) { SubCommand::Approve(cmd_opts) => { let result = approve( config, - cmd_opts.transaction_address(), + &[*cmd_opts.transaction_address()], cmd_opts.multisig_program_id(), cmd_opts.multisig_address(), ); @@ -384,10 +374,8 @@ enum ParsedInstruction { new_owners: Vec, }, SolidoInstruction(SolidoInstruction), - AnkerInstruction(AnkerInstruction), TokenInstruction(TokenInstruction), InvalidSolidoInstruction, - InvalidAnkerInstruction, Unrecognized, } @@ -412,6 +400,8 @@ enum SolidoInstruction { #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, + + validator_index: u32, }, AddMaintainer { #[serde(serialize_with = "serialize_b58")] @@ -432,6 +422,8 @@ enum SolidoInstruction { #[serde(serialize_with = "serialize_b58")] maintainer: Pubkey, + + maintainer_index: u32, }, ChangeRewardDistribution { current_solido: Box, @@ -454,45 +446,26 @@ enum SolidoInstruction { #[serde(serialize_with = "serialize_b58")] manager: Pubkey, }, -} - -#[allow(clippy::enum_variant_names)] -#[derive(Serialize)] -enum AnkerInstruction { - ChangeTerraRewardsDestination { - #[serde(serialize_with = "serialize_b58")] - anker_instance: Pubkey, - + MigrateStateToV2 { #[serde(serialize_with = "serialize_b58")] - manager: Pubkey, - - old_terra_rewards_destination: TerraAddress, - - new_terra_rewards_destination: TerraAddress, - }, - ChangeTokenSwapPool { - #[serde(serialize_with = "serialize_b58")] - anker_instance: Pubkey, + solido_instance: Pubkey, #[serde(serialize_with = "serialize_b58")] manager: Pubkey, #[serde(serialize_with = "serialize_b58")] - old_token_swap_pool: Pubkey, + validator_list: Pubkey, #[serde(serialize_with = "serialize_b58")] - new_token_swap_pool: Pubkey, - }, - ChangeSellRewardsMinOutBps { - #[serde(serialize_with = "serialize_b58")] - anker_instance: Pubkey, + maintainer_list: Pubkey, #[serde(serialize_with = "serialize_b58")] - manager: Pubkey, + developer_account: Pubkey, - old_sell_rewards_min_out_bps: u64, - - new_sell_rewards_min_out_bps: u64, + reward_distribution: RewardDistribution, + max_validators: u32, + max_maintainers: u32, + max_commission_percentage: u8, }, } @@ -616,11 +589,13 @@ impl fmt::Display for ShowTransactionOutput { solido_instance, manager, validator_vote_account, + validator_index, } => { writeln!(f, "It deactivates a validator.")?; writeln!(f, " Solido instance: {}", solido_instance)?; writeln!(f, " Manager: {}", manager)?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; + writeln!(f, " Validator index: {}", validator_index)?; } SolidoInstruction::AddMaintainer { solido_instance, @@ -636,11 +611,13 @@ impl fmt::Display for ShowTransactionOutput { solido_instance, manager, maintainer, + maintainer_index, } => { writeln!(f, "It removes a maintainer")?; - writeln!(f, " Solido instance: {}", solido_instance)?; - writeln!(f, " Manager: {}", manager)?; - writeln!(f, " Maintainer: {}", maintainer)?; + writeln!(f, " Solido instance: {}", solido_instance)?; + writeln!(f, " Manager: {}", manager)?; + writeln!(f, " Maintainer: {}", maintainer)?; + writeln!(f, " Maintainer index: {}", maintainer_index)?; } SolidoInstruction::ChangeRewardDistribution { current_solido, @@ -670,6 +647,32 @@ impl fmt::Display for ShowTransactionOutput { max_commission_percentage )?; } + SolidoInstruction::MigrateStateToV2 { + solido_instance, + manager, + validator_list, + maintainer_list, + developer_account, + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + } => { + writeln!(f, "It migrates Lido state to a version 2")?; + writeln!(f, " Solido instance: {}", solido_instance)?; + writeln!(f, " Manager: {}", manager)?; + writeln!(f, " Validator list: {}", validator_list)?; + writeln!(f, " Maintainer list: {}", maintainer_list)?; + writeln!(f, " Developer account: {}", developer_account)?; + writeln!(f, " Max validators: {}", max_validators)?; + writeln!(f, " Max maintainers: {}", max_maintainers)?; + writeln!( + f, + " Max validation commission: {}%", + max_commission_percentage + )?; + writeln!(f, " {:?}", reward_distribution)?; + } } } ParsedInstruction::Unrecognized => { @@ -718,66 +721,6 @@ impl fmt::Display for ShowTransactionOutput { } } } - ParsedInstruction::AnkerInstruction(anker_instruction) => match anker_instruction { - AnkerInstruction::ChangeTerraRewardsDestination { - anker_instance, - manager, - old_terra_rewards_destination, - new_terra_rewards_destination, - } => { - writeln!(f, "It changes the Terra rewards destination in Anker")?; - writeln!(f, " Anker instance: {}", anker_instance)?; - writeln!(f, " Manager: {}", manager)?; - writeln!( - f, - " Old Terra rewards destination: {}", - old_terra_rewards_destination - )?; - writeln!( - f, - " New Terra rewards destination: {}", - new_terra_rewards_destination - )?; - } - AnkerInstruction::ChangeTokenSwapPool { - anker_instance, - manager, - old_token_swap_pool, - new_token_swap_pool, - } => { - writeln!(f, "It changes the Token Swap Pool in Anker")?; - writeln!(f, " Anker instance: {}", anker_instance)?; - writeln!(f, " Manager: {}", manager)?; - writeln!(f, " Old Token Swap Pool: {}", old_token_swap_pool)?; - writeln!(f, " New Token Swap Pool: {}", new_token_swap_pool)?; - } - AnkerInstruction::ChangeSellRewardsMinOutBps { - anker_instance, - manager, - old_sell_rewards_min_out_bps, - new_sell_rewards_min_out_bps, - } => { - writeln!(f, "It changes the sell rewards min bps in Anker")?; - writeln!(f, " Anker instance: {}", anker_instance)?; - writeln!(f, " Manager: {}", manager)?; - writeln!( - f, - " Old sell rewards min bps: {}", - old_sell_rewards_min_out_bps - )?; - writeln!( - f, - " New sell rewards min bps: {}", - new_sell_rewards_min_out_bps - )?; - } - }, - ParsedInstruction::InvalidAnkerInstruction => { - writeln!( - f, - " Tried to deserialize an Anker instruction, but failed." - )?; - } } Ok(()) @@ -919,7 +862,6 @@ fn show_transaction( transaction_address: &Pubkey, multisig_program_id: &Pubkey, solido_program_id: &Pubkey, - anker_program_id: Option, ) -> Result { let transaction: serum_multisig::Transaction = config.client.get_account_deserialize(transaction_address)?; @@ -1018,19 +960,6 @@ fn show_transaction( ParsedInstruction::InvalidSolidoInstruction } } - } else if anker_program_id == Some(instr.program_id) { - match try_parse_anker_instruction(config, &instr) { - Ok(instr) => instr, - Err(SnapshotError::MissingAccount) => return Err(SnapshotError::MissingAccount), - Err(SnapshotError::MissingValidatorIdentity(addr)) => { - return Err(SnapshotError::MissingValidatorIdentity(addr)) - } - Err(SnapshotError::OtherError(err)) => { - println!("Warning: Failed to parse Anker instruction."); - err.print_pretty(); - ParsedInstruction::InvalidAnkerInstruction - } - } } else { ParsedInstruction::Unrecognized }; @@ -1075,28 +1004,30 @@ fn try_parse_solido_instruction( validator_vote_account: accounts.validator_vote_account, }) } - LidoInstruction::DeactivateValidator => { - let accounts = DeactivateValidatorMeta::try_from_slice(&instr.accounts)?; + LidoInstruction::DeactivateValidatorV2 { validator_index } => { + let accounts = DeactivateValidatorMetaV2::try_from_slice(&instr.accounts)?; ParsedInstruction::SolidoInstruction(SolidoInstruction::DeactivateValidator { solido_instance: accounts.lido, manager: accounts.manager, validator_vote_account: accounts.validator_vote_account_to_deactivate, + validator_index, }) } - LidoInstruction::AddMaintainer => { - let accounts = AddMaintainerMeta::try_from_slice(&instr.accounts)?; + LidoInstruction::AddMaintainerV2 => { + let accounts = AddMaintainerMetaV2::try_from_slice(&instr.accounts)?; ParsedInstruction::SolidoInstruction(SolidoInstruction::AddMaintainer { solido_instance: accounts.lido, manager: accounts.manager, maintainer: accounts.maintainer, }) } - LidoInstruction::RemoveMaintainer => { - let accounts = RemoveMaintainerMeta::try_from_slice(&instr.accounts)?; + LidoInstruction::RemoveMaintainerV2 { maintainer_index } => { + let accounts = RemoveMaintainerMetaV2::try_from_slice(&instr.accounts)?; ParsedInstruction::SolidoInstruction(SolidoInstruction::RemoveMaintainer { solido_instance: accounts.lido, manager: accounts.manager, maintainer: accounts.maintainer, + maintainer_index, }) } LidoInstruction::SetMaxValidationCommission { @@ -1109,6 +1040,25 @@ fn try_parse_solido_instruction( manager: accounts.manager, }) } + LidoInstruction::MigrateStateToV2 { + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + } => { + let accounts = MigrateStateToV2Meta::try_from_slice(&instr.accounts)?; + ParsedInstruction::SolidoInstruction(SolidoInstruction::MigrateStateToV2 { + solido_instance: accounts.lido, + manager: accounts.manager, + validator_list: accounts.validator_list, + maintainer_list: accounts.maintainer_list, + developer_account: accounts.developer_account, + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + }) + } _ => ParsedInstruction::InvalidSolidoInstruction, }) @@ -1140,51 +1090,6 @@ fn try_parse_token_instruction( } } -fn try_parse_anker_instruction( - config: &mut SnapshotConfig, - instr: &Instruction, -) -> Result { - let instruction: anker::instruction::AnkerInstruction = - BorshDeserialize::deserialize(&mut instr.data.as_slice())?; - Ok(match instruction { - anker::instruction::AnkerInstruction::ChangeTerraRewardsDestination { - terra_rewards_destination, - } => { - let accounts = - ChangeTerraRewardsDestinationAccountsMeta::try_from_slice(&instr.accounts)?; - let current_anker = config.client.get_anker(&accounts.anker)?; - ParsedInstruction::AnkerInstruction(AnkerInstruction::ChangeTerraRewardsDestination { - anker_instance: accounts.anker, - manager: accounts.manager, - old_terra_rewards_destination: current_anker.terra_rewards_destination, - new_terra_rewards_destination: terra_rewards_destination, - }) - } - anker::instruction::AnkerInstruction::ChangeTokenSwapPool => { - let accounts = ChangeTokenSwapPoolAccountsMeta::try_from_slice(&instr.accounts)?; - ParsedInstruction::AnkerInstruction(AnkerInstruction::ChangeTokenSwapPool { - anker_instance: accounts.anker, - manager: accounts.manager, - old_token_swap_pool: accounts.current_token_swap_pool, - new_token_swap_pool: accounts.new_token_swap_pool, - }) - } - anker::instruction::AnkerInstruction::ChangeSellRewardsMinOutBps { - sell_rewards_min_out_bps, - } => { - let accounts = ChangeSellRewardsMinOutBpsAccountsMeta::try_from_slice(&instr.accounts)?; - let current_anker = config.client.get_anker(&accounts.anker)?; - ParsedInstruction::AnkerInstruction(AnkerInstruction::ChangeSellRewardsMinOutBps { - anker_instance: accounts.anker, - manager: accounts.manager, - old_sell_rewards_min_out_bps: current_anker.sell_rewards_min_out_bps, - new_sell_rewards_min_out_bps: sell_rewards_min_out_bps, - }) - } - _ => ParsedInstruction::InvalidAnkerInstruction, - }) -} - #[derive(Serialize)] pub struct ProposeInstructionOutput { #[serde(serialize_with = "serialize_b58")] @@ -1231,7 +1136,7 @@ pub fn propose_instruction( let dummy_tx = serum_multisig::Transaction { multisig: multisig_address, program_id: instruction.program_id, - accounts, + accounts: accounts.clone(), data: instruction.data.clone(), signers: multisig .owners @@ -1264,15 +1169,6 @@ pub fn propose_instruction( multisig_program_id, ); - // The Multisig program expects `serum_multisig::TransactionAccount` instead - // of `solana_sdk::AccountMeta`. The types are structurally identical, - // but not nominally, so we need to convert these. - let accounts: Vec<_> = instruction - .accounts - .iter() - .map(serum_multisig::TransactionAccount::from) - .collect(); - let multisig_accounts = multisig_accounts::CreateTransaction { multisig: multisig_address, transaction: transaction_account.pubkey(), @@ -1367,49 +1263,59 @@ fn propose_change_multisig( #[derive(Serialize)] struct ApproveOutput { pub transaction_id: Signature, - pub num_approvals: u64, + pub sub_transactions: Vec, + pub num_approvals: Vec, pub threshold: u64, } impl fmt::Display for ApproveOutput { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Transaction approved.")?; + writeln!(f, "Transactions approved.")?; writeln!( f, "Solana transaction id of approval: {}", self.transaction_id )?; - writeln!( - f, - "Multisig transaction now has {} out of {} required approvals.", - self.num_approvals, self.threshold, - )?; + + for (sub_transaction, num_approvals) in + self.sub_transactions.iter().zip(&self.num_approvals) + { + writeln!( + f, + "Multisig transaction {} now has {} out of {} required approvals.", + sub_transaction, num_approvals, self.threshold, + )?; + } Ok(()) } } -fn approve( +fn approve<'a>( config: &mut SnapshotClientConfig, - transaction_address: &Pubkey, + transactions: &'a [Pubkey], multisig_program_id: &Pubkey, multisig_address: &Pubkey, ) -> std::result::Result { // First, do the actual approval. let signature = config.with_snapshot(|config| { - let approve_accounts = multisig_accounts::Approve { - multisig: *multisig_address, - transaction: *transaction_address, - // The owner that signs the multisig proposed transaction, should be - // the public key that signs the entire approval transaction (which - // is also the payer). - owner: config.signer.pubkey(), - }; - let approve_instruction = Instruction { - program_id: *multisig_program_id, - data: multisig_instruction::Approve.data(), - accounts: approve_accounts.to_account_metas(None), - }; - config.sign_and_send_transaction(&[approve_instruction], &[config.signer]) + let mut instructions = vec![]; + for transaction_address in transactions { + let approve_accounts = multisig_accounts::Approve { + multisig: *multisig_address, + transaction: *transaction_address, + // The owner that signs the multisig proposed transaction, should be + // the public key that signs the entire approval transaction (which + // is also the payer). + owner: config.signer.pubkey(), + }; + let approve_instruction = Instruction { + program_id: *multisig_program_id, + data: multisig_instruction::Approve.data(), + accounts: approve_accounts.to_account_metas(None), + }; + instructions.push(approve_instruction); + } + config.sign_and_send_transaction(&instructions, &[config.signer]) })?; // After a successful approval, query the new state of the transaction, so @@ -1418,12 +1324,17 @@ fn approve( let multisig: serum_multisig::Multisig = config.client.get_account_deserialize(multisig_address)?; - let transaction: serum_multisig::Transaction = - config.client.get_account_deserialize(transaction_address)?; + let mut num_approvals = vec![]; + for transaction_address in transactions { + let transaction: serum_multisig::Transaction = + config.client.get_account_deserialize(transaction_address)?; + num_approvals.push(transaction.signers.iter().filter(|x| **x).count() as u64); + } let result = ApproveOutput { transaction_id: signature, - num_approvals: transaction.signers.iter().filter(|x| **x).count() as u64, + num_approvals, + sub_transactions: transactions.to_vec(), threshold: multisig.threshold, }; @@ -1447,27 +1358,41 @@ fn approve_batch( OutputMode::Text => { /* This is fine. */ } } + // If not interactive will execute transactions in chunks + const CHUNK_SIZE: usize = 70; + let transaction_addresses = std::fs::read_to_string(opts.transaction_addresses_path()) .expect("Failed to read transaction addresses from file."); - for (i, line) in transaction_addresses.lines().enumerate() { - // Take the first word from the line; the remainder can contain a comment - // about what the transaction is for. - match line - .split_ascii_whitespace() - .next() - .and_then(|addr_str| Pubkey::from_str(addr_str).ok()) - { - Some(addr) => { - // Now that we know the transaction address is valid, print the - // full line, to preserve any trailing content. (But trim the - // newline, println already adds one.) - println!("\nTransaction {}", line.trim()); - approve_transaction_interactive(config, opts, &addr)?; - } - None => { - println!("\nInvalid transaction address on line {}, skipping.", i + 1); + for (i, chunk) in transaction_addresses + .lines() + .chunks(CHUNK_SIZE) + .into_iter() + .enumerate() + { + let mut transactions = vec![]; + for (j, line) in chunk.enumerate() { + // Take the first word from the line; the remainder can contain a comment + // about what the transaction is for. + match line + .split_ascii_whitespace() + .next() + .and_then(|addr_str| Pubkey::from_str(addr_str).ok()) + { + Some(addr) => { + // Now that we know the transaction address is valid, print the + // full line, to preserve any trailing content. (But trim the + // newline, println already adds one.) + transactions.push(addr); + } + None => { + println!( + "\nInvalid transaction address on line {}, skipping.", + i * CHUNK_SIZE + j + 1 + ); + } } } + approve_transactions(config, opts, &transactions)?; } Ok(()) @@ -1499,51 +1424,66 @@ fn ask_user_y_n(prompt: &'static str) -> bool { } } -fn approve_transaction_interactive( +/// Approve and execute transactions interactively or not. +/// Will execute transaction if not interactive. +fn approve_transactions( config: &mut SnapshotClientConfig, opts: &ApproveBatchOpts, - transaction_address: &Pubkey, + transactions: &[Pubkey], ) -> std::result::Result<(), crate::Error> { - config.with_snapshot(|config| { - let output = show_transaction( + if *opts.silent() { + let approve_result = approve( config, - transaction_address, + transactions, opts.multisig_program_id(), - opts.solido_program_id(), - None, + opts.multisig_address(), )?; - println!("{}", output); - Ok(()) - })?; - - if !ask_user_y_n("Sign and submit approval transaction?") { - println!( - "Not approving transaction {}, continuing with next transaction if any.", - transaction_address - ); + println!("{}", approve_result); return Ok(()); } - let approve_result = approve( - config, - transaction_address, - opts.multisig_program_id(), - opts.multisig_address(), - )?; - println!("{}", approve_result); - - let can_execute = approve_result.num_approvals >= approve_result.threshold; - if can_execute && ask_user_y_n("Transaction can be executed, sign and submit execution?") { + for transaction_address in transactions { + println!("\nTransaction {}", transaction_address); config.with_snapshot(|config| { - let execute_result = execute_transaction( + let output = show_transaction( config, transaction_address, opts.multisig_program_id(), - opts.multisig_address(), + opts.solido_program_id(), )?; - println!("{}", execute_result); + println!("{}", output); Ok(()) })?; + + if !ask_user_y_n("Sign and submit approval transaction?") { + println!( + "Not approving transaction {}, continuing with next transaction if any.", + transaction_address + ); + return Ok(()); + } + + let approve_result = approve( + config, + &[*transaction_address], + opts.multisig_program_id(), + opts.multisig_address(), + )?; + println!("{}", approve_result); + + let can_execute = approve_result.num_approvals[0] >= approve_result.threshold; + if can_execute && ask_user_y_n("Transaction can be executed, sign and submit execution?") { + config.with_snapshot(|config| { + let execute_result = execute_transaction( + config, + transaction_address, + opts.multisig_program_id(), + opts.multisig_address(), + )?; + println!("{}", execute_result); + Ok(()) + })?; + } } Ok(()) diff --git a/cli/maintainer/src/commands_solido.rs b/cli/maintainer/src/commands_solido.rs index 976004eac..6d30fc76b 100644 --- a/cli/maintainer/src/commands_solido.rs +++ b/cli/maintainer/src/commands_solido.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 +use std::convert::TryFrom; use std::{fmt, path::PathBuf}; use serde::Serialize; @@ -15,7 +16,7 @@ use lido::{ find_authority_program_address, metrics::LamportsHistogram, processor::StakeType, - state::{Lido, RewardDistribution}, + state::{AccountList, Lido, ListEntry, Maintainer, RewardDistribution, Validator}, token::{Lamports, StLamports}, util::serialize_b58, vote_state::get_vote_account_commission, @@ -35,9 +36,10 @@ use crate::{ }; use crate::{ config::{ - AddRemoveMaintainerOpts, AddValidatorOpts, CreateSolidoOpts, + AddRemoveMaintainerOpts, AddValidatorOpts, CreateSolidoOpts, CreateV2AccountsOpts, DeactivateValidatorIfCommissionExceedsMaxOpts, DeactivateValidatorOpts, DepositOpts, - SetMaxValidationCommissionOpts, ShowSolidoAuthoritiesOpts, ShowSolidoOpts, WithdrawOpts, + MigrateStateToV2Opts, SetMaxValidationCommissionOpts, ShowSolidoAuthoritiesOpts, + ShowSolidoOpts, WithdrawOpts, }, get_signer_from_path, }; @@ -67,6 +69,14 @@ pub struct CreateSolidoOutput { /// Authority for the minting. #[serde(serialize_with = "serialize_b58")] pub mint_authority: Pubkey, + + /// Data account that holds list of validators + #[serde(serialize_with = "serialize_b58")] + pub validator_list_address: Pubkey, + + /// Data account that holds list of maintainers + #[serde(serialize_with = "serialize_b58")] + pub maintainer_list_address: Pubkey, } impl fmt::Display for CreateSolidoOutput { @@ -106,19 +116,27 @@ impl fmt::Display for CreateSolidoOutput { } } -pub fn command_create_solido( - config: &mut SnapshotConfig, - opts: &CreateSolidoOpts, -) -> solido_cli_common::Result { +/// Get keypair from key path or random if not set +fn from_key_path_or_random(key_path: &PathBuf) -> solido_cli_common::Result> { let lido_signer = { - if opts.solido_key_path() != &PathBuf::default() { + if key_path != &PathBuf::default() { // If we've been given a solido private key, use it to create the solido instance. - get_signer_from_path(opts.solido_key_path().clone())? + get_signer_from_path(key_path.clone())? } else { // If not, use a random key Box::new(Keypair::new()) } }; + Ok(lido_signer) +} + +pub fn command_create_solido( + config: &mut SnapshotConfig, + opts: &CreateSolidoOpts, +) -> solido_cli_common::Result { + let lido_signer = from_key_path_or_random(opts.solido_key_path())?; + let validator_list_signer = from_key_path_or_random(opts.validator_list_key_path())?; + let maintainer_list_signer = from_key_path_or_random(opts.maintainer_list_key_path())?; let (reserve_account, _) = lido::find_authority_program_address( opts.solido_program_id(), @@ -135,11 +153,21 @@ pub fn command_create_solido( let (manager, _nonce) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); - let lido_size = Lido::calculate_size(*opts.max_validators(), *opts.max_maintainers()); + let lido_size = Lido::calculate_size(); let lido_account_balance = config .client .get_minimum_balance_for_rent_exemption(lido_size)?; + let validator_list_size = AccountList::::required_bytes(*opts.max_validators()); + let validator_list_account_balance = config + .client + .get_minimum_balance_for_rent_exemption(validator_list_size)?; + + let maintainer_list_size = AccountList::::required_bytes(*opts.max_maintainers()); + let maintainer_list_account_balance = config + .client + .get_minimum_balance_for_rent_exemption(maintainer_list_size)?; + let mut instructions = Vec::new(); // We need to fund Lido's reserve account so it is rent-exempt, otherwise it @@ -200,6 +228,24 @@ pub fn command_create_solido( opts.solido_program_id(), )); + // Create the account that holds the validator list itself. + instructions.push(system_instruction::create_account( + &config.signer.pubkey(), + &validator_list_signer.pubkey(), + validator_list_account_balance.0, + validator_list_size as u64, + opts.solido_program_id(), + )); + + // Create the account that holds the maintainer list itself. + instructions.push(system_instruction::create_account( + &config.signer.pubkey(), + &maintainer_list_signer.pubkey(), + maintainer_list_account_balance.0, + maintainer_list_size as u64, + opts.solido_program_id(), + )); + instructions.push(lido::instruction::initialize( opts.solido_program_id(), RewardDistribution { @@ -217,10 +263,20 @@ pub fn command_create_solido( treasury_account: treasury_keypair.pubkey(), developer_account: developer_keypair.pubkey(), reserve_account, + validator_list: validator_list_signer.pubkey(), + maintainer_list: maintainer_list_signer.pubkey(), }, )); - config.sign_and_send_transaction(&instructions[..], &[config.signer, &*lido_signer])?; + config.sign_and_send_transaction( + &instructions[..], + &[ + config.signer, + &*lido_signer, + &*validator_list_signer, + &*maintainer_list_signer, + ], + )?; eprintln!("Did send Lido init."); let result = CreateSolidoOutput { @@ -230,6 +286,8 @@ pub fn command_create_solido( st_sol_mint_address: st_sol_mint_pubkey, treasury_account: treasury_keypair.pubkey(), developer_account: developer_keypair.pubkey(), + validator_list_address: validator_list_signer.pubkey(), + maintainer_list_address: maintainer_list_signer.pubkey(), }; Ok(result) } @@ -242,12 +300,15 @@ pub fn command_add_validator( let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + let solido = config.client.get_solido(opts.solido_address())?; + let instruction = lido::instruction::add_validator( opts.solido_program_id(), &lido::instruction::AddValidatorMetaV2 { lido: *opts.solido_address(), manager: multisig_address, validator_vote_account: *opts.validator_vote_account(), + validator_list: solido.validator_list, }, ); propose_instruction( @@ -266,13 +327,24 @@ pub fn command_deactivate_validator( let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + let solido = config.client.get_solido(opts.solido_address())?; + let validators = config + .client + .get_account_list::(&solido.validator_list)?; + + let validator_index = validators + .position(opts.validator_vote_account()) + .ok_or_else(|| CliError::new("Pubkey not found in validator list"))?; + let instruction = lido::instruction::deactivate_validator( opts.solido_program_id(), - &lido::instruction::DeactivateValidatorMeta { + &lido::instruction::DeactivateValidatorMetaV2 { lido: *opts.solido_address(), manager: multisig_address, validator_vote_account_to_deactivate: *opts.validator_vote_account(), + validator_list: solido.validator_list, }, + validator_index, ); propose_instruction( config, @@ -289,12 +361,16 @@ pub fn command_add_maintainer( ) -> solido_cli_common::Result { let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + + let solido = config.client.get_solido(opts.solido_address())?; + let instruction = lido::instruction::add_maintainer( opts.solido_program_id(), - &lido::instruction::AddMaintainerMeta { + &lido::instruction::AddMaintainerMetaV2 { lido: *opts.solido_address(), manager: multisig_address, maintainer: *opts.maintainer_address(), + maintainer_list: solido.maintainer_list, }, ); propose_instruction( @@ -312,13 +388,25 @@ pub fn command_remove_maintainer( ) -> solido_cli_common::Result { let (multisig_address, _) = get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + + let solido = config.client.get_solido(opts.solido_address())?; + let maintainers = config + .client + .get_account_list::(&solido.maintainer_list)?; + + let maintainer_index = maintainers + .position(opts.maintainer_address()) + .ok_or_else(|| CliError::new("Pubkey not found in maintainer list"))?; + let instruction = lido::instruction::remove_maintainer( opts.solido_program_id(), - &lido::instruction::RemoveMaintainerMeta { + &lido::instruction::RemoveMaintainerMetaV2 { lido: *opts.solido_address(), manager: multisig_address, maintainer: *opts.maintainer_address(), + maintainer_list: solido.maintainer_list, }, + maintainer_index, ); propose_instruction( config, @@ -355,6 +443,12 @@ pub struct ShowSolidoOutput { /// Contains validator fees in the same order as `solido.validators`. pub validator_commission_percentages: Vec, + + pub validators: AccountList, + + pub maintainers: AccountList, + + pub reserve_account_balance: Lamports, } impl fmt::Display for ShowSolidoOutput { @@ -383,6 +477,8 @@ impl fmt::Display for ShowSolidoOutput { self.solido.exchange_rate.st_sol_supply )?; + writeln!(f, "\nReserve balance: {}", self.reserve_account_balance)?; + writeln!(f, "\nAuthorities (public key, bump seed):")?; writeln!( f, @@ -481,14 +577,14 @@ impl fmt::Display for ShowSolidoOutput { )?; } + writeln!(f, "\nValidator list {}", self.solido.validator_list)?; writeln!( f, - "\nValidators: {} in use out of {} that the instance can support", - self.solido.validators.len(), - self.solido.validators.maximum_entries + "Validators: {} in use out of {} that the instance can support", + self.validators.len(), + self.validators.header.max_entries )?; for (((pe, identity), info), commission) in self - .solido .validators .entries .iter() @@ -503,7 +599,7 @@ impl fmt::Display for ShowSolidoOutput { Keybase username: {}\n \ Vote account: {}\n \ Identity account: {}\n \ - Commission: {}%\n \ + Commission: {}%\n \ Active: {}\n \ Stake in all accounts: {}\n \ Stake in stake accounts: {}\n \ @@ -513,20 +609,20 @@ impl fmt::Display for ShowSolidoOutput { Some(username) => &username[..], None => "not set", }, - pe.pubkey, + pe.pubkey(), identity, commission, - pe.entry.active, - pe.entry.stake_accounts_balance, - pe.entry.effective_stake_balance(), - pe.entry.unstake_accounts_balance, + pe.active, + pe.stake_accounts_balance, + pe.effective_stake_balance, + pe.unstake_accounts_balance, )?; writeln!(f, " Stake accounts (seed, address):")?; - if pe.entry.stake_seeds.begin == pe.entry.stake_seeds.end { + if pe.stake_seeds.begin == pe.stake_seeds.end { writeln!(f, " This validator has no stake accounts.")?; }; - for seed in &pe.entry.stake_seeds { + for seed in &pe.stake_seeds { writeln!( f, " - {}: {}", @@ -542,10 +638,10 @@ impl fmt::Display for ShowSolidoOutput { } writeln!(f, " Unstake accounts (seed, address):")?; - if pe.entry.unstake_seeds.begin == pe.entry.unstake_seeds.end { + if pe.unstake_seeds.begin == pe.unstake_seeds.end { writeln!(f, " This validator has no unstake accounts.")?; }; - for seed in &pe.entry.unstake_seeds { + for seed in &pe.unstake_seeds { writeln!( f, " - {}: {}", @@ -560,14 +656,15 @@ impl fmt::Display for ShowSolidoOutput { )?; } } + writeln!(f, "\nMaintainer list {}", self.solido.maintainer_list)?; writeln!( f, - "\nMaintainers: {} in use out of {} that the instance can support\n", - self.solido.maintainers.len(), - self.solido.maintainers.maximum_entries + "Maintainers: {} in use out of {} that the instance can support\n", + self.maintainers.len(), + self.maintainers.header.max_entries )?; - for pe in &self.solido.maintainers.entries { - writeln!(f, " - {}", pe.pubkey)?; + for e in &self.maintainers.entries { + writeln!(f, " - {}", e.pubkey())?; } Ok(()) } @@ -585,16 +682,26 @@ pub fn command_show_solido( let mint_authority = lido.get_mint_authority(opts.solido_program_id(), opts.solido_address())?; + let reserve_account_balance = config.client.get_account(&reserve_account)?.lamports; + + let validators = config + .client + .get_account_list::(&lido.validator_list)?; + let maintainers = config + .client + .get_account_list::(&lido.maintainer_list)?; + let mut validator_identities = Vec::new(); let mut validator_infos = Vec::new(); let mut validator_commission_percentages = Vec::new(); - for validator in lido.validators.entries.iter() { - let vote_state = config.client.get_vote_account(&validator.pubkey)?; + for validator in validators.entries.iter() { + let vote_state = config.client.get_vote_account(validator.pubkey())?; validator_identities.push(vote_state.node_pubkey); let info = config.client.get_validator_info(&vote_state.node_pubkey)?; validator_infos.push(info); - let vote_account = config.client.get_account(&validator.pubkey)?; + let vote_account = config.client.get_account(validator.pubkey())?; let commission = get_vote_account_commission(&vote_account.data) + .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; validator_commission_percentages.push(commission); } @@ -609,6 +716,9 @@ pub fn command_show_solido( reserve_account, stake_authority, mint_authority, + validators, + maintainers, + reserve_account_balance: Lamports(reserve_account_balance), }) } @@ -820,6 +930,10 @@ pub fn command_withdraw( let (st_sol_address, new_stake_account) = config.with_snapshot(|config| { let solido = config.client.get_solido(opts.solido_address())?; + let validators = config + .client + .get_account_list::(&solido.validator_list)?; + let st_sol_address = spl_associated_token_account::get_associated_token_address( &config.signer.pubkey(), &solido.st_sol_mint, @@ -829,7 +943,7 @@ pub fn command_withdraw( solido.get_stake_authority(opts.solido_program_id(), opts.solido_address())?; // Get heaviest validator. - let heaviest_validator = get_validator_to_withdraw(&solido.validators).map_err(|err| { + let heaviest_validator = get_validator_to_withdraw(&validators).map_err(|err| { CliError::with_cause( "The instance has no active validators to withdraw from.", err, @@ -839,25 +953,30 @@ pub fn command_withdraw( let (stake_address, _bump_seed) = heaviest_validator.find_stake_account_address( opts.solido_program_id(), opts.solido_address(), - heaviest_validator.entry.stake_seeds.begin, + heaviest_validator.stake_seeds.begin, StakeType::Stake, ); let destination_stake_account = Keypair::new(); + let validator_index = validators + .position(heaviest_validator.pubkey()) + .ok_or_else(|| CliError::new("Pubkey not found in validator list"))?; let instr = lido::instruction::withdraw( opts.solido_program_id(), - &lido::instruction::WithdrawAccountsMeta { + &lido::instruction::WithdrawAccountsMetaV2 { lido: *opts.solido_address(), st_sol_mint: solido.st_sol_mint, st_sol_account_owner: config.signer.pubkey(), st_sol_account: st_sol_address, - validator_vote_account: heaviest_validator.pubkey, + validator_vote_account: *heaviest_validator.pubkey(), source_stake_account: stake_address, destination_stake_account: destination_stake_account.pubkey(), stake_authority, + validator_list: solido.validator_list, }, *opts.amount_st_sol(), + validator_index, ); config.sign_and_send_transaction(&[instr], &[config.signer, &destination_stake_account])?; @@ -916,13 +1035,17 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( ) -> solido_cli_common::Result { let solido = config.client.get_solido(opts.solido_address())?; + let validators = config + .client + .get_account_list::(&solido.validator_list)?; + let mut violations = vec![]; let mut instructions = vec![]; - for pubkey_entry in solido.validators.entries { - let validator = pubkey_entry.entry; - let vote_pubkey = pubkey_entry.pubkey; - let validator_account = config.client.get_account(&vote_pubkey)?; + for (validator_index, validator) in validators.entries.iter().enumerate() { + let vote_pubkey = validator.pubkey(); + let validator_account = config.client.get_account(vote_pubkey)?; let commission = get_vote_account_commission(&validator_account.data) + .ok() .ok_or_else(|| CliError::new("Validator account data too small"))?; if !validator.active || commission <= solido.max_commission_percentage { @@ -933,12 +1056,14 @@ pub fn command_deactivate_validator_if_commission_exceeds_max( opts.solido_program_id(), &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { lido: *opts.solido_address(), - validator_vote_account_to_deactivate: vote_pubkey, + validator_vote_account_to_deactivate: *validator.pubkey(), + validator_list: solido.validator_list, }, + u32::try_from(validator_index).expect("Too many validators"), ); instructions.push(instruction); violations.push(ValidatorViolationInfo { - validator_vote_account: vote_pubkey, + validator_vote_account: *validator.pubkey(), commission, }); } @@ -978,3 +1103,138 @@ pub fn command_set_max_commission_percentage( instruction, ) } + +#[derive(Serialize)] +pub struct CreateV2AccountsOutput { + /// Account that stores validator list data. + #[serde(serialize_with = "serialize_b58")] + validator_list_address: Pubkey, + /// Account that stores maintainer list data. + #[serde(serialize_with = "serialize_b58")] + maintainer_list_address: Pubkey, + /// Account that will receive developer stSOL fee + #[serde(serialize_with = "serialize_b58")] + developer_fee_address: Pubkey, +} + +impl fmt::Display for CreateV2AccountsOutput { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Created new v2 accounts:")?; + writeln!( + f, + " Validator list account: {}", + self.validator_list_address + )?; + writeln!( + f, + " Maintainer list account: {}", + self.maintainer_list_address + )?; + writeln!( + f, + " Developer fee account: {}", + self.developer_fee_address + )?; + Ok(()) + } +} + +/// CLI entry point to create new accounts for Solido v2. +pub fn command_create_v2_accounts( + config: &mut SnapshotConfig, + opts: &CreateV2AccountsOpts, +) -> solido_cli_common::Result { + let validator_list_signer = Keypair::new(); + let maintainer_list_signer = Keypair::new(); + + let validator_list_size = AccountList::::required_bytes(50_000); + let validator_list_account_balance = config + .client + .get_minimum_balance_for_rent_exemption(validator_list_size)?; + + let maintainer_list_size = AccountList::::required_bytes(5_000); + let maintainer_list_account_balance = config + .client + .get_minimum_balance_for_rent_exemption(maintainer_list_size)?; + + let mut instructions = Vec::new(); + + let developer_keypair = push_create_spl_token_account( + config, + &mut instructions, + opts.st_sol_mint(), + opts.developer_account_owner(), + )?; + + // Create the account that holds the validator list itself. + instructions.push(system_instruction::create_account( + &config.signer.pubkey(), + &validator_list_signer.pubkey(), + validator_list_account_balance.0, + validator_list_size as u64, + opts.solido_program_id(), + )); + + // Create the account that holds the maintainer list itself. + instructions.push(system_instruction::create_account( + &config.signer.pubkey(), + &maintainer_list_signer.pubkey(), + maintainer_list_account_balance.0, + maintainer_list_size as u64, + opts.solido_program_id(), + )); + + config.sign_and_send_transaction( + &instructions[..], + &[ + config.signer, + &validator_list_signer, + &maintainer_list_signer, + &developer_keypair, + ], + )?; + Ok(CreateV2AccountsOutput { + validator_list_address: validator_list_signer.pubkey(), + maintainer_list_address: maintainer_list_signer.pubkey(), + developer_fee_address: developer_keypair.pubkey(), + }) +} + +/// CLI entry point to update Solido state to V2 +pub fn command_migrate_state_to_v2( + config: &mut SnapshotClientConfig, + opts: &MigrateStateToV2Opts, +) -> solido_cli_common::Result { + let propose_output = config.with_snapshot(|config| { + let (multisig_address, _) = + get_multisig_program_address(opts.multisig_program_id(), opts.multisig_address()); + + let instruction = lido::instruction::migrate_state_to_v2( + opts.solido_program_id(), + RewardDistribution { + treasury_fee: *opts.treasury_fee_share(), + developer_fee: *opts.developer_fee_share(), + st_sol_appreciation: *opts.st_sol_appreciation_share(), + }, + 6_700, + 5_000, + *opts.max_commission_percentage(), + &lido::instruction::MigrateStateToV2Meta { + lido: *opts.solido_address(), + manager: multisig_address, + validator_list: *opts.validator_list_address(), + maintainer_list: *opts.maintainer_list_address(), + developer_account: *opts.developer_fee_address(), + }, + ); + + propose_instruction( + config, + opts.multisig_program_id(), + *opts.multisig_address(), + instruction, + ) + })?; + + Ok(propose_output) +} diff --git a/cli/maintainer/src/config.rs b/cli/maintainer/src/config.rs index b7fd9129d..083eeac66 100644 --- a/cli/maintainer/src/config.rs +++ b/cli/maintainer/src/config.rs @@ -11,8 +11,6 @@ use serde::Deserialize; use serde_json::Value; use solana_sdk::pubkey::{ParsePubkeyError, Pubkey}; -use anker::token::BLamports; -use anker::wormhole::TerraAddress; use lido::token::Lamports; use lido::token::StLamports; use solido_cli_common::snapshot::OutputMode; @@ -189,7 +187,13 @@ pub struct ConfigFile { } pub fn read_config(config_path: &Path) -> ConfigFile { - let file_content = std::fs::read(config_path).expect("Failed to open config file."); + let file_content = match std::fs::read(config_path) { + Ok(content) => content, + Err(err) => { + eprintln!("{}: {:?}.", err, config_path); + std::process::exit(0x0100); + } + }; let values: Value = serde_json::from_slice(&file_content).expect("Error while reading config."); ConfigFile { values } } @@ -284,6 +288,17 @@ cli_opt_struct! { #[clap(long)] solido_key_path: PathBuf => PathBuf::default(), + /// Optional argument for the validator list address, if not passed a random one + /// will be created. + #[clap(long)] + validator_list_key_path: PathBuf => PathBuf::default(), + + /// Optional argument for the maintainer list address, if not passed a random one + /// will be created. + #[clap(long)] + maintainer_list_key_path: PathBuf => PathBuf::default(), + + /// Used to compute Solido's manager. Multisig instance. #[clap(long, value_name = "address")] multisig_address: Pubkey, @@ -419,28 +434,12 @@ cli_opt_struct! { } } -cli_opt_struct! { - ShowAnkerAuthoritiesOpts { - /// The Solido instance, used to derive the Anker instance. - #[clap(long, value_name = "address")] - solido_address: Pubkey, - - /// Address of the Anker program. - #[clap(long, value_name = "address")] - anker_program_id: Pubkey, - } -} - cli_opt_struct! { PerformMaintenanceOpts { /// Address of the Solido program. #[clap(long, value_name = "address")] solido_program_id: Pubkey, - /// Address of the Anker program. - #[clap(long, value_name = "address")] - anker_program_id: Pubkey => Pubkey::default(), - /// Account that stores the data for this Solido instance. #[clap(long, value_name = "address")] solido_address: Pubkey, @@ -451,6 +450,12 @@ cli_opt_struct! { /// "anytime" option is only intended for testing purposes. #[clap(long, value_name = "anytime/only-near-epoch-end")] stake_time: StakeTime => StakeTime::OnlyNearEpochEnd, + + /// Threshold for when to consider the end of an epoch. + /// E.g. if set to 95, the end of epoch would be considered if the system + /// is past 95% of the epoch's time. + #[clap(long)] + end_of_epoch_threshold: u8 => 95, } } @@ -574,10 +579,6 @@ cli_opt_struct! { /// Address of the Multisig program. #[clap(long)] multisig_program_id: Pubkey, - - /// Address of the Anker program. - #[clap(long, value_name = "address")] - anker_program_id: Pubkey => Pubkey::default(), } } @@ -632,6 +633,10 @@ cli_opt_struct! { /// Address of the Solido program. #[clap(long)] solido_program_id: Pubkey, + + /// Don't interactively ask to check each transaction before signing + #[clap(long, takes_value = false, action = clap::ArgAction::SetTrue)] + silent: bool } } @@ -681,10 +686,6 @@ cli_opt_struct! { #[clap(long)] solido_program_id: Pubkey, - /// Address of the Anker program. - #[clap(long, value_name = "address")] - anker_program_id: Pubkey => Pubkey::default(), - /// Account that stores the data for this Solido instance. #[clap(long)] solido_address: Pubkey, @@ -706,6 +707,12 @@ cli_opt_struct! { /// "anytime" option is only intended for testing purposes. #[clap(long, value_name = "anytime/only-near-epoch-end")] stake_time: StakeTime => StakeTime::OnlyNearEpochEnd, + + /// Threshold for when to consider the end of an epoch. + /// E.g. if set to 95, the end of epoch would be considered if the system + /// is past 95% of the epoch's time. + #[clap(long)] + end_of_epoch_threshold: u8 => 95, } } @@ -743,81 +750,6 @@ cli_opt_struct! { } } -cli_opt_struct! { - CreateAnkerOpts { - /// Address of the Solido program. - #[clap(long, value_name = "address")] - solido_program_id: Pubkey, - - /// Account that stores the data for the underlying Solido instance. - #[clap(long, value_name = "address")] - solido_address: Pubkey, - - /// Address of the Anker program. - #[clap(long, value_name = "address")] - anker_program_id: Pubkey, - - /// Address of the Wormhole core bridge program. - #[clap(long, value_name = "address")] - wormhole_core_bridge_program_id: Pubkey, - - /// Address of the Wormhole token bridge program. - #[clap(long, value_name = "address")] - wormhole_token_bridge_program_id: Pubkey, - - /// Optionally the bSOL mint address. If not passed a random one will be created. - #[clap(long, value_name = "address")] - b_sol_mint_address: Pubkey, - - /// The UST mint address. - /// - /// The mainnet address of Wormhole-v2 wrapped UST is - /// 9vMJfxuKxXBoEa7rM12mYLMwTacLMLDJqHozw96WQL8i. - #[clap(long, value_name = "address")] - ust_mint_address: Pubkey, - - /// Orca (or other SPL token swap) pool used for stSOL/UST swap. - #[clap(long, value_name = "address")] - token_swap_pool: Pubkey, - - /// Terra address that will receive the UST rewards. - /// - /// Must be provided in the usual Terra bech32 encoding. - #[clap(long, value_name = "terra_address")] - terra_rewards_address: TerraAddress, - - /// Minimum fraction of the expected proceeds for which selling rewards is allowed, in basis points. - /// - /// To prevent rewards selling from being sandwiched, Anker tracks recent - /// prices of the pool. Based on the median of recent prices, it has an - /// "expected" amount of the proceeds. If the actual proceeds would be - /// lower than `sell_rewards_min_out_bps / 1e4` times the expected proceeds, - /// selling rewards is not allowed. Lower values allow more slippage and - /// sandwiching, higher values protect against this, but can make it more - /// difficult to sell rewards at times of high volatility. To allow 1% - /// slippage w.r.t. the expected price, set this value to 9900 bps. - /// - /// This fraction includes the swap fee. For example, if there is a 5% - /// swap fee, then this setting should be set to less than 9500, because - /// it is unlikely that the actual proceeds are more than 95% of the - /// expected proceeds. In other words, the expected proceeds do not take - /// the swap fee into account. - /// - /// NB: This means that values greater than 9999 will likely prevent - /// Anker from ever selling rewards. - #[clap(long, value_name = "basis points")] - sell_rewards_min_out_bps: u64, - } -} - -cli_opt_struct! { - ShowAnkerOpts { - /// Address of the Anker instance. - #[clap(long, value_name = "address")] - anker_address: Pubkey, - } -} - cli_opt_struct! { CreateTokenPoolOpts { /// Program id of the token swap program. @@ -842,100 +774,60 @@ cli_opt_struct! { } cli_opt_struct! { - AnkerDepositOpts { - /// Address of the Anker instance. - #[clap(long, value_name = "address")] - anker_address: Pubkey, - - /// stSOL SPL token account to send from. - /// - /// By default, the stSOL associated token account of the signer is used. - /// In any case, the signer must own this account. - #[clap(long, value_name = "address")] - from_st_sol_address: Pubkey => Pubkey::default(), - - /// Amount to deposit, in stSOL, using . as decimal separator. - #[clap(long, value_name = "amount")] - amount_st_sol: StLamports, - } -} - -cli_opt_struct! { - AnkerWithdrawOpts { - /// Address of the Anker instance. + CreateV2AccountsOpts { + /// Address of the Solido program #[clap(long, value_name = "address")] - anker_address: Pubkey, + solido_program_id: Pubkey, - /// bSOL SPL token account from where we will remove the bSOL. - /// - /// By default, the bSOL associated token account of the signer is used. - /// In any case, the signer must own this account. + /// stSol mint address, used to create SPL token developer account #[clap(long, value_name = "address")] - from_b_sol_address: Pubkey => Pubkey::default(), + st_sol_mint: Pubkey, - /// stSOL SPL token account that will receive the stSOL. - /// - /// By default, the stSOL associated token account of the signer is used. + /// Account who will own the stSOL SPL token account that receives the developer fees. #[clap(long, value_name = "address")] - to_st_sol_address: Pubkey => Pubkey::default(), - - /// Amount to withdraw, in bSOL, using . as decimal separator. - #[clap(long, value_name = "amount")] - amount_b_sol: BLamports, + developer_account_owner: Pubkey, } } cli_opt_struct! { - AnkerChangeTerraRewardsDestinationOpts { - /// Address of the Anker instance. + MigrateStateToV2Opts { + /// Address of the Solido program #[clap(long, value_name = "address")] - anker_address: Pubkey, + solido_program_id: Pubkey, - /// New terra rewards address. + /// Solido address #[clap(long, value_name = "address")] - terra_rewards_destination: TerraAddress, + solido_address: Pubkey, - /// Multisig instance. - #[clap(long, value_name = "address")] - multisig_address: Pubkey, + /// The maximum validator fee a validator can have to be accepted by protocol. + #[clap(long, value_name = "int")] + max_commission_percentage: u8, - /// Address of the Multisig program. - #[clap(long)] - multisig_program_id: Pubkey, - } -} + // See also the docs section of `create-solido` in main.rs for a description + // of the fee shares. + /// Treasury fee share of the rewards. + #[clap(long, value_name = "int")] + treasury_fee_share: u32, -cli_opt_struct! { - AnkerChangeTokenSwapPoolOpts { - /// Address of the Anker instance. - #[clap(long, value_name = "address")] - anker_address: Pubkey, + /// Developer fee share of the rewards. + #[clap(long, value_name = "int")] + developer_fee_share: u32, - /// New token swap pool address. - #[clap(long, value_name = "address")] - token_swap_pool: Pubkey, + /// Share of the rewards that goes to stSOL appreciation (the non-fee part). + #[clap(long, value_name = "int")] + st_sol_appreciation_share: u32, - /// Multisig instance. + /// Account who will receive stSOL developer fees. #[clap(long, value_name = "address")] - multisig_address: Pubkey, + developer_fee_address: Pubkey, - /// Address of the Multisig program. + /// Validator list data address #[clap(long)] - multisig_program_id: Pubkey, - } -} + validator_list_address: Pubkey, -cli_opt_struct! { - AnkerChangeSellRewardsMinOutBpsOpts { - /// Address of the Anker instance. - #[clap(long, value_name = "address")] - anker_address: Pubkey, - - /// New Anker's `sell_rewards_min_out_bps`. - // - // See also `anker create --sell-rewards-min-out-bps`. - #[clap(long, value_name = "basis points")] - sell_rewards_min_out_bps: u64, + /// Maintainer list data address + #[clap(long)] + maintainer_list_address: Pubkey, /// Multisig instance. #[clap(long, value_name = "address")] diff --git a/cli/maintainer/src/daemon.rs b/cli/maintainer/src/daemon.rs index fd4c71e02..2dd0f5cda 100644 --- a/cli/maintainer/src/daemon.rs +++ b/cli/maintainer/src/daemon.rs @@ -43,7 +43,7 @@ struct MaintenanceMetrics { /// Number of times we performed `UpdateExchangeRate`. transactions_update_exchange_rate: u64, - /// Number of times we performed `WithdrawInactiveStake`. + /// Number of times we performed `UpdateStakeAccountBalance`. transactions_update_stake_account_balance: u64, /// Number of times we performed a `MergeStake`. @@ -63,12 +63,6 @@ struct MaintenanceMetrics { /// Number of times we performed `Unstake` on an active validator for balancing purposes. transactions_unstake_from_active_validator: u64, - - /// Number of times we performed `SellRewards` on the Anker instance. - transactions_sell_rewards: u64, - - /// Number of times we performed `FetchPoolPrice` on the Anker instance. - transactions_fetch_pool_price: u64, } impl MaintenanceMetrics { @@ -111,10 +105,6 @@ impl MaintenanceMetrics { .with_label("operation", "RemoveValidator".to_string()), Metric::new(self.transactions_unstake_from_active_validator) .with_label("operation", "UnstakeFromActiveValidator".to_string()), - Metric::new(self.transactions_sell_rewards) - .with_label("operation", "SellRewards".to_string()), - Metric::new(self.transactions_fetch_pool_price) - .with_label("operation", "FetchPoolPrice".to_string()), Metric::new(self.transactions_deactivate_validator_if_commission_exceeds_max) .with_label( "operation", @@ -135,7 +125,7 @@ impl MaintenanceMetrics { MaintenanceOutput::UpdateExchangeRate => { self.transactions_update_exchange_rate += 1; } - MaintenanceOutput::WithdrawInactiveStake { .. } => { + MaintenanceOutput::UpdateStakeAccountBalance { .. } => { self.transactions_update_stake_account_balance += 1; } MaintenanceOutput::MergeStake { .. } => self.transactions_merge_stake += 1, @@ -149,8 +139,6 @@ impl MaintenanceMetrics { MaintenanceOutput::UnstakeFromActiveValidator { .. } => { self.transactions_unstake_from_active_validator += 1 } - MaintenanceOutput::SellRewards { .. } => self.transactions_sell_rewards += 1, - MaintenanceOutput::FetchPoolPrice { .. } => self.transactions_fetch_pool_price += 1, } } } @@ -188,9 +176,9 @@ fn run_maintenance_iteration( let state = SolidoState::new( config, opts.solido_program_id(), - opts.anker_program_id(), opts.solido_address(), *opts.stake_time(), + *opts.end_of_epoch_threshold(), )?; // If it's not our maintainer duty at this time, then don't try to @@ -312,8 +300,6 @@ impl<'a, 'b> Daemon<'a, 'b> { transactions_remove_validator: 0, transactions_deactivate_validator_if_commission_exceeds_max: 0, transactions_unstake_from_active_validator: 0, - transactions_sell_rewards: 0, - transactions_fetch_pool_price: 0, }; Daemon { config, diff --git a/cli/maintainer/src/main.rs b/cli/maintainer/src/main.rs index 601129517..d8ad7cc10 100644 --- a/cli/maintainer/src/main.rs +++ b/cli/maintainer/src/main.rs @@ -17,24 +17,21 @@ use solana_sdk::signer::Signer; use solido_cli_common::error::{Abort, CliError, Error}; use solido_cli_common::snapshot::{Config, OutputMode, SnapshotClient}; -use crate::commands_anker::AnkerOpts; use crate::commands_multisig::MultisigOpts; use crate::commands_solido::{ command_add_maintainer, command_add_validator, command_create_solido, - command_deactivate_validator, command_deactivate_validator_if_commission_exceeds_max, - command_deposit, command_remove_maintainer, command_set_max_commission_percentage, + command_create_v2_accounts, command_deactivate_validator, + command_deactivate_validator_if_commission_exceeds_max, command_deposit, + command_migrate_state_to_v2, command_remove_maintainer, command_set_max_commission_percentage, command_show_solido, command_show_solido_authorities, command_withdraw, }; use crate::config::*; -mod anker_state; -mod commands_anker; mod commands_multisig; mod commands_solido; mod config; mod daemon; mod maintenance; -mod serialization_utils; mod spl_token_utils; /// Solido -- Interact with Lido for Solana. @@ -47,7 +44,9 @@ mod spl_token_utils; // we write the default values on the rustdoc so Clap can print them in help // messages. #[derive(Parser, Debug)] -#[clap(after_long_help = r#"CONFIGURATION: +#[clap( + version, + after_long_help = r#"CONFIGURATION: All of the options of this program can also be provided as an environment variable with "SOLIDO_" prefix. E.g. to provide --keypair-path, set the SOLIDO_KEYPAIR_PATH environment variable. @@ -61,7 +60,8 @@ mod spl_token_utils; { "cluster": "https://api.mainnet-beta.solana.com", "keypair_path": "/path/to/id.json" - }"#)] + }"# +)] struct Opts { /// The contents of a keypair file to sign and pay with, as json array. /// @@ -205,15 +205,18 @@ REWARDS /// Interact with a deployed Multisig program for governance tasks. Multisig(MultisigOpts), - /// Interact with the Anker (Anchor Protocol integration) program. - Anker(AnkerOpts), - /// Set max_commission_percentage to control validator's fees. /// If validators exeed the threshold they will be deactivated by /// a maintainer. /// /// Requires the manager to sign. SetMaxValidationCommission(SetMaxValidationCommissionOpts), + + /// Update Solido state to V2 + MigrateStateToV2(MigrateStateToV2Opts), + + /// Create new Solido V2 accounts + CreateV2Accounts(CreateV2AccountsOpts), } fn print_output(mode: OutputMode, output: &Output) { @@ -263,9 +266,18 @@ fn main() { output_mode, }; + fn check_end_of_epoch_threshold(threshold: u8) { + if threshold > 100 { + eprint!( + "End of epoch threshold should be less than 100, but is {}", + threshold + ); + std::process::exit(1); + } + } + merge_with_config_and_environment(&mut opts.subcommand, config_file.as_ref()); match opts.subcommand { - SubCommand::Anker(cmd_opts) => commands_anker::main(&mut config, &cmd_opts), SubCommand::CreateSolido(cmd_opts) => { let result = config.with_snapshot(|config| command_create_solido(config, &cmd_opts)); let output = result.ok_or_abort_with("Failed to create Solido instance."); @@ -273,6 +285,7 @@ fn main() { } SubCommand::Multisig(cmd_opts) => commands_multisig::main(&mut config, cmd_opts), SubCommand::PerformMaintenance(cmd_opts) => { + check_end_of_epoch_threshold(*cmd_opts.end_of_epoch_threshold()); // This command only performs one iteration, `RunMaintainer` runs continuously. let result = config .with_snapshot(|config| maintenance::run_perform_maintenance(config, &cmd_opts)); @@ -287,6 +300,7 @@ fn main() { } } SubCommand::RunMaintainer(cmd_opts) => { + check_end_of_epoch_threshold(*cmd_opts.end_of_epoch_threshold()); daemon::main(&mut config, &cmd_opts); } SubCommand::AddValidator(cmd_opts) => { @@ -346,6 +360,17 @@ fn main() { let output = result.ok_or_abort_with("Failed to set max validation commission."); print_output(output_mode, &output); } + SubCommand::MigrateStateToV2(cmd_opts) => { + let result = command_migrate_state_to_v2(&mut config, &cmd_opts); + let output = result.ok_or_abort_with("Failed to update Solido state to V2."); + print_output(output_mode, &output); + } + SubCommand::CreateV2Accounts(cmd_opts) => { + let result = + config.with_snapshot(|config| command_create_v2_accounts(config, &cmd_opts)); + let output = result.ok_or_abort_with("Failed to create new Solido V2 accounts."); + print_output(output_mode, &output); + } } } @@ -354,7 +379,6 @@ fn merge_with_config_and_environment( config_file: Option<&ConfigFile>, ) { match subcommand { - SubCommand::Anker(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::CreateSolido(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::AddValidator(opts) => opts.merge_with_config_and_environment(config_file), SubCommand::DeactivateValidator(opts) => { @@ -376,6 +400,8 @@ fn merge_with_config_and_environment( SubCommand::SetMaxValidationCommission(opts) => { opts.merge_with_config_and_environment(config_file) } + SubCommand::MigrateStateToV2(opts) => opts.merge_with_config_and_environment(config_file), + SubCommand::CreateV2Accounts(opts) => opts.merge_with_config_and_environment(config_file), } } diff --git a/cli/maintainer/src/maintenance.rs b/cli/maintainer/src/maintenance.rs index b13c2ed3d..5ee83d183 100644 --- a/cli/maintainer/src/maintenance.rs +++ b/cli/maintainer/src/maintenance.rs @@ -3,15 +3,11 @@ //! Entry point for maintenance operations, such as updating the pool balance. +use std::convert::TryFrom; use std::fmt; use std::io; use std::time::SystemTime; -use anker::{ - logic::get_one_st_sol_for_ust_price_from_pool, - state::{POOL_PRICE_MAX_SAMPLE_AGE, POOL_PRICE_MIN_SAMPLE_DISTANCE}, - token::MicroUst, -}; use itertools::izip; use serde::Serialize; @@ -28,28 +24,27 @@ use solana_sdk::{ fee_calculator::DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, instruction::Instruction, signer::{keypair::Keypair, Signer}, + stake::state::StakeState, }; use solana_vote_program::vote_state::VoteState; use solido_cli_common::{ - error::MaintenanceError, snapshot::SnapshotConfig, validator_info_utils::ValidatorInfo, Result, + error::MaintenanceError, snapshot::SnapshotConfig, snapshot::SnapshotError, + validator_info_utils::ValidatorInfo, Result, }; use spl_token::state::Mint; use lido::{ - account_map::PubkeyAndEntry, processor::StakeType, stake_account::StakeAccount, stake_account::{deserialize_stake_account, StakeBalance}, - state::{Lido, Validator}, + state::{AccountList, Lido, ListEntry, Maintainer, Validator}, token::Lamports, token::Rational, token::StLamports, util::serialize_b58, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, STAKE_AUTHORITY, }; -use spl_token_swap::curve::calculator::{CurveCalculator, TradeDirection}; -use crate::anker_state::AnkerState; use crate::config::{PerformMaintenanceOpts, StakeTime}; /// A brief description of the maintenance performed. Not relevant functionally, @@ -69,7 +64,7 @@ pub enum MaintenanceOutput { UpdateExchangeRate, - WithdrawInactiveStake { + UpdateStakeAccountBalance { /// The vote account of the validator that we want to update. #[serde(serialize_with = "serialize_b58")] validator_vote_account: Pubkey, @@ -78,7 +73,7 @@ pub enum MaintenanceOutput { /// /// This is only an expected value, because a different transaction might /// execute between us observing the state and concluding that there is - /// a difference, and our `WithdrawInactiveStake` instruction executing. + /// a difference, and our `UpdateStakeAccountBalance` instruction executing. #[serde(rename = "expected_difference_stake_lamports")] expected_difference_stake: Lamports, @@ -107,16 +102,6 @@ pub enum MaintenanceOutput { validator_vote_account: Pubkey, }, UnstakeFromActiveValidator(Unstake), - - FetchPoolPrice { - #[serde(rename = "st_sol_price_in_micro_ust")] - expected_st_sol_price_in_ust: MicroUst, - }, - - SellRewards { - #[serde(rename = "st_sol_amount_st_lamports")] - st_sol_amount: StLamports, - }, } #[derive(Debug, Eq, PartialEq, Serialize)] @@ -170,12 +155,12 @@ impl fmt::Display for MaintenanceOutput { MaintenanceOutput::UpdateExchangeRate => { writeln!(f, "Updated exchange rate.")?; } - MaintenanceOutput::WithdrawInactiveStake { + MaintenanceOutput::UpdateStakeAccountBalance { validator_vote_account, expected_difference_stake, unstake_withdrawn_to_reserve, } => { - writeln!(f, "Withdrew inactive stake.")?; + writeln!(f, "Updated stake account balance.")?; writeln!( f, " Validator vote account: {}", @@ -234,20 +219,6 @@ impl fmt::Display for MaintenanceOutput { )?; writeln!(f, " Validator vote account: {}", validator_vote_account)?; } - MaintenanceOutput::SellRewards { st_sol_amount } => { - writeln!(f, "Sell stSOL rewards")?; - writeln!(f, " Amount: {}", st_sol_amount)?; - } - MaintenanceOutput::FetchPoolPrice { - expected_st_sol_price_in_ust, - } => { - writeln!(f, "Fetch Pool Price")?; - writeln!( - f, - " Expected amount per stSOL: {}", - expected_st_sol_price_in_ust - )?; - } } Ok(()) } @@ -294,9 +265,6 @@ pub struct SolidoState { pub solido_address: Pubkey, pub solido: Lido, - /// Anker parameters - pub anker_state: Option, - /// For each validator, in the same order as in `solido.validators`, holds /// the stake balance of the derived stake accounts from the begin seed until /// end seed. @@ -311,8 +279,8 @@ pub struct SolidoState { pub validator_vote_account_balances: Vec, /// For each validator, in the same order as in `solido.validators`, holds - /// the deserialized vote account. - pub validator_vote_accounts: Vec, + /// the deserialized vote account or None if vote account is closed. + pub validator_vote_accounts: Vec>, /// For each validator, in the same order as in `solido.validators`, holds /// the balance of the validator's identity account (which pays for the @@ -346,6 +314,15 @@ pub struct SolidoState { /// whenever possible. If set to StakeTime::OnlyNearEpochEnd the /// instructions are issued only close to the end of epoch. pub stake_time: StakeTime, + + /// Parsed list entries from list accounts + pub validators: AccountList, + pub maintainers: AccountList, + + /// Threshold for when to consider the end of an epoch. + /// E.g. if set to 95, the end of epoch would be considered if the system + /// is past 95% of the epoch's time. + pub end_of_epoch_threshold: u8, } fn get_validator_stake_accounts( @@ -354,13 +331,13 @@ fn get_validator_stake_accounts( solido_address: &Pubkey, clock: &Clock, stake_history: &StakeHistory, - validator: &PubkeyAndEntry, + validator: &Validator, stake_type: StakeType, ) -> Result> { let mut result = Vec::new(); let seeds = match stake_type { - StakeType::Stake => &validator.entry.stake_seeds, - StakeType::Unstake => &validator.entry.unstake_seeds, + StakeType::Stake => &validator.stake_seeds, + StakeType::Unstake => &validator.unstake_seeds, }; for seed in seeds { let (addr, _bump_seed) = validator.find_stake_account_address( @@ -374,7 +351,8 @@ fn get_validator_stake_accounts( .expect("Derived stake account contains invalid data."); assert_eq!( - stake.delegation.voter_pubkey, validator.pubkey, + &stake.delegation.voter_pubkey, + validator.pubkey(), "Expected the stake account for validator to delegate to that validator." ); @@ -440,24 +418,23 @@ impl SolidoState { denominator: 10, }; - /// Threshold for when to consider the end of an epoch. - /// E.g. if set to 19/20, the end of epoch would be considered if the system - /// is past 95% of the epoch's time. - const END_OF_EPOCH_THRESHOLD: Rational = Rational { - numerator: 19, - denominator: 20, - }; - /// Read the state from the on-chain data. pub fn new( config: &mut SnapshotConfig, solido_program_id: &Pubkey, - anker_program_id: &Pubkey, solido_address: &Pubkey, stake_time: StakeTime, + end_of_epoch_threshold: u8, ) -> Result { let solido = config.client.get_solido(solido_address)?; + let validators = config + .client + .get_account_list::(&solido.validator_list)?; + let maintainers = config + .client + .get_account_list::(&solido.maintainer_list)?; + let reserve_address = solido.get_reserve_account(solido_program_id, solido_address)?; let reserve_account = config.client.get_account(&reserve_address)?; @@ -475,17 +452,35 @@ impl SolidoState { let mut validator_identity_account_balances = Vec::new(); let mut validator_vote_accounts = Vec::new(); let mut validator_infos = Vec::new(); - for validator in solido.validators.entries.iter() { - let vote_account = config.client.get_account(&validator.pubkey)?; - let vote_state = config.client.get_vote_account(&validator.pubkey)?; - let validator_info = config.client.get_validator_info(&vote_state.node_pubkey)?; - let identity_account = config.client.get_account(&vote_state.node_pubkey)?; - validator_vote_accounts.push(vote_state); - validator_vote_account_balances - .push(get_account_balance_except_rent(&rent, vote_account)); - validator_identity_account_balances - .push(get_account_balance_except_rent(&rent, identity_account)); - validator_infos.push(validator_info); + for validator in validators.entries.iter() { + match config.client.get_account(validator.pubkey()) { + Ok(vote_account) => { + let vote_state = config.client.get_vote_account(validator.pubkey())?; + + // prometheus + validator_vote_account_balances + .push(get_account_balance_except_rent(&rent, vote_account)); + let validator_info = + config.client.get_validator_info(&vote_state.node_pubkey)?; + let identity_account = config.client.get_account(&vote_state.node_pubkey)?; + validator_identity_account_balances + .push(get_account_balance_except_rent(&rent, identity_account)); + validator_infos.push(validator_info); + + validator_vote_accounts.push(Some(vote_state)); + } + Err(err) => match err { + SnapshotError::OtherError(_) => { + // Vote account will not exist if it was closed by node operator. + // It is possible to close a vote account only with inactive stake + // or with no stake, in first case the stake will be withdrawn to + // a reserve and in both cases the validator will be removed + // by a maintainer + validator_vote_accounts.push(None); + } + other => return Err(other), + }, + }; validator_stake_accounts.push(get_validator_stake_accounts( config, @@ -508,9 +503,9 @@ impl SolidoState { } let mut maintainer_balances = Vec::new(); - for maintainer in solido.maintainers.entries.iter() { + for maintainer in maintainers.entries.iter() { maintainer_balances.push(Lamports( - config.client.get_account(&maintainer.pubkey)?.lamports, + config.client.get_account(maintainer.pubkey())?.lamports, )); } @@ -519,25 +514,11 @@ impl SolidoState { // program does that anyway. let maintainer_address = config.signer.pubkey(); - let anker_state = if anker_program_id == &Pubkey::default() { - None - } else { - let (anker_address, _bump_seed) = - anker::find_instance_address(anker_program_id, solido_address); - Some(AnkerState::new( - config, - anker_program_id, - &anker_address, - &solido, - )?) - }; - Ok(SolidoState { produced_at: SystemTime::now(), solido_program_id: *solido_program_id, solido_address: *solido_address, solido, - anker_state, validator_stake_accounts, validator_unstake_accounts, validator_vote_account_balances, @@ -554,6 +535,9 @@ impl SolidoState { stake_history, maintainer_address, stake_time, + validators, + maintainers, + end_of_epoch_threshold, }) } @@ -572,7 +556,7 @@ impl SolidoState { self.confirm_should_stake_unstake_in_current_slot()?; // We can only stake if there is an active validator. If there is none, // this will short-circuit and return None. - self.solido.validators.iter_active().next()?; + self.validators.iter_active().next()?; let reserve_balance = self.get_effective_reserve(); @@ -581,24 +565,13 @@ impl SolidoState { // deposit to that validator. If we get here there is at least one active // validator, so computing the target balance should not fail. let undelegated_lamports = reserve_balance; - let targets = - lido::balance::get_target_balance(undelegated_lamports, &self.solido.validators) - .expect("Failed to compute target balance."); + let targets = lido::balance::get_target_balance(undelegated_lamports, &self.validators) + .expect("Failed to compute target balance."); let (validator_index, amount_below_target) = - lido::balance::get_minimum_stake_validator_index_amount( - &self.solido.validators, - &targets[..], - ); + lido::balance::get_minimum_stake_validator_index_amount(&self.validators, &targets[..]); - let validator = &self.solido.validators.entries[validator_index]; - - let (stake_account_end, _bump_seed_end) = validator.find_stake_account_address( - &self.solido_program_id, - &self.solido_address, - validator.entry.stake_seeds.end, - StakeType::Stake, - ); + let validator = &self.validators.entries[validator_index]; // Top up the validator to at most its target. If that means we don't use the full // reserve, a future maintenance run will stake the remainder with the next validator. @@ -624,26 +597,53 @@ impl SolidoState { // activated in the current epoch. If merging is not possible, then we // set `account_merge_into` to the same account as `end`, to signal that // we shouldn't merge. - let account_merge_into = match self.validator_stake_accounts[validator_index].last() { - Some((addr, account)) if account.activation_epoch == self.clock.epoch => *addr, - _ => stake_account_end, - }; + let (stake_account_end, account_merge_into) = + match self.validator_stake_accounts[validator_index].last() { + // Merge + Some((addr, account)) if account.activation_epoch == self.clock.epoch => { + let (stake_account_end, _) = validator.find_temporary_stake_account_address( + &self.solido_program_id, + &self.solido_address, + validator.stake_seeds.end, + self.clock.epoch, + ); + + (stake_account_end, *addr) + } + // Append + _ => { + let (stake_account_end, _) = validator.find_stake_account_address( + &self.solido_program_id, + &self.solido_address, + validator.stake_seeds.end, + StakeType::Stake, + ); + + (stake_account_end, stake_account_end) + } + }; + + let maintainer_index = self.maintainers.position(&self.maintainer_address)?; let instruction = lido::instruction::stake_deposit( &self.solido_program_id, - &lido::instruction::StakeDepositAccountsMeta { + &lido::instruction::StakeDepositAccountsMetaV2 { lido: self.solido_address, maintainer: self.maintainer_address, reserve: self.reserve_address, - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), stake_account_merge_into: account_merge_into, stake_account_end, stake_authority: self.get_stake_authority(), + validator_list: self.solido.validator_list, + maintainer_list: self.solido.maintainer_list, }, amount_to_deposit, + u32::try_from(validator_index).expect("Too many validators"), + maintainer_index, ); let task = MaintenanceOutput::StakeDeposit { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), amount: amount_to_deposit, stake_account: stake_account_end, }; @@ -654,38 +654,45 @@ impl SolidoState { /// unstake `amount` from it. pub fn get_unstake_instruction( &self, - validator: &PubkeyAndEntry, + validator: &Validator, stake_account: &(Pubkey, StakeAccount), amount: Lamports, - ) -> (Pubkey, Instruction) { + ) -> Option<(Pubkey, Instruction)> { let (validator_unstake_account, _) = validator.find_stake_account_address( &self.solido_program_id, &self.solido_address, - validator.entry.unstake_seeds.end, + validator.unstake_seeds.end, StakeType::Unstake, ); + + let validator_index = self.validators.position(validator.pubkey())?; + let maintainer_index = self.maintainers.position(&self.maintainer_address)?; + let (stake_account_address, _) = stake_account; - ( + Some(( validator_unstake_account, lido::instruction::unstake( &self.solido_program_id, - &lido::instruction::UnstakeAccountsMeta { + &lido::instruction::UnstakeAccountsMetaV2 { lido: self.solido_address, maintainer: self.maintainer_address, - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), source_stake_account: *stake_account_address, destination_unstake_account: validator_unstake_account, stake_authority: self.get_stake_authority(), + validator_list: self.solido.validator_list, + maintainer_list: self.solido.maintainer_list, }, amount, + validator_index, + maintainer_index, ), - ) + )) } /// If there is a validator being deactivated, try to unstake its funds. pub fn try_unstake_from_inactive_validator(&self) -> Option { for (validator, stake_accounts) in self - .solido .validators .entries .iter() @@ -693,11 +700,11 @@ impl SolidoState { { // We are only interested in unstaking from inactive validators that // have stake accounts. - if validator.entry.active { + if validator.active { continue; } // Validator already has 3 unstake accounts. - if validator.entry.unstake_seeds.end - validator.entry.unstake_seeds.begin + if validator.unstake_seeds.end - validator.unstake_seeds.begin >= lido::MAXIMUM_UNSTAKE_ACCOUNTS { continue; @@ -711,13 +718,13 @@ impl SolidoState { validator, &stake_accounts[0], stake_account_balance.balance.total(), - ); + )?; let task = MaintenanceOutput::UnstakeFromInactiveValidator(Unstake { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), from_stake_account: stake_account_address, to_unstake_account: unstake_account, - from_stake_seed: validator.entry.stake_seeds.begin, - to_unstake_seed: validator.entry.unstake_seeds.end, + from_stake_seed: validator.stake_seeds.begin, + to_unstake_seed: validator.unstake_seeds.end, amount: stake_account_balance.balance.total(), }); @@ -726,34 +733,43 @@ impl SolidoState { None } - /// If there is a validator which exceeded commission limit, try to deactivate it. + /// If there is a validator which exceeded commission limit or it's vote account is closed, + /// try to deactivate it. pub fn try_deactivate_validator_if_commission_exceeds_max( &self, ) -> Option { - for (validator, vote_state) in self - .solido + for (validator_index, (validator, vote_state)) in self .validators .entries .iter() .zip(self.validator_vote_accounts.iter()) + .enumerate() { - // We are only interested in validators that violate commission limit - if !validator.entry.active - || vote_state.commission <= self.solido.max_commission_percentage - { + if !validator.active { continue; } + // We are only interested in validators that violate commission limit + if let Some(state) = vote_state { + if state.commission <= self.solido.max_commission_percentage { + continue; + } + } else { + // Vote account is closed + } + let task = MaintenanceOutput::DeactivateValidatorIfCommissionExceedsMax { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), }; let instruction = lido::instruction::deactivate_validator_if_commission_exceeds_max( &self.solido_program_id, &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { lido: self.solido_address, - validator_vote_account_to_deactivate: validator.pubkey, + validator_vote_account_to_deactivate: *validator.pubkey(), + validator_list: self.solido.validator_list, }, + u32::try_from(validator_index).expect("Too many validators"), ); return Some(MaintenanceInstruction::new(instruction, task)); } @@ -762,143 +778,36 @@ impl SolidoState { /// If there is a validator ready for removal, try to remove it. pub fn try_remove_validator(&self) -> Option { - for validator in &self.solido.validators.entries { + for (validator_index, validator) in self.validators.entries.iter().enumerate() { // We are only interested in validators that can be removed. - if validator.entry.check_can_be_removed().is_err() { + if validator.check_can_be_removed().is_err() { continue; } let task = MaintenanceOutput::RemoveValidator { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), }; let instruction = lido::instruction::remove_validator( &self.solido_program_id, - &lido::instruction::RemoveValidatorMeta { + &lido::instruction::RemoveValidatorMetaV2 { lido: self.solido_address, - validator_vote_account_to_remove: validator.pubkey, + validator_vote_account_to_remove: *validator.pubkey(), + validator_list: self.solido.validator_list, }, + u32::try_from(validator_index).expect("Too many validators"), ); return Some(MaintenanceInstruction::new(instruction, task)); } None } - /// Get the amount of rewards we can sell in Anker. - fn get_anker_rewards(&self) -> Option { - let anker_state = self.anker_state.as_ref()?; - let reserve_st_sol = anker_state.st_sol_reserve_balance; - let st_sol_amount = self - .solido - .exchange_rate - .exchange_sol(Lamports(anker_state.b_sol_total_supply_amount.0)) - .expect("It will not overflow because we always have less than the total amount of minted Sol."); - - (reserve_st_sol - st_sol_amount).ok() - } - - /// Try to sell the extra stSOL rewards for UST tokens or - /// to update the historical pool price exchange rate to protect us - /// against sandwiching attacks. - pub fn try_sell_anker_rewards(&self) -> Option { - let anker_state = self.anker_state.as_ref()?; - - let rewards = self.get_anker_rewards()?; - let min_rewards_to_sell = self - .solido - .exchange_rate - .exchange_sol(Self::MINIMUM_WITHDRAW_AMOUNT) - .expect("The price of a signature should be small enough that it doesn't overflow."); - // We should not call the instruction if the rewards are 0, or if the rewards are so small - // that the transaction cost is a significant portion of the rewards. - if rewards < min_rewards_to_sell { - return None; - } - - // Fees as in the `spl_token_swap` `SwapCurve::swap` calculation. - let trade_fee = anker_state.pool_fees.trading_fee(rewards.0 as u128)?; - let owner_fee = anker_state.pool_fees.owner_trading_fee(rewards.0 as u128)?; - - let total_fees = trade_fee.checked_add(owner_fee)?; - let rewards_minus_fees = (rewards.0 as u128).checked_sub(total_fees)?; - - let expected_proceeds = anker_state - .constant_product_calculator - .swap_without_fees( - rewards_minus_fees, - anker_state.pool_st_sol_balance.0 as u128, - anker_state.pool_ust_balance.0 as u128, - TradeDirection::AtoB, - )? - .destination_amount_swapped; - let expected_proceeds = MicroUst(expected_proceeds as u64); - - // We want at least 0.01 UST out if we are going to do the swap at all. - let min_proceeds = MicroUst(10_000); - if expected_proceeds < min_proceeds { - return None; - } - - // Check if we can sell the rewards with the preset slippage tolerance. - // Note that this might change when the instruction gets included in the block. - let minimum_ust_amount_for_rewards = anker_state - .anker - .historical_st_sol_prices - .minimum_ust_swap_amount(rewards, anker_state.anker.sell_rewards_min_out_bps) - .ok()?; - if expected_proceeds < minimum_ust_amount_for_rewards { - return None; - } - - let oldest_price_sample = anker_state.anker.historical_st_sol_prices.first(); - let slots_elapsed_since_oldest_sample = - self.clock.slot.saturating_sub(oldest_price_sample.slot); - - let youngest_sample = anker_state.anker.historical_st_sol_prices.last(); - let slots_elapsed_since_youngest_sample = - self.clock.slot.saturating_sub(youngest_sample.slot); - - // If the youngest sample is too recent, we are not yet allowed to sell - // rewards or update the price. - if slots_elapsed_since_youngest_sample < POOL_PRICE_MIN_SAMPLE_DISTANCE { - return None; - } - - // Time to update the historical price - if slots_elapsed_since_oldest_sample > POOL_PRICE_MAX_SAMPLE_AGE - || oldest_price_sample.slot == 0 - { - let expected_st_sol_price_in_ust = get_one_st_sol_for_ust_price_from_pool( - &anker_state.constant_product_calculator, - &anker_state.pool_st_sol_account, - &anker_state.pool_ust_account, - anker_state.pool_st_sol_balance, - anker_state.pool_ust_balance, - ) - .ok()?; - Some(MaintenanceInstruction::new( - anker_state.get_fetch_pool_price_instruction(self.solido_address), - MaintenanceOutput::FetchPoolPrice { - expected_st_sol_price_in_ust, - }, - )) - } else { - Some(MaintenanceInstruction::new( - anker_state - .get_sell_rewards_instruction(self.solido_address, self.solido.st_sol_mint), - MaintenanceOutput::SellRewards { - st_sol_amount: anker_state.st_sol_reserve_balance, - }, - )) - } - } - /// Get an instruction to merge accounts. fn get_merge_instruction( &self, - validator: &PubkeyAndEntry, + validator: &Validator, from_seed: u64, to_seed: u64, - ) -> Instruction { + ) -> Option { // Stake Account created by this transaction. let (from_stake, _bump_seed_end) = validator.find_stake_account_address( &self.solido_program_id, @@ -913,23 +822,27 @@ impl SolidoState { to_seed, StakeType::Stake, ); - lido::instruction::merge_stake( + + let validator_index = self.validators.position(validator.pubkey())?; + + Some(lido::instruction::merge_stake( &self.solido_program_id, - &lido::instruction::MergeStakeMeta { + &lido::instruction::MergeStakeMetaV2 { lido: self.solido_address, - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), from_stake, to_stake, stake_authority: self.get_stake_authority(), + validator_list: self.solido.validator_list, }, - ) + validator_index, + )) } // Tries to merge accounts from the beginning of the validator's // stake accounts. May return None or one instruction. pub fn try_merge_on_all_stakes(&self) -> Option { for (validator, stake_accounts) in self - .solido .validators .entries .iter() @@ -941,9 +854,9 @@ impl SolidoState { let to_stake = stake_accounts[1]; if to_stake.1.can_merge(&from_stake.1) { let instruction = - self.get_merge_instruction(validator, from_stake.1.seed, to_stake.1.seed); + self.get_merge_instruction(validator, from_stake.1.seed, to_stake.1.seed)?; let task = MaintenanceOutput::MergeStake { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), from_stake: from_stake.0, to_stake: to_stake.0, from_stake_seed: from_stake.1.seed, @@ -965,10 +878,11 @@ impl SolidoState { let instruction = lido::instruction::update_exchange_rate( &self.solido_program_id, - &lido::instruction::UpdateExchangeRateAccountsMeta { + &lido::instruction::UpdateExchangeRateAccountsMetaV2 { lido: self.solido_address, reserve: self.reserve_address, st_sol_mint: self.solido.st_sol_mint, + validator_list: self.solido.validator_list, }, ); let task = MaintenanceOutput::UpdateExchangeRate; @@ -982,23 +896,34 @@ impl SolidoState { /// or if some joker donates to one of the stake accounts we can use the same function /// to claim these rewards back to the reserve account so they can be re-staked. pub fn try_update_stake_account_balance(&self) -> Option { - for (validator, stake_accounts, unstake_accounts) in izip!( - self.solido.validators.entries.iter(), + for (validator_index, validator, stake_accounts, unstake_accounts) in izip!( + 0..self.validators.len(), + self.validators.entries.iter(), self.validator_stake_accounts.iter(), self.validator_unstake_accounts.iter() ) { - let current_stake_balance = stake_accounts - .iter() - .map(|(_addr, detail)| detail.balance.total()) - .sum::>() - .expect("If this overflows, there would be more than u64::MAX staked."); + // Check if total stake changed or some part is inactive and update balance. + // Part of total stake can become inactive after merging two stake accounts + // (without changing a total value) or can be increased after a donation. + // Active stake increases after receiving rewards. + let stake_rent = Lamports(self.rent.minimum_balance(std::mem::size_of::())); + let mut total_stake_balance = Lamports(0); + let mut can_be_withdrawn = Lamports(0); + for (_, detail) in stake_accounts { + total_stake_balance = (total_stake_balance + detail.balance.total()) + .expect("If this overflows, there would be more than u64::MAX staked."); + let diff = (detail.balance.inactive - stake_rent) + .expect("Inactive stake is always greater than rent exempt amount"); + can_be_withdrawn = (can_be_withdrawn + diff) + .expect("If this overflows, there would be more than u64::MAX staked."); + } let expected_difference_stake = - if current_stake_balance > validator.entry.effective_stake_balance() { - (current_stake_balance - validator.entry.effective_stake_balance()) + if total_stake_balance > validator.compute_effective_stake_balance() { + (total_stake_balance - validator.compute_effective_stake_balance()) .expect("Does not overflow because current > entry.balance.") } else { - Lamports(0) + can_be_withdrawn }; let mut removed_unstake = Lamports(0); @@ -1026,7 +951,7 @@ impl SolidoState { &self.solido_program_id, &lido::instruction::UpdateStakeAccountBalanceMeta { lido: self.solido_address, - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), stake_accounts: stake_account_addrs, reserve: self.reserve_address, stake_authority: self.get_stake_authority(), @@ -1034,10 +959,12 @@ impl SolidoState { st_sol_mint: self.solido.st_sol_mint, treasury_st_sol_account: self.solido.fee_recipients.treasury_account, developer_st_sol_account: self.solido.fee_recipients.developer_account, + validator_list: self.solido.validator_list, }, + u32::try_from(validator_index).expect("Too many validators"), ); - let task = MaintenanceOutput::WithdrawInactiveStake { - validator_vote_account: validator.pubkey, + let task = MaintenanceOutput::UpdateStakeAccountBalance { + validator_vote_account: *validator.pubkey(), expected_difference_stake, unstake_withdrawn_to_reserve: removed_unstake, }; @@ -1052,23 +979,21 @@ impl SolidoState { pub fn try_unstake_from_active_validators(&self) -> Option { self.confirm_should_stake_unstake_in_current_slot()?; // Return None if there's no active validator to unstake from. - self.solido.validators.iter_active().next()?; + self.validators.iter_active().next()?; // Get the target for each validator. Undelegated Lamports can be // sent when staking with validators. - let targets = lido::balance::get_target_balance( - self.get_effective_reserve(), - &self.solido.validators, - ) - .expect("Failed to compute target balance."); + let targets = + lido::balance::get_target_balance(self.get_effective_reserve(), &self.validators) + .expect("Failed to compute target balance."); let (validator_index, unstake_amount) = lido::balance::get_unstake_validator_index( - &self.solido.validators, + &self.validators, &targets, SolidoState::UNBALANCE_THRESHOLD, )?; - let validator = &self.solido.validators.entries[validator_index]; - let stake_account = &self.validator_stake_accounts[validator_index][0]; + let validator = &self.validators.entries[validator_index]; + let stake_account = &self.validator_stake_accounts[validator_index].get(0)?; let maximum_unstake = (stake_account.1.balance.total() - MINIMUM_STAKE_ACCOUNT_BALANCE) .expect("Stake account should always have the minimum amount."); @@ -1084,13 +1009,13 @@ impl SolidoState { } let (unstake_account, instruction) = - self.get_unstake_instruction(validator, stake_account, amount); + self.get_unstake_instruction(validator, stake_account, amount)?; let task = MaintenanceOutput::UnstakeFromActiveValidator(Unstake { - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), from_stake_account: stake_account.0, to_unstake_account: unstake_account, - from_stake_seed: validator.entry.stake_seeds.begin, - to_unstake_seed: validator.entry.unstake_seeds.end, + from_stake_seed: validator.stake_seeds.begin, + to_unstake_seed: validator.unstake_seeds.end, amount, }); Some(MaintenanceInstruction::new(instruction, task)) @@ -1099,8 +1024,7 @@ impl SolidoState { /// Write metrics about the current Solido instance in Prometheus format. pub fn write_prometheus(&self, out: &mut W) -> io::Result<()> { use solido_cli_common::prometheus::{ - write_anker_metrics_as_prometheus, write_metric, write_solido_metrics_as_prometheus, - Metric, MetricFamily, + write_metric, write_solido_metrics_as_prometheus, Metric, MetricFamily, }; write_metric( @@ -1179,7 +1103,6 @@ impl SolidoState { help: "Balance of the maintainer accounts, in SOL.", type_: "gauge", metrics: self - .solido .maintainers .entries .iter() @@ -1210,7 +1133,6 @@ impl SolidoState { ), info, ) in self - .solido .validators .entries .iter() @@ -1243,7 +1165,7 @@ impl SolidoState { let annotator = MetricAnnotator { produced_at: self.produced_at, - vote_account: validator.pubkey.to_string(), + vote_account: validator.pubkey().to_string(), name: sanitize_validator_name(&info.name), keybase_username: info .keybase_username @@ -1268,14 +1190,17 @@ impl SolidoState { balance_sol_metrics.push(metric(stake_balance.active, "active")); balance_sol_metrics.push(metric(stake_balance.deactivating, "deactivating")); - last_voted_slot_metrics - .push(annotator.add_labels(Metric::new(vote_account.last_timestamp.slot))); - last_voted_timestamp_metrics.push( - annotator.add_labels(Metric::new(vote_account.last_timestamp.timestamp as u64)), - ); + if let Some(vote_account) = vote_account { + last_voted_slot_metrics + .push(annotator.add_labels(Metric::new(vote_account.last_timestamp.slot))); + last_voted_timestamp_metrics.push( + annotator.add_labels(Metric::new(vote_account.last_timestamp.timestamp as u64)), + ); + vote_credits_metrics + .push(annotator.add_labels(Metric::new(vote_account.credits()))); + } identity_account_balance_metrics .push(annotator.add_labels(Metric::new_sol(*identity_account_balance))); - vote_credits_metrics.push(annotator.add_labels(Metric::new(vote_account.credits()))); } write_metric( @@ -1380,44 +1305,6 @@ impl SolidoState { )?; write_solido_metrics_as_prometheus(&self.solido.metrics, self.produced_at, out)?; - if let Some(anker_state) = &self.anker_state { - write_metric( - out, - &MetricFamily { - name: "anker_token_supply_b_sol", - help: "Amount of bSOL that exists currently.", - type_: "gauge", - metrics: vec![Metric::new_b_sol(anker_state.b_sol_total_supply_amount) - .at(self.produced_at)], - }, - )?; - - write_metric( - out, - &MetricFamily { - name: "anker_reserve_st_sol", - help: "Amount of stSOL in reserve.", - type_: "gauge", - metrics: vec![ - Metric::new_st_sol(anker_state.st_sol_reserve_balance).at(self.produced_at) - ], - }, - )?; - - write_metric( - out, - &MetricFamily { - name: "anker_reserve_ust", - help: "Amount of UST in reserve.", - type_: "gauge", - metrics: vec![ - Metric::new_ust(anker_state.ust_reserve_balance).at(self.produced_at) - ], - }, - )?; - - write_anker_metrics_as_prometheus(&anker_state.anker.metrics, self.produced_at, out)?; - } Ok(()) } @@ -1482,7 +1369,7 @@ impl SolidoState { /// one maintainer is offline, this means maintenance operations get delayed /// by at most ~55s. pub fn get_current_maintainer_duty(&self) -> Option { - if self.solido.maintainers.entries.is_empty() { + if self.maintainers.entries.is_empty() { return None; } @@ -1497,8 +1384,8 @@ impl SolidoState { return None; } - let maintainer_index = duty_slice % self.solido.maintainers.len() as u64; - Some(self.solido.maintainers.entries[maintainer_index as usize].pubkey) + let maintainer_index = duty_slice % self.maintainers.len() as u64; + Some(self.maintainers.entries[maintainer_index as usize].pubkey) } /// Return the slot at which the given maintainer's next duty slice starts. @@ -1508,19 +1395,17 @@ impl SolidoState { /// /// See also [`get_current_maintainer_duty`]. pub fn get_next_maintainer_duty_slot(&self, maintainer: &Pubkey) -> Option { - if self.solido.maintainers.entries.is_empty() { + if self.maintainers.entries.is_empty() { return None; } // Compute the start of the current "cycle", where in every cycle, every // maintainer has a single duty slice. - let cycle_length = - self.solido.maintainers.entries.len() as u64 * Self::MAINTAINER_DUTY_SLICE_LENGTH; + let cycle_length = self.maintainers.len() as u64 * Self::MAINTAINER_DUTY_SLICE_LENGTH; let current_cycle_start_slot = (self.clock.slot / cycle_length) * cycle_length; // Compute the start of our slice within the current cycle. let self_index = self - .solido .maintainers .entries .iter() @@ -1538,7 +1423,7 @@ impl SolidoState { } /// Return None if we observe we moved past `1 - - /// SolidoState::END_OF_EPOCH_THRESHOLD`%. Return Some(()) if the above + /// config.end_of_epoch_threshold`%. Return Some(()) if the above /// condition fails or `self.stake_unstake_any_time` is set to /// `true`. pub fn confirm_should_stake_unstake_in_current_slot(&self) -> Option<()> { @@ -1562,7 +1447,11 @@ impl SolidoState { numerator: slot_past_epoch, denominator: slots_per_epoch, }; - if ratio > SolidoState::END_OF_EPOCH_THRESHOLD { + let theshold = Rational { + numerator: self.end_of_epoch_threshold as u64, + denominator: 100, + }; + if ratio > theshold { Some(()) } else { None @@ -1582,7 +1471,6 @@ pub fn try_perform_maintenance( // transaction fees. let minimum_maintainer_balance = Lamports(100_000_000); match state - .solido .maintainers .entries .iter() @@ -1618,8 +1506,7 @@ pub fn try_perform_maintenance( .or_else(|| state.try_deactivate_validator_if_commission_exceeds_max()) .or_else(|| state.try_stake_deposit()) .or_else(|| state.try_unstake_from_active_validators()) - .or_else(|| state.try_remove_validator()) - .or_else(|| state.try_sell_anker_rewards()); + .or_else(|| state.try_remove_validator()); match instruction_output { Some(maintenance_instruction) => { @@ -1649,9 +1536,9 @@ pub fn run_perform_maintenance( let state = SolidoState::new( config, opts.solido_program_id(), - opts.anker_program_id(), opts.solido_address(), *opts.stake_time(), + *opts.end_of_epoch_threshold(), )?; try_perform_maintenance(config, &state) } @@ -1668,7 +1555,6 @@ mod test { solido_program_id: Pubkey::new_unique(), solido_address: Pubkey::new_unique(), solido: Lido::default(), - anker_state: Some(AnkerState::default()), validator_stake_accounts: vec![], validator_unstake_accounts: vec![], validator_vote_account_balances: vec![], @@ -1685,10 +1571,17 @@ mod test { stake_history: StakeHistory::default(), maintainer_address: Pubkey::new_unique(), stake_time: StakeTime::Anytime, + validators: AccountList::::new_default(0), + maintainers: AccountList::::new_default(0), + end_of_epoch_threshold: 95, }; // The reserve should be rent-exempt. state.reserve_account.lamports = state.rent.minimum_balance(0); + state + .maintainers + .entries + .push(Maintainer::new(state.maintainer_address)); state } @@ -1704,12 +1597,11 @@ mod test { let mut state = new_empty_solido(); // Add a validators, without any stake accounts yet. - state.solido.validators.maximum_entries = 1; + state.validators.header.max_entries = 1; state - .solido .validators - .add(Pubkey::new_unique(), Validator::new()) - .unwrap(); + .entries + .push(Validator::new(Pubkey::new_unique())); state.validator_stake_accounts.push(vec![]); // Put some SOL in the reserve, but not enough to stake. state.reserve_account.lamports += MINIMUM_STAKE_ACCOUNT_BALANCE.0 - 1; @@ -1734,17 +1626,16 @@ mod test { let mut state = new_empty_solido(); // Add two validators, both without any stake account yet. - state.solido.validators.maximum_entries = 2; + state.validators.header.max_entries = 2; state - .solido .validators - .add(Pubkey::new_unique(), Validator::new()) - .unwrap(); + .entries + .push(Validator::new(Pubkey::new_unique())); + state - .solido .validators - .add(Pubkey::new_unique(), Validator::new()) - .unwrap(); + .entries + .push(Validator::new(Pubkey::new_unique())); state.validator_stake_accounts = vec![vec![], vec![]]; // Put enough SOL in the reserve that we can stake half of the deposit @@ -1752,7 +1643,7 @@ mod test { // balance. state.reserve_account.lamports += 4 * MINIMUM_STAKE_ACCOUNT_BALANCE.0; - let stake_account_0 = state.solido.validators.entries[0].find_stake_account_address( + let stake_account_0 = state.validators.entries[0].find_stake_account_address( &state.solido_program_id, &state.solido_address, 0, @@ -1763,13 +1654,13 @@ mod test { assert_eq!( state.try_stake_deposit().unwrap().output, MaintenanceOutput::StakeDeposit { - validator_vote_account: state.solido.validators.entries[0].pubkey, + validator_vote_account: *state.validators.entries[0].pubkey(), amount: (MINIMUM_STAKE_ACCOUNT_BALANCE * 2).unwrap(), stake_account: stake_account_0.0, } ); - let stake_account_1 = state.solido.validators.entries[1].find_stake_account_address( + let stake_account_1 = state.validators.entries[1].find_stake_account_address( &state.solido_program_id, &state.solido_address, 0, @@ -1778,7 +1669,7 @@ mod test { // Pretend that the amount was actually staked. state.reserve_account.lamports -= 2 * MINIMUM_STAKE_ACCOUNT_BALANCE.0; - let validator = &mut state.solido.validators.entries[0].entry; + let validator = &mut state.validators.entries[0]; validator.stake_accounts_balance = validator .stake_accounts_balance .add((MINIMUM_STAKE_ACCOUNT_BALANCE * 2).unwrap()) @@ -1789,7 +1680,7 @@ mod test { assert_eq!( state.try_stake_deposit().unwrap().output, MaintenanceOutput::StakeDeposit { - validator_vote_account: state.solido.validators.entries[1].pubkey, + validator_vote_account: *state.validators.entries[1].pubkey(), amount: (MINIMUM_STAKE_ACCOUNT_BALANCE * 2).unwrap(), stake_account: stake_account_1.0, } @@ -1800,21 +1691,20 @@ mod test { fn next_maintainer_duty_slot_agrees_with_current_duty() { for num_maintainers in 1..10 { let mut state = new_empty_solido(); - state.solido.maintainers.maximum_entries = num_maintainers; + state.maintainers.header.max_entries = num_maintainers; for _ in 0..num_maintainers { state - .solido .maintainers - .add(Pubkey::new_unique(), ()) - .unwrap(); + .entries + .push(Maintainer::new(Pubkey::new_unique())); } let maintainer_keys: Vec = state - .solido .maintainers .entries .iter() - .map(|p| p.pubkey) + .map(|p| p.pubkey()) + .cloned() .collect(); // Check the next slot in forward order but also reverse order. With @@ -1865,8 +1755,8 @@ mod test { fn next_maintainer_duty_returns_slot_greater_than_current_slot() { let mut state = new_empty_solido(); let maintainer = Pubkey::new_unique(); - state.solido.maintainers.maximum_entries = 1; - state.solido.maintainers.add(maintainer, ()).unwrap(); + state.maintainers.header.max_entries = 1; + state.maintainers.entries.push(Maintainer::new(maintainer)); for _ in 0..10 { let next_slot = state.get_next_maintainer_duty_slot(&maintainer).unwrap(); diff --git a/cli/maintainer/src/maintenance_output.rs b/cli/maintainer/src/maintenance_output.rs deleted file mode 100644 index e896608fd..000000000 --- a/cli/maintainer/src/maintenance_output.rs +++ /dev/null @@ -1,203 +0,0 @@ -use serde::Serialize; - -/// A brief description of the maintenance performed. Not relevant functionally, -/// but helpful for automated testing, and just for info. -#[derive(Debug, Eq, PartialEq, Serialize)] -pub enum MaintenanceOutput { - StakeDeposit { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - - #[serde(serialize_with = "serialize_b58")] - stake_account: Pubkey, - - #[serde(rename = "amount_lamports")] - amount: Lamports, - }, - - UpdateExchangeRate, - - WithdrawInactiveStake { - /// The vote account of the validator that we want to update. - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - - /// The expected difference that the update will observe. - /// - /// This is only an expected value, because a different transaction might - /// execute between us observing the state and concluding that there is - /// a difference, and our `WithdrawInactiveStake` instruction executing. - #[serde(rename = "expected_difference_stake_lamports")] - expected_difference_stake: Lamports, - - #[serde(rename = "unstake_withdrawn_to_reserve_lamports")] - unstake_withdrawn_to_reserve: Lamports, - }, - - CollectValidatorFee { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - #[serde(rename = "fee_rewards_lamports")] - fee_rewards: Lamports, - }, - - ClaimValidatorFee { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - #[serde(rename = "fee_rewards_st_lamports")] - fee_rewards: StLamports, - }, - - MergeStake { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - #[serde(serialize_with = "serialize_b58")] - from_stake: Pubkey, - #[serde(serialize_with = "serialize_b58")] - to_stake: Pubkey, - from_stake_seed: u64, - to_stake_seed: u64, - }, - - UnstakeFromInactiveValidator(Unstake), - RemoveValidator { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - }, - UnstakeFromActiveValidator(Unstake), - - SellRewards { - st_sol_amount: StLamports, - }, -} - -#[derive(Debug, Eq, PartialEq, Serialize)] -pub struct Unstake { - #[serde(serialize_with = "serialize_b58")] - validator_vote_account: Pubkey, - #[serde(serialize_with = "serialize_b58")] - from_stake_account: Pubkey, - #[serde(serialize_with = "serialize_b58")] - to_unstake_account: Pubkey, - from_stake_seed: u64, - to_unstake_seed: u64, - amount: Lamports, -} - -impl fmt::Display for Unstake { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!( - f, - " Validator vote account: {}", - self.validator_vote_account - )?; - writeln!( - f, - " Stake account: {}, seed: {}", - self.from_stake_account, self.from_stake_seed - )?; - writeln!( - f, - " Unstake account: {}, seed: {}", - self.to_unstake_account, self.to_unstake_seed - )?; - writeln!(f, " Amount: {}", self.amount)?; - Ok(()) - } -} - -impl fmt::Display for MaintenanceOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - MaintenanceOutput::StakeDeposit { - validator_vote_account, - stake_account, - amount, - } => { - writeln!(f, "Staked deposit.")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - writeln!(f, " Stake account: {}", stake_account)?; - writeln!(f, " Amount staked: {}", amount)?; - } - MaintenanceOutput::UpdateExchangeRate => { - writeln!(f, "Updated exchange rate.")?; - } - MaintenanceOutput::WithdrawInactiveStake { - validator_vote_account, - expected_difference_stake, - unstake_withdrawn_to_reserve, - } => { - writeln!(f, "Withdrew inactive stake.")?; - writeln!( - f, - " Validator vote account: {}", - validator_vote_account - )?; - writeln!( - f, - " Expected difference in stake: {}", - expected_difference_stake - )?; - writeln!( - f, - " Amount withdrawn from unstake: {}", - unstake_withdrawn_to_reserve - )?; - } - MaintenanceOutput::CollectValidatorFee { - validator_vote_account, - fee_rewards, - } => { - writeln!(f, "Collected validator fees.")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - writeln!(f, " Collected fee rewards: {}", fee_rewards)?; - } - - MaintenanceOutput::ClaimValidatorFee { - validator_vote_account, - fee_rewards, - } => { - writeln!(f, "Claimed validator fees.")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - writeln!(f, " Claimed fee: {}", fee_rewards)?; - } - MaintenanceOutput::MergeStake { - validator_vote_account, - from_stake, - to_stake, - from_stake_seed, - to_stake_seed, - } => { - writeln!(f, "Stake accounts merged")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - writeln!( - f, - " From stake: {}, seed: {}", - from_stake, from_stake_seed - )?; - writeln!( - f, - " To stake: {}, seed: {}", - to_stake, to_stake_seed - )?; - } - MaintenanceOutput::UnstakeFromInactiveValidator(unstake) => { - writeln!(f, "Unstake from inactive validator\n{}", unstake)?; - } - MaintenanceOutput::UnstakeFromActiveValidator(unstake) => { - writeln!(f, "Unstake from active validator\n{}", unstake)?; - } - MaintenanceOutput::RemoveValidator { - validator_vote_account, - } => { - writeln!(f, "Remove validator")?; - writeln!(f, " Validator vote account: {}", validator_vote_account)?; - } - MaintenanceOutput::SellRewards { st_sol_amount } => { - writeln!(f, "Sell stSOL rewards")?; - writeln!(f, " Amount: {}", st_sol_amount)?; - } - } - Ok(()) - } -} diff --git a/cli/maintainer/src/serialization_utils.rs b/cli/maintainer/src/serialization_utils.rs deleted file mode 100644 index 7395b0fc4..000000000 --- a/cli/maintainer/src/serialization_utils.rs +++ /dev/null @@ -1,7 +0,0 @@ -use anker::wormhole::TerraAddress; -use serde::Serializer; - -/// Serde serializer that serializes a Terra address as bech32 string, for use in json. -pub fn serialize_bech32(x: &TerraAddress, serializer: S) -> Result { - serializer.serialize_str(&x.to_string()) -} diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index ebdcf9d3f..39a2472c2 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,7 +1,7 @@ -FROM chorusone/solido-base +FROM guyos/solido-base -ENV SOLVERSION=1.8.16 -ENV SOLINSTALLCHECKSUM=08c092af36706e0556a0516e270171d9ea3683965246ee81d979e0c157a3864e +ENV SOLVERSION=1.9.28 +ENV SOLINSTALLCHECKSUM=eaaefe4ea811bccf668aac77a789d506a3ab72b1d33a1428729b7fc14ea0e0b5 ENV SOLPATH="/root/.local/share/solana/install/active_release/bin" ENV SOLIDOBUILDPATH="$SOLPATH/solido-build" ENV SOLIDORELEASEPATH="$SOLPATH/solido" @@ -40,7 +40,6 @@ RUN cd $SOLIDOBUILDPATH \ # Hash on-chain programs RUN cd $SOLIDORELEASEPATH/deploy \ && sha256sum lido.so >> lido.hash \ - && sha256sum anker.so >> anker.hash \ && sha256sum serum_multisig.so >> serum_multisig.hash @@ -50,4 +49,3 @@ RUN cd $SOLIDORELEASEPATH/cli \ && sha256sum listener >> listener.hash WORKDIR $SOLPATH - diff --git a/docker/Dockerfile.maintainer b/docker/Dockerfile.maintainer index df5ecaad3..fbf32e6b0 100644 --- a/docker/Dockerfile.maintainer +++ b/docker/Dockerfile.maintainer @@ -1,4 +1,4 @@ -FROM chorusone/solido-base +FROM guyos/solido-base ENV SOLIDOBUILDPATH="/solido-build" ENV SOLIDOPATH="/solido" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 228502d8e..4cdd11f97 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,12 +1,12 @@ version: '3.9' services: solido-base: - image: chorusone/solido-base + image: guyos/solido-base build: context: ../ dockerfile: docker/Dockerfile.base solido: - image: chorusone/solido-maintainer:${SOLIDO_VERSION} + image: guyos/solido-maintainer:${SOLIDO_VERSION} build: context: ../ dockerfile: docker/Dockerfile.maintainer diff --git a/multisig b/multisig deleted file mode 160000 index 20228f0eb..000000000 --- a/multisig +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 20228f0eb762e5426b25c4b26023dcb7486dbd9b diff --git a/program/Cargo.toml b/program/Cargo.toml index 73abc4b24..b2e6d7be6 100644 --- a/program/Cargo.toml +++ b/program/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Chorus One "] license = "GPL-3.0" edition = "2018" name = "lido" -version = "1.3.3" +version = "1.3.6" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,21 +12,22 @@ no-entrypoint = [] test-bpf = [] [dependencies] -borsh = "0.9.3" -num-derive = "0.3" -num-traits = "0.2" -serde = "1.0.137" -serde_derive = "1.0.137" -solana-program = "1.9.28" -spl-token = { version = "3.1.1", features = ["no-entrypoint"] } +borsh = ">=0.9" +num-derive = "^0.3" +num-traits = "^0.2" +serde = "^1.0" +serde_derive = "^1.0" +solana-program = ">=1" +spl-token = { version = ">=3.0", features = ["no-entrypoint"] } +arrayref = "^0.3" -[dev-dependencies] -bincode = "1.3.3" -serde_json = "1.0" -solana-program-test = "1.9.28" -solana-sdk = "1.9.28" -solana-vote-program = "1.9.28" -testlib = { path = "../testlib" } +# [dev-dependencies] +# bincode = "1.3.3" +# serde_json = "1.0" +# solana-program-test = "1.16" +# solana-sdk = "1.16" +# solana-vote-program = "1.16" +# testlib = { path = "../testlib" } [lib] crate-type = ["cdylib", "lib"] diff --git a/program/src/account_map.rs b/program/src/account_map.rs deleted file mode 100644 index 7a63d609f..000000000 --- a/program/src/account_map.rs +++ /dev/null @@ -1,221 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -//! A type that stores a map (dictionary) from public key to some value `T`. - -use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use serde::Serialize; -use solana_program::pubkey::Pubkey; - -use crate::error::LidoError; -use crate::util::serialize_b58; - -/// An entry in `AccountMap`. -#[derive( - Clone, Default, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema, Serialize, -)] -pub struct PubkeyAndEntry { - #[serde(serialize_with = "serialize_b58")] - pub pubkey: Pubkey, - pub entry: T, -} - -/// A map from public key to `T`, implemented as a vector of key-value pairs. -#[derive( - Clone, Default, Debug, Eq, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema, Serialize, -)] -pub struct AccountMap { - pub entries: Vec>, - pub maximum_entries: u32, -} -pub trait EntryConstantSize { - const SIZE: usize; -} - -pub type AccountSet = AccountMap<()>; - -impl AccountMap { - /// Creates a new instance with the `maximum_entries` positions filled with the default value - pub fn new_fill_default(maximum_entries: u32) -> Self { - let mut v = Vec::with_capacity(maximum_entries as usize); - for _ in 0..maximum_entries { - v.push(PubkeyAndEntry { - pubkey: Pubkey::default(), - entry: T::default(), - }); - } - AccountMap { - entries: v, - maximum_entries, - } - } - - /// Creates a new empty instance - pub fn new(maximum_entries: u32) -> Self { - AccountMap { - entries: Vec::new(), - maximum_entries, - } - } - - pub fn len(&self) -> usize { - self.entries.len() - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn add(&mut self, address: Pubkey, value: T) -> Result<(), LidoError> { - if self.len() == self.maximum_entries as usize { - return Err(LidoError::MaximumNumberOfAccountsExceeded); - } - if !self.entries.iter().any(|pe| pe.pubkey == address) { - self.entries.push(PubkeyAndEntry { - pubkey: address, - entry: value, - }); - } else { - return Err(LidoError::DuplicatedEntry); - } - Ok(()) - } - - pub fn remove(&mut self, address: &Pubkey) -> Result { - let idx = self - .entries - .iter() - .position(|pe| &pe.pubkey == address) - .ok_or(LidoError::InvalidAccountMember)?; - Ok(self.entries.swap_remove(idx).entry) - } - - pub fn get(&self, address: &Pubkey) -> Result<&PubkeyAndEntry, LidoError> { - self.entries - .iter() - .find(|pe| &pe.pubkey == address) - .ok_or(LidoError::InvalidAccountMember) - } - - pub fn get_mut(&mut self, address: &Pubkey) -> Result<&mut PubkeyAndEntry, LidoError> { - self.entries - .iter_mut() - .find(|pe| &pe.pubkey == address) - .ok_or(LidoError::InvalidAccountMember) - } - - /// Return how many bytes are needed to serialize an instance holding `max_entries`. - pub fn required_bytes(max_entries: usize) -> usize { - let key_size = std::mem::size_of::(); - let value_size = T::SIZE; - let entry_size = key_size + value_size; - - // 8 bytes for the length and u32 field, then the entries themselves. - 8 + entry_size * max_entries as usize - } - - /// Return how many entries could fit in a buffer of the given size. - pub fn maximum_entries(buffer_size: usize) -> usize { - let key_size = std::mem::size_of::(); - let value_size = T::SIZE; - let entry_size = key_size + value_size; - - buffer_size.saturating_sub(8) / entry_size - } - - /// Iterate just the values, not the keys. - pub fn iter_entries(&self) -> IterEntries { - IterEntries { - iter: self.entries.iter(), - } - } - - /// Iterate just the values mutably, not the keys. - pub fn iter_entries_mut(&mut self) -> IterEntriesMut { - IterEntriesMut { - iter: self.entries.iter_mut(), - } - } -} - -pub struct IterEntries<'a, T: 'a> { - iter: std::slice::Iter<'a, PubkeyAndEntry>, -} - -impl<'a, T: 'a> std::iter::Iterator for IterEntries<'a, T> { - type Item = &'a T; - - fn next(&mut self) -> Option<&'a T> { - self.iter.next().map(|pubkey_entry| &pubkey_entry.entry) - } -} - -pub struct IterEntriesMut<'a, T: 'a> { - iter: std::slice::IterMut<'a, PubkeyAndEntry>, -} - -impl<'a, T: 'a> std::iter::Iterator for IterEntriesMut<'a, T> { - type Item = &'a mut T; - - fn next(&mut self) -> Option<&'a mut T> { - self.iter.next().map(|pubkey_entry| &mut pubkey_entry.entry) - } -} - -#[cfg(test)] -mod test { - use super::*; - - impl EntryConstantSize for u32 { - const SIZE: usize = 4; - } - - #[test] - fn test_account_map_limit() { - let mut map = AccountMap::new(1); - let result_0 = map.add(Pubkey::new_unique(), 0_u32); - let result_1 = map.add(Pubkey::new_unique(), 1_u32); - assert_eq!(result_0, Ok(())); - assert_eq!(result_1, Err(LidoError::MaximumNumberOfAccountsExceeded)); - } - - #[test] - fn test_account_map_duplicate() { - let mut map = AccountMap::new(2); - let key = Pubkey::new_unique(); - let result_0 = map.add(key, 0_u32); - let result_1 = map.add(key, 1_u32); - assert_eq!(result_0, Ok(())); - assert_eq!(result_1, Err(LidoError::DuplicatedEntry)); - } - - #[test] - fn test_account_map_add_remove() { - let mut map = AccountMap::new(1); - let key = Pubkey::new_unique(); - map.add(key, 0_u32).unwrap(); - - assert_eq!(map.get(&key).map(|pe| pe.entry), Ok(0)); - assert_eq!(map.get_mut(&key).map(|pe| pe.entry), Ok(0)); - assert_eq!(map.remove(&key), Ok(0)); - - assert_eq!(map.get(&key), Err(LidoError::InvalidAccountMember)); - assert_eq!(map.get_mut(&key), Err(LidoError::InvalidAccountMember)); - assert_eq!(map.remove(&key), Err(LidoError::InvalidAccountMember)); - } - - #[test] - fn test_account_map_iter_entries() { - let mut map: AccountMap = AccountMap::new(2); - map.add(Pubkey::new_unique(), 0).unwrap(); - map.add(Pubkey::new_unique(), 1).unwrap(); - - for entry in map.iter_entries_mut() { - *entry = 2; - } - - for entry in map.iter_entries() { - assert_eq!(*entry, 2); - } - } -} diff --git a/program/src/balance.rs b/program/src/balance.rs index 34f017794..da566d82b 100644 --- a/program/src/balance.rs +++ b/program/src/balance.rs @@ -5,8 +5,7 @@ use std::ops::Mul; -use crate::account_map::PubkeyAndEntry; -use crate::state::{Validator, Validators}; +use crate::state::{Validator, ValidatorList}; use crate::{ error::LidoError, token, @@ -20,10 +19,11 @@ use crate::{ /// This function targets a uniform distribution over all active validators. pub fn get_target_balance( undelegated_lamports: Lamports, - validators: &Validators, + validators: &ValidatorList, ) -> Result, LidoError> { let total_delegated_lamports: token::Result = validators - .iter_entries() + .entries + .iter() .map(|v| v.stake_accounts_balance) .sum(); @@ -47,7 +47,8 @@ pub fn get_target_balance( // Target an uniform distribution. let mut target_balance: Vec = validators - .iter_entries() + .entries + .iter() .map(|validator| { if validator.active { lamports_per_validator @@ -77,7 +78,7 @@ pub fn get_target_balance( // fee per signature is 10k Lamports at the time of writing. Also, there is // a minimum amount we can stake, so in practice, validators will never be // as close to their target that the one Lamport matters anyway. - for (target, validator) in target_balance.iter_mut().zip(validators.iter_entries()) { + for (target, validator) in target_balance.iter_mut().zip(validators.entries.iter()) { if remainder == Lamports(0) { break; } @@ -108,7 +109,7 @@ pub fn get_target_balance( /// will try to unstake, and return the index of the validator where unstaking /// will have the largest impact. pub fn get_unstake_validator_index( - validators: &Validators, + validators: &ValidatorList, target_balance: &[Lamports], threshold: Rational, ) -> Option<(usize, Lamports)> { @@ -122,7 +123,7 @@ pub fn get_unstake_validator_index( .any(|(validator, target)| { let target_difference = target .0 - .saturating_sub(validator.entry.effective_stake_balance().0); + .saturating_sub(validator.compute_effective_stake_balance().0); if target == &Lamports(0) { return false; } @@ -139,15 +140,13 @@ pub fn get_unstake_validator_index( .zip(target_balance) .max_by_key(|((_idx, validator), target)| { validator - .entry - .effective_stake_balance() + .compute_effective_stake_balance() .0 .saturating_sub(target.0) })?; let amount = validator - .entry - .effective_stake_balance() + .compute_effective_stake_balance() .0 .saturating_sub(target.0); let ratio = Rational { @@ -166,7 +165,7 @@ pub fn get_unstake_validator_index( /// /// This assumes that there is at least one active validator. Panics otherwise. pub fn get_minimum_stake_validator_index_amount( - validators: &Validators, + validators: &ValidatorList, target_balance: &[Lamports], ) -> (usize, Lamports) { assert_eq!( @@ -177,26 +176,28 @@ pub fn get_minimum_stake_validator_index_amount( // Our initial index, that will be returned when no validator is below its target, // is the first active validator. - let mut index = validators - .iter_entries() - .position(|v| v.active) - .expect("get_minimum_stake_validator_index_amount requires at least one active validator."); - let mut lowest_balance = validators.entries[index].entry.effective_stake_balance(); + let mut index = + validators.entries.iter().position(|v| v.active).expect( + "get_minimum_stake_validator_index_amount requires at least one active validator.", + ); + let mut lowest_balance = validators.entries[index].compute_effective_stake_balance(); let mut amount = Lamports( - target_balance[index] - .0 - .saturating_sub(validators.entries[index].entry.effective_stake_balance().0), + target_balance[index].0.saturating_sub( + validators.entries[index] + .compute_effective_stake_balance() + .0, + ), ); - for (i, (validator, target)) in validators.iter_entries().zip(target_balance).enumerate() { - if validator.active && validator.effective_stake_balance() < lowest_balance { + for (i, (validator, target)) in validators.entries.iter().zip(target_balance).enumerate() { + if validator.active && validator.compute_effective_stake_balance() < lowest_balance { index = i; amount = Lamports( target .0 - .saturating_sub(validator.effective_stake_balance().0), + .saturating_sub(validator.compute_effective_stake_balance().0), ); - lowest_balance = validator.effective_stake_balance(); + lowest_balance = validator.compute_effective_stake_balance(); } } @@ -204,26 +205,26 @@ pub fn get_minimum_stake_validator_index_amount( } pub fn get_validator_to_withdraw( - validators: &Validators, -) -> Result<&PubkeyAndEntry, crate::error::LidoError> { + validators: &ValidatorList, +) -> Result<&Validator, crate::error::LidoError> { validators .entries .iter() - .max_by_key(|v| v.entry.effective_stake_balance()) + .max_by_key(|v| v.compute_effective_stake_balance()) .ok_or(LidoError::NoActiveValidators) } #[cfg(test)] mod test { use super::*; - use crate::state::Validators; + use crate::state::ValidatorList; use crate::token::Lamports; #[test] fn get_target_balance_works_for_single_validator() { // 100 Lamports delegated + 50 undelegated => 150 per validator target. - let mut validators = Validators::new_fill_default(1); - validators.entries[0].entry.stake_accounts_balance = Lamports(100); + let mut validators = ValidatorList::new_default(1); + validators.entries[0].stake_accounts_balance = Lamports(100); let undelegated_stake = Lamports(50); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); assert_eq!(targets[0], Lamports(150)); @@ -239,9 +240,9 @@ mod test { #[test] fn get_target_balance_works_for_integer_multiple() { // 200 Lamports delegated + 50 undelegated => 125 per validator target. - let mut validators = Validators::new_fill_default(2); - validators.entries[0].entry.stake_accounts_balance = Lamports(101); - validators.entries[1].entry.stake_accounts_balance = Lamports(99); + let mut validators = ValidatorList::new_default(2); + validators.entries[0].stake_accounts_balance = Lamports(101); + validators.entries[1].stake_accounts_balance = Lamports(99); let undelegated_stake = Lamports(50); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -258,9 +259,9 @@ mod test { fn get_target_balance_works_for_non_integer_multiple() { // 200 Lamports delegated + 51 undelegated => 125 per validator target, // and one validator gets 1 more. - let mut validators = Validators::new_fill_default(2); - validators.entries[0].entry.stake_accounts_balance = Lamports(101); - validators.entries[1].entry.stake_accounts_balance = Lamports(99); + let mut validators = ValidatorList::new_default(2); + validators.entries[0].stake_accounts_balance = Lamports(101); + validators.entries[1].stake_accounts_balance = Lamports(99); let undelegated_stake = Lamports(51); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -275,9 +276,9 @@ mod test { #[test] fn get_target_balance_already_balanced() { - let mut validators = Validators::new_fill_default(2); - validators.entries[0].entry.stake_accounts_balance = Lamports(50); - validators.entries[1].entry.stake_accounts_balance = Lamports(50); + let mut validators = ValidatorList::new_default(2); + validators.entries[0].stake_accounts_balance = Lamports(50); + validators.entries[1].stake_accounts_balance = Lamports(50); let undelegated_stake = Lamports(0); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -290,11 +291,11 @@ mod test { } #[test] fn get_target_balance_works_with_inactive_for_non_integer_multiple() { - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(101); - validators.entries[1].entry.stake_accounts_balance = Lamports(0); - validators.entries[1].entry.active = false; - validators.entries[2].entry.stake_accounts_balance = Lamports(99); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(101); + validators.entries[1].stake_accounts_balance = Lamports(0); + validators.entries[1].active = false; + validators.entries[2].stake_accounts_balance = Lamports(99); let undelegated_stake = Lamports(51); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -310,11 +311,11 @@ mod test { fn get_target_balance_works_with_inactive_for_integer_multiple() { // 500 Lamports delegated, but only two active validators out of three. // All target should be divided equally within the active validators. - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(100); - validators.entries[1].entry.stake_accounts_balance = Lamports(100); - validators.entries[1].entry.active = false; - validators.entries[2].entry.stake_accounts_balance = Lamports(300); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(100); + validators.entries[1].stake_accounts_balance = Lamports(100); + validators.entries[1].active = false; + validators.entries[2].stake_accounts_balance = Lamports(300); let undelegated_stake = Lamports(0); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -329,13 +330,13 @@ mod test { #[test] fn get_target_balance_all_inactive() { // No active validators exist. - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(1); - validators.entries[1].entry.stake_accounts_balance = Lamports(2); - validators.entries[2].entry.stake_accounts_balance = Lamports(3); - validators.entries[0].entry.active = false; - validators.entries[1].entry.active = false; - validators.entries[2].entry.active = false; + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(1); + validators.entries[1].stake_accounts_balance = Lamports(2); + validators.entries[2].stake_accounts_balance = Lamports(3); + validators.entries[0].active = false; + validators.entries[1].active = false; + validators.entries[2].active = false; let undelegated_stake = Lamports(0); let result = get_target_balance(undelegated_stake, &validators); @@ -347,10 +348,10 @@ mod test { // Every validator is exactly at its target, no validator is below. // But the validator furthest below target should still be an active one, // not the inactive one. - let mut validators = Validators::new_fill_default(2); - validators.entries[0].entry.stake_accounts_balance = Lamports(0); - validators.entries[1].entry.stake_accounts_balance = Lamports(10); - validators.entries[0].entry.active = false; + let mut validators = ValidatorList::new_default(2); + validators.entries[0].stake_accounts_balance = Lamports(0); + validators.entries[1].stake_accounts_balance = Lamports(10); + validators.entries[0].active = false; let undelegated_stake = Lamports(0); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -362,10 +363,10 @@ mod test { #[test] fn get_target_balance_works_for_minimum_staked_validator() { - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(101); - validators.entries[1].entry.stake_accounts_balance = Lamports(101); - validators.entries[2].entry.stake_accounts_balance = Lamports(100); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(101); + validators.entries[1].stake_accounts_balance = Lamports(101); + validators.entries[2].stake_accounts_balance = Lamports(100); let undelegated_stake = Lamports(200); let targets = get_target_balance(undelegated_stake, &validators).unwrap(); @@ -379,10 +380,10 @@ mod test { #[test] fn get_unstake_from_active_validator_above_or_equal_threshold() { - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(10); - validators.entries[1].entry.stake_accounts_balance = Lamports(16); - validators.entries[2].entry.stake_accounts_balance = Lamports(10); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(10); + validators.entries[1].stake_accounts_balance = Lamports(16); + validators.entries[2].stake_accounts_balance = Lamports(10); let targets = get_target_balance(Lamports(0), &validators).unwrap(); @@ -408,10 +409,10 @@ mod test { #[test] fn get_unstake_from_active_validator_below_threshold() { - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(10); - validators.entries[1].entry.stake_accounts_balance = Lamports(16); - validators.entries[2].entry.stake_accounts_balance = Lamports(10); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(10); + validators.entries[1].stake_accounts_balance = Lamports(16); + validators.entries[2].stake_accounts_balance = Lamports(10); let targets = get_target_balance(Lamports(0), &validators).unwrap(); @@ -429,10 +430,10 @@ mod test { #[test] fn get_unstake_from_active_validator_because_another_needs_stake() { - let mut validators = Validators::new_fill_default(3); - validators.entries[0].entry.stake_accounts_balance = Lamports(17); - validators.entries[1].entry.stake_accounts_balance = Lamports(15); - validators.entries[2].entry.stake_accounts_balance = Lamports(0); + let mut validators = ValidatorList::new_default(3); + validators.entries[0].stake_accounts_balance = Lamports(17); + validators.entries[1].stake_accounts_balance = Lamports(15); + validators.entries[2].stake_accounts_balance = Lamports(0); let targets = get_target_balance(Lamports(0), &validators).unwrap(); diff --git a/program/src/big_vec.rs b/program/src/big_vec.rs new file mode 100644 index 000000000..12a0bc508 --- /dev/null +++ b/program/src/big_vec.rs @@ -0,0 +1,329 @@ +// Copied from https://github.com/solana-labs/solana-program-library/blob/1a0155e34bf96489db2cd498be79ca417c87c09f/stake-pool/program/src/big_vec.rs and modified + +//! Big vector type, used with vectors that can't be serde'd + +use { + crate::error::LidoError, + arrayref::array_ref, + borsh::{BorshDeserialize, BorshSerialize}, + solana_program::{program_error::ProgramError, program_pack::Pack}, + std::marker::PhantomData, +}; + +/// Contains easy to use utilities for a big vector of Borsh-compatible types, +/// to avoid managing the entire struct on-chain and blow through stack limits. +#[derive(Debug)] +pub struct BigVec<'data, T> { + /// Underlying data buffer, pieces of which are serialized + pub data: &'data mut [u8], + phantom: PhantomData, +} + +const VEC_SIZE_BYTES: usize = std::mem::size_of::(); + +impl<'data, T: Pack + Clone> BigVec<'data, T> { + /// Get the length of the vector + pub fn len(&self) -> u32 { + let vec_len = array_ref![self.data, 0, VEC_SIZE_BYTES]; + u32::from_le_bytes(*vec_len) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn new(data: &'data mut [u8]) -> Self { + Self { + data, + phantom: PhantomData, + } + } + + // Get start and end positions of slice at index + fn get_slice_bounds(&mut self, index: u32) -> Result<(usize, usize), ProgramError> { + if index >= self.len() { + return Err(LidoError::AccountListIndexOutOfBounds.into()); + } + let index = index as usize; + let start_index = VEC_SIZE_BYTES.saturating_add(index.saturating_mul(T::LEN)); + let end_index = start_index.saturating_add(T::LEN); + + if self.data.len() < end_index { + return Err(ProgramError::AccountDataTooSmall); + } + + if end_index - start_index != T::LEN { + // This only happends if start_index is very close to usize::MAX, + // which means that T::LEN should be huge. Solana does not allow such values on-chain + return Err(LidoError::AccountListIndexOutOfBounds.into()); + } + + Ok((start_index, end_index)) + } + + /// Get element at position + pub fn get_mut(&mut self, index: u32) -> Result<&mut T, ProgramError> { + let (start_index, end_index) = self.get_slice_bounds(index)?; + let ptr = self.data[start_index..end_index].as_ptr(); + Ok(unsafe { &mut *(ptr as *mut T) }) + } + + /// Removes an element from the vector and returns it. + /// The removed element is replaced by the last element of the vector. + /// This does not preserve ordering, but is O(1). If you need to preserve the element order, use remove instead + pub fn swap_remove(&mut self, index: u32) -> Result { + let (start_index, end_index) = self.get_slice_bounds(index)?; + let ptr = self.data[start_index..end_index].as_ptr(); + let value = unsafe { (*(ptr as *const T)).clone() }; + + let data_start_index = VEC_SIZE_BYTES; + let data_end_index = + data_start_index.saturating_add((self.len() as usize).saturating_mul(T::LEN)); + + // if not last element replace it with last + if index != self.len() - 1 { + self.data + .copy_within(data_end_index - T::LEN..data_end_index, start_index); + // If ever performance will be an issue try this code: + // unsafe { + // sol_memmove( + // self.data[start_index..end_index].as_mut_ptr(), + // self.data[data_end_index - T::LEN..data_end_index].as_mut_ptr(), + // T::LEN, + // ); + // } + } + + let new_len = self.len() - 1; + let mut vec_len_ref = &mut self.data[0..VEC_SIZE_BYTES]; + new_len.serialize(&mut vec_len_ref)?; + + Ok(value) + } + + /// Add new element to the end + pub fn push(&mut self, element: T) -> Result<(), ProgramError> { + let data_len = self.data.len(); + let mut vec_len_ref = &mut self.data[0..VEC_SIZE_BYTES]; + let mut vec_len = u32::try_from_slice(vec_len_ref)?; + + let start_index = VEC_SIZE_BYTES + vec_len as usize * T::LEN; + let end_index = start_index + T::LEN; + + if data_len < end_index { + return Err(ProgramError::AccountDataTooSmall); + } + + vec_len += 1; + vec_len.serialize(&mut vec_len_ref)?; + + let element_ref = &mut self.data[start_index..end_index]; + element.pack_into_slice(element_ref); + Ok(()) + } + + /// Get an iterator for the type provided + pub fn iter<'vec>(&'vec self) -> Iter<'data, 'vec, T> { + Iter { + len: self.len() as usize, + current: 0, + current_index: VEC_SIZE_BYTES, + inner: self, + phantom: PhantomData, + } + } + + /// Find matching data in the array + pub fn find(&self, data: &[u8], predicate: fn(&[u8], &[u8]) -> bool) -> Option<&T> { + let len = self.len() as usize; + let mut current = 0; + let mut current_index = VEC_SIZE_BYTES; + while current != len { + let end_index = current_index + T::LEN; + let current_slice = &self.data[current_index..end_index]; + if predicate(current_slice, data) { + return Some(unsafe { &*(current_slice.as_ptr() as *const T) }); + } + current_index = end_index; + current += 1; + } + None + } +} + +/// Iterator wrapper over a BigVec +pub struct Iter<'data, 'vec, T> { + len: usize, + current: usize, + current_index: usize, + inner: &'vec BigVec<'data, T>, + phantom: PhantomData, +} + +impl<'data, 'vec, T: Pack + 'data> Iterator for Iter<'data, 'vec, T> { + type Item = &'data T; + + fn next(&mut self) -> Option { + if self.current == self.len { + None + } else { + let end_index = self.current_index + T::LEN; + let value = Some(unsafe { + &*(self.inner.data[self.current_index..end_index].as_ptr() as *const T) + }); + self.current += 1; + self.current_index = end_index; + value + } + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_program::{program_memory::sol_memcmp, program_pack::Sealed}, + }; + + #[derive(Debug, PartialEq, Clone)] + struct TestStruct { + value: u64, + } + + impl Sealed for TestStruct {} + + impl Pack for TestStruct { + const LEN: usize = 8; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + self.value.serialize(&mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + Ok(TestStruct { + value: u64::try_from_slice(src).unwrap(), + }) + } + } + + impl TestStruct { + fn new(value: u64) -> Self { + Self { value } + } + } + + fn from_slice<'data, 'other>( + data: &'data mut [u8], + vec: &'other [u64], + ) -> BigVec<'data, TestStruct> { + let mut big_vec = BigVec::new(data); + for element in vec { + big_vec.push(TestStruct::new(*element)).unwrap(); + } + big_vec + } + + fn check_big_vec_eq(big_vec: &BigVec, slice: &[u64]) { + assert!(big_vec + .iter() + .map(|x| &x.value) + .zip(slice.iter()) + .all(|(a, b)| a == b)); + } + + #[test] + fn push() { + let mut data = [0u8; 4 + 8 * 3]; + let mut v = BigVec::new(&mut data); + v.push(TestStruct::new(1)).unwrap(); + check_big_vec_eq(&v, &[1]); + v.push(TestStruct::new(2)).unwrap(); + check_big_vec_eq(&v, &[1, 2]); + v.push(TestStruct::new(3)).unwrap(); + check_big_vec_eq(&v, &[1, 2, 3]); + assert_eq!( + v.push(TestStruct::new(4)).unwrap_err(), + ProgramError::AccountDataTooSmall + ); + } + + #[test] + fn at_position() { + let mut data = [0u8; 4 + 8 * 3]; + let mut v = from_slice(&mut data, &[1, 2, 3]); + + let elem = v.get_mut(0); + assert_eq!(elem.unwrap().value, 1); + + let elem = v.get_mut(1).unwrap(); + assert_eq!(elem.value, 2); + + elem.value = 22; + let elem = v.get_mut(1); + assert_eq!(elem.unwrap().value, 22); + + let elem = v.get_mut(2); + assert_eq!(elem.unwrap().value, 3); + + let elem = v.get_mut(3).unwrap_err(); + assert_eq!(elem, LidoError::AccountListIndexOutOfBounds.into()); + + let mut data = [0u8; 4 + 0]; + let mut v = from_slice(&mut data, &[]); + + let elem = v.get_mut(0).unwrap_err(); + assert_eq!(elem, LidoError::AccountListIndexOutOfBounds.into()); + } + + #[test] + fn swap_remove() { + let mut data = [0u8; 4 + 8 * 4]; + let mut v = from_slice(&mut data, &[1, 2, 3, 4]); + + let elem = v.swap_remove(1); + check_big_vec_eq(&v, &[1, 4, 3]); + assert_eq!(elem.unwrap().value, 2); + + let elem = v.swap_remove(0); + check_big_vec_eq(&v, &[3, 4]); + assert_eq!(elem.unwrap().value, 1); + + let elem = v.swap_remove(2).unwrap_err(); + check_big_vec_eq(&v, &[3, 4]); + assert_eq!(elem, LidoError::AccountListIndexOutOfBounds.into()); + + let elem = v.swap_remove(1); + check_big_vec_eq(&v, &[3]); + assert_eq!(elem.unwrap().value, 4); + + let elem = v.swap_remove(0); + check_big_vec_eq(&v, &[]); + assert_eq!(elem.unwrap().value, 3); + + let elem = v.swap_remove(0).unwrap_err(); + check_big_vec_eq(&v, &[]); + assert_eq!(elem, LidoError::AccountListIndexOutOfBounds.into()); + } + + fn find_predicate(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + false + } else { + sol_memcmp(a, b, a.len()) == 0 + } + } + + #[test] + fn find() { + let mut data = [0u8; 4 + 8 * 4]; + let v = from_slice(&mut data, &[1, 2, 3, 4]); + assert_eq!( + v.find(&1u64.to_le_bytes(), find_predicate), + Some(&TestStruct::new(1)) + ); + assert_eq!( + v.find(&4u64.to_le_bytes(), find_predicate), + Some(&TestStruct::new(4)) + ); + assert_eq!(v.find(&5u64.to_le_bytes(), find_predicate), None); + } +} diff --git a/program/src/error.rs b/program/src/error.rs index 058e59e1d..523b777c7 100644 --- a/program/src/error.rs +++ b/program/src/error.rs @@ -94,9 +94,8 @@ pub enum LidoError { /// in the structure InvalidAccountMember = 25, - /// Lido has an invalid size, calculated with the Lido's constant size plus - /// required to hold variable structures - InvalidLidoSize = 26, + /// Account has an invalid size, calculated with the constant size + InvalidAccountSize = 26, /// The instance has no validators. NoActiveValidators = 27, @@ -171,6 +170,36 @@ pub enum LidoError { /// Validation commission is more than 100% ValidationCommissionOutOfBounds = 48, + + /// The size of the given validator or maintainer list doesn't match the expected amount + UnexpectedListAccountSize = 49, + + /// Account has incorrect account type + InvalidAccountType = 50, + + /// Account list address does not belong to Lido + InvalidListAccount = 51, + + /// Lido version mismatch when deserializing + LidoVersionMismatch = 52, + + /// Index out of bounds when indexing into an account list + IndexOutOfBounds = 53, + + /// Pubkey at index does not match while indexing in account list + PubkeyIndexMismatch = 54, + + /// Index out of bounds when indexing into an account list + AccountListIndexOutOfBounds = 55, + + /// Validator list should be empty prior to state update + ValidatorListNotEmpty = 56, + + /// Stake was not distributed over validators yet, this is done at epoch end + ValidatorHasNoStake = 57, + + /// The reserve account address is wrong + IncorrectReserveAddress = 58, } // Just reuse the generated Debug impl for Display. It shows the variant names. diff --git a/program/src/instruction.rs b/program/src/instruction.rs index 4d2ffc832..b9b4e359d 100644 --- a/program/src/instruction.rs +++ b/program/src/instruction.rs @@ -12,7 +12,6 @@ use solana_program::{ pubkey::Pubkey, stake as stake_program, system_program, sysvar::{self, stake_history}, - vote, }; use crate::{ @@ -44,33 +43,25 @@ pub enum LidoInstruction { amount: Lamports, }, - /// Withdraw a given amount of stSOL. - /// - /// Caller provides some `amount` of StLamports that are to be burned in - /// order to withdraw SOL. + /// Deprecated in favour of WithdrawV2 Withdraw { #[allow(dead_code)] // but it's not amount: StLamports, }, - /// Move deposits from the reserve into a stake account and delegate it to a member validator. + /// Deprecated in favour of StakeDepositV2 StakeDeposit { #[allow(dead_code)] // but it's not amount: Lamports, }, - /// Unstake from a validator to a new stake account. + /// Deprecated in favour of UnstakeV2 Unstake { #[allow(dead_code)] // but it's not amount: Lamports, }, - /// Update the exchange rate, at the beginning of the epoch. - /// - /// This can be called by anybody. + /// Deprecated in favour of UpdateExchangeRateV2 UpdateExchangeRate, - /// Observe any external changes in the balances of a validator's stake accounts. - /// - /// If there is inactive balance in stake accounts, withdraw this back to the reserve. /// Deprecated in favour of UpdateStakeAccountBalance WithdrawInactiveStake, @@ -82,35 +73,30 @@ pub enum LidoInstruction { new_reward_distribution: RewardDistribution, }, - /// Add a new validator to the validator set. - /// - /// Requires the manager to sign. /// Deprecated in favour of AddValidatorV2 AddValidator, - /// Set the `active` flag to false for a given validator. - /// - /// Requires the manager to sign. - /// - /// Deactivation initiates the validator removal process: - /// - /// * It prevents new funds from being staked with the validator. - /// * It signals to the maintainer bot to start unstaking from this validator. - /// - /// Once there are no more delegations to this validator, and it has no - /// unclaimed fee credits, then the validator can be removed. + /// Deprecated in favour of DeactivateValidatorV2 DeactivateValidator, + /// Deprecated in favour of RemoveValidatorV2 RemoveValidator, + /// Deprecated in favour of AddMaintainerV2 AddMaintainer, + /// Deprecated in favour of RemoveMaintainerV2 RemoveMaintainer, + /// Deprecated in favour of MergeStakeV2 MergeStake, /// Observe any external changes in the balances of a validator's stake accounts. /// /// If there is inactive balance in stake accounts, withdraw this back to the reserve. /// Distribute fees. - UpdateStakeAccountBalance, + UpdateStakeAccountBalance { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, /// Add a new validator to the validator set. /// @@ -118,10 +104,14 @@ pub enum LidoInstruction { AddValidatorV2, /// Check if validator increased his commission over maximum allowed - /// and deactivate him if he did + /// or if vote account is closed, then deactivate it /// /// Requires no permission - DeactivateValidatorIfCommissionExceedsMax, + DeactivateValidatorIfCommissionExceedsMax { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, /// Set max_commission_percentage to control validator's fees. /// If validators exeed the threshold they will be deactivated by @@ -132,6 +122,94 @@ pub enum LidoInstruction { #[allow(dead_code)] // but it's not max_commission_percentage: u8, // percent in [0, 100] }, + + /// Move deposits from the reserve into a stake account and delegate it to a member validator. + StakeDepositV2 { + #[allow(dead_code)] // but it's not + amount: Lamports, + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + // Index of a maintainer in maintainer list + #[allow(dead_code)] // but it's not + maintainer_index: u32, + }, + + /// Unstake from a validator to a new stake account. + UnstakeV2 { + #[allow(dead_code)] // but it's not + amount: Lamports, + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + // Index of a maintainer in maintainer list + #[allow(dead_code)] // but it's not + maintainer_index: u32, + }, + + /// Update the exchange rate, at the beginning of the epoch. + /// + /// This can be called by anybody. + UpdateExchangeRateV2, + + /// Withdraw a given amount of stSOL. + /// + /// Caller provides some `amount` of StLamports that are to be burned in + /// order to withdraw SOL. + WithdrawV2 { + #[allow(dead_code)] // but it's not + amount: StLamports, + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, + + RemoveValidatorV2 { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, + + /// Set the `active` flag to false for a given validator. + /// + /// Requires the manager to sign. + /// + /// Deactivation initiates the validator removal process: + /// + /// * It prevents new funds from being staked with the validator. + /// * It signals to the maintainer bot to start unstaking from this validator. + /// + /// Once there are no more delegations to this validator, and it has no + /// unclaimed fee credits, then the validator can be removed. + DeactivateValidatorV2 { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, + + AddMaintainerV2, + RemoveMaintainerV2 { + // Index of a maintainer in maintainer list + #[allow(dead_code)] // but it's not + maintainer_index: u32, + }, + MergeStakeV2 { + // Index of a validator in validator list + #[allow(dead_code)] // but it's not + validator_index: u32, + }, + + /// Update Solido state to V2 + MigrateStateToV2 { + #[allow(dead_code)] // but it's not + reward_distribution: RewardDistribution, + #[allow(dead_code)] // but it's not + max_validators: u32, + #[allow(dead_code)] // but it's not + max_maintainers: u32, + #[allow(dead_code)] // but it's not + max_commission_percentage: u8, + }, } impl LidoInstruction { @@ -140,7 +218,7 @@ impl LidoInstruction { // `Borsh::serialize`, which takes an arbitrary writer, and which can // therefore return an IoError. But when serializing to a vec, there // is no IO, so for this particular writer, it should never fail. - self.try_to_vec() + borsh::to_vec(self) .expect("Serializing an Instruction to Vec does not fail.") } } @@ -171,6 +249,15 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + pub maintainer_list { + is_signer: false, + is_writable: true, + }, + const sysvar_rent = sysvar::rent::id(), const spl_token = spl_token::id(), } @@ -251,7 +338,7 @@ pub fn deposit( } accounts_struct! { - WithdrawAccountsMeta, WithdrawAccountsInfo { + WithdrawAccountsMetaV2, WithdrawAccountsInfoV2 { pub lido { is_signer: false, // Needs to be writable for us to update the metrics. @@ -294,6 +381,11 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + const spl_token = spl_token::id(), const sysvar_clock = sysvar::clock::id(), const system_program = system_program::id(), @@ -303,10 +395,14 @@ accounts_struct! { pub fn withdraw( program_id: &Pubkey, - accounts: &WithdrawAccountsMeta, + accounts: &WithdrawAccountsMetaV2, amount: StLamports, + validator_index: u32, ) -> Instruction { - let data = LidoInstruction::Withdraw { amount }; + let data = LidoInstruction::WithdrawV2 { + amount, + validator_index, + }; Instruction { program_id: *program_id, accounts: accounts.to_vec(), @@ -315,10 +411,10 @@ pub fn withdraw( } accounts_struct! { - StakeDepositAccountsMeta, StakeDepositAccountsInfo { + StakeDepositAccountsMetaV2, StakeDepositAccountsInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub maintainer { is_signer: true, @@ -359,6 +455,15 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + pub maintainer_list { + is_signer: false, + is_writable: false, + }, + const sysvar_clock = sysvar::clock::id(), const system_program = system_program::id(), const sysvar_rent = sysvar::rent::id(), @@ -370,10 +475,16 @@ accounts_struct! { pub fn stake_deposit( program_id: &Pubkey, - accounts: &StakeDepositAccountsMeta, + accounts: &StakeDepositAccountsMetaV2, amount: Lamports, + validator_index: u32, + maintainer_index: u32, ) -> Instruction { - let data = LidoInstruction::StakeDeposit { amount }; + let data = LidoInstruction::StakeDepositV2 { + amount, + validator_index, + maintainer_index, + }; Instruction { program_id: *program_id, accounts: accounts.to_vec(), @@ -382,10 +493,10 @@ pub fn stake_deposit( } accounts_struct! { - UnstakeAccountsMeta, UnstakeAccountsInfo { + UnstakeAccountsMetaV2, UnstakeAccountsInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub maintainer { is_signer: true, @@ -416,6 +527,15 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + pub maintainer_list { + is_signer: false, + is_writable: false, + }, + // Required to call `solana_program::stake::instruction::deactivate_stake`. const sysvar_clock = sysvar::clock::id(), // Required to call cross-program. @@ -427,10 +547,16 @@ accounts_struct! { pub fn unstake( program_id: &Pubkey, - accounts: &UnstakeAccountsMeta, + accounts: &UnstakeAccountsMetaV2, amount: Lamports, + validator_index: u32, + maintainer_index: u32, ) -> Instruction { - let data = LidoInstruction::Unstake { amount }; + let data = LidoInstruction::UnstakeV2 { + amount, + validator_index, + maintainer_index, + }; Instruction { program_id: *program_id, accounts: accounts.to_vec(), @@ -439,7 +565,7 @@ pub fn unstake( } accounts_struct! { - UpdateExchangeRateAccountsMeta, UpdateExchangeRateAccountsInfo { + UpdateExchangeRateAccountsMetaV2, UpdateExchangeRateAccountsInfoV2 { pub lido { is_signer: false, is_writable: true, @@ -452,98 +578,24 @@ accounts_struct! { is_signer: false, is_writable: false, }, - const sysvar_clock = sysvar::clock::id(), - const sysvar_rent = sysvar::rent::id(), - } -} - -pub fn update_exchange_rate( - program_id: &Pubkey, - accounts: &UpdateExchangeRateAccountsMeta, -) -> Instruction { - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: LidoInstruction::UpdateExchangeRate.to_vec(), - } -} - -accounts_struct! { - // Note: there are no signers among these accounts, updating a validator - // account is permissionless, anybody can do it. - CollectValidatorFeeMeta, CollectValidatorFeeInfo { - pub lido { - is_signer: false, - is_writable: true, - }, - // The validator to update the balance for. - // Needs to be writable so we withdraw from it. - pub validator_vote_account { - is_signer: false, - // Is writable due to withdraw to reserve (vote_instruction::withdraw) - is_writable: true, - }, - - // Updating balances also immediately mints rewards, so we need the stSOL - // mint, and the fee accounts to deposit the stSOL into. - pub st_sol_mint { - is_signer: false, - // Is writable due to fee mint (spl_token::instruction::mint_to) - is_writable: true, - }, - - // Mint authority is required to mint tokens. - pub mint_authority { - is_signer: false, - is_writable: false, - }, - - pub treasury_st_sol_account { - is_signer: false, - // Is writable due to fee mint (spl_token::instruction::mint_to) to treasury - is_writable: true, - }, - pub developer_st_sol_account { - is_signer: false, - // Is writable due to fee mint (spl_token::instruction::mint_to) to developer - is_writable: true, - }, - - pub reserve { - is_signer: false, - // Is writable due to withdraw to reserve (vote_instruction::withdraw) - is_writable: true, - }, - // Used to get the rewards out of the validator vote account. - pub rewards_withdraw_authority { + pub validator_list { is_signer: false, is_writable: false, }, - // We only allow updating balances if the exchange rate is up to date, - // so we need to know the current epoch. const sysvar_clock = sysvar::clock::id(), - - // Needed for minting rewards. - const spl_token_program = spl_token::id(), - - // Needed to calculate the validator's vote account rent exempt, so it - // can subtracted from the rewards. const sysvar_rent = sysvar::rent::id(), - - // Needed to withdraw from the vote account. - const vote_program = vote::program::id(), } } -pub fn collect_validator_fee( +pub fn update_exchange_rate( program_id: &Pubkey, - accounts: &CollectValidatorFeeMeta, + accounts: &UpdateExchangeRateAccountsMetaV2, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::CollectValidatorFee.to_vec(), + data: LidoInstruction::UpdateExchangeRateV2.to_vec(), } } @@ -578,41 +630,48 @@ pub fn change_reward_distribution( Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::ChangeRewardDistribution { + data: borsh::to_vec(&LidoInstruction::ChangeRewardDistribution { new_reward_distribution, - } + }) // Serializing the instruction should never fail. - .try_to_vec() .unwrap(), } } accounts_struct! { - RemoveValidatorMeta, RemoveValidatorInfo { + RemoveValidatorMetaV2, RemoveValidatorInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub validator_vote_account_to_remove { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, } } -pub fn remove_validator(program_id: &Pubkey, accounts: &RemoveValidatorMeta) -> Instruction { +pub fn remove_validator( + program_id: &Pubkey, + accounts: &RemoveValidatorMetaV2, + validator_index: u32, +) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::RemoveValidator.to_vec(), + data: LidoInstruction::RemoveValidatorV2 { validator_index }.to_vec(), } } accounts_struct! { - DeactivateValidatorMeta, DeactivateValidatorInfo { + DeactivateValidatorMetaV2, DeactivateValidatorInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub manager { is_signer: true, @@ -622,59 +681,60 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, } } pub fn deactivate_validator( program_id: &Pubkey, - accounts: &DeactivateValidatorMeta, + accounts: &DeactivateValidatorMetaV2, + validator_index: u32, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::DeactivateValidator.to_vec(), + data: LidoInstruction::DeactivateValidatorV2 { validator_index }.to_vec(), } } accounts_struct! { - ClaimValidatorFeeMeta, ClaimValidatorFeeInfo { + AddMaintainerMetaV2, AddMaintainerInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, - pub st_sol_mint { - is_signer: false, - // Is writable due to fee mint (spl_token::instruction::mint_to) to validator fee - // st_sol account - is_writable: true, + pub manager { + is_signer: true, + is_writable: false, }, - pub mint_authority { + pub maintainer { is_signer: false, is_writable: false, }, - pub validator_fee_st_sol_account { + pub maintainer_list { is_signer: false, - // Is writable due to fee mint (spl_token::instruction::mint_to) to validator fee - // st_sol account is_writable: true, }, - const spl_token = spl_token::id(), + } } -pub fn claim_validator_fee(program_id: &Pubkey, accounts: &ClaimValidatorFeeMeta) -> Instruction { +pub fn add_maintainer(program_id: &Pubkey, accounts: &AddMaintainerMetaV2) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::ClaimValidatorFee.to_vec(), + data: LidoInstruction::AddMaintainerV2.to_vec(), } } accounts_struct! { - AddMaintainerMeta, AddMaintainerInfo { + RemoveMaintainerMetaV2, RemoveMaintainerInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub manager { is_signer: true, @@ -684,47 +744,31 @@ accounts_struct! { is_signer: false, is_writable: false, }, - } -} - -pub fn add_maintainer(program_id: &Pubkey, accounts: &AddMaintainerMeta) -> Instruction { - Instruction { - program_id: *program_id, - accounts: accounts.to_vec(), - data: LidoInstruction::AddMaintainer.to_vec(), - } -} - -accounts_struct! { - RemoveMaintainerMeta, RemoveMaintainerInfo { - pub lido { + pub maintainer_list { is_signer: false, is_writable: true, }, - pub manager { - is_signer: true, - is_writable: false, - }, - pub maintainer { - is_signer: false, - is_writable: false, - }, + } } -pub fn remove_maintainer(program_id: &Pubkey, accounts: &RemoveMaintainerMeta) -> Instruction { +pub fn remove_maintainer( + program_id: &Pubkey, + accounts: &RemoveMaintainerMetaV2, + maintainer_index: u32, +) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::RemoveMaintainer.to_vec(), + data: LidoInstruction::RemoveMaintainerV2 { maintainer_index }.to_vec(), } } accounts_struct! { - MergeStakeMeta, MergeStakeInfo { + MergeStakeMetaV2, MergeStakeInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub validator_vote_account { is_signer: false, @@ -747,18 +791,27 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, const sysvar_clock = sysvar::clock::id(), const stake_history = stake_history::id(), const stake_program = stake_program::program::id(), } } -pub fn merge_stake(program_id: &Pubkey, accounts: &MergeStakeMeta) -> Instruction { +pub fn merge_stake( + program_id: &Pubkey, + accounts: &MergeStakeMetaV2, + validator_index: u32, +) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), // this can fail on OutOfMemory - data: LidoInstruction::MergeStake.try_to_vec().unwrap(), // This should never fail. + data: borsh::to_vec(&LidoInstruction::MergeStakeV2 { validator_index }) + .unwrap(), // This should never fail. } } @@ -766,7 +819,7 @@ accounts_struct! { AddValidatorMetaV2, AddValidatorInfoV2 { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub manager { is_signer: true, @@ -776,6 +829,10 @@ accounts_struct! { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, } } @@ -839,6 +896,10 @@ accounts_struct! { // Is writable due to fee mint (spl_token::instruction::mint_to) to developer is_writable: true, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, // Needed for minting rewards. const spl_token_program = spl_token::id(), @@ -865,11 +926,12 @@ accounts_struct! { pub fn update_stake_account_balance( program_id: &Pubkey, accounts: &UpdateStakeAccountBalanceMeta, + validator_index: u32, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::UpdateStakeAccountBalance.to_vec(), + data: LidoInstruction::UpdateStakeAccountBalance { validator_index }.to_vec(), } } @@ -878,23 +940,29 @@ accounts_struct! { DeactivateValidatorIfCommissionExceedsMaxInfo { pub lido { is_signer: false, - is_writable: true, + is_writable: false, }, pub validator_vote_account_to_deactivate { is_signer: false, is_writable: false, }, + pub validator_list { + is_signer: false, + is_writable: true, + }, } } pub fn deactivate_validator_if_commission_exceeds_max( program_id: &Pubkey, accounts: &DeactivateValidatorIfCommissionExceedsMaxMeta, + validator_index: u32, ) -> Instruction { Instruction { program_id: *program_id, accounts: accounts.to_vec(), - data: LidoInstruction::DeactivateValidatorIfCommissionExceedsMax.to_vec(), + data: LidoInstruction::DeactivateValidatorIfCommissionExceedsMax { validator_index } + .to_vec(), } } @@ -925,3 +993,50 @@ pub fn set_max_commission_percentage( data: data.to_vec(), } } + +accounts_struct! { + MigrateStateToV2Meta, MigrateStateToV2Info { + pub lido { + is_signer: false, + // Needs to be writable for us to update the metrics. + is_writable: true, + }, + pub manager { + is_signer: true, + is_writable: false, + }, + pub validator_list { + is_signer: false, + is_writable: true, + }, + pub maintainer_list { + is_signer: false, + is_writable: true, + }, + pub developer_account { + is_signer: false, + is_writable: false, + }, + } +} + +pub fn migrate_state_to_v2( + program_id: &Pubkey, + reward_distribution: RewardDistribution, + max_validators: u32, + max_maintainers: u32, + max_commission_percentage: u8, + accounts: &MigrateStateToV2Meta, +) -> Instruction { + let data = LidoInstruction::MigrateStateToV2 { + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + }; + Instruction { + program_id: *program_id, + accounts: accounts.to_vec(), + data: data.to_vec(), + } +} diff --git a/program/src/lib.rs b/program/src/lib.rs index d8eb841b3..f2b9a5204 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -6,7 +6,6 @@ use solana_program::pubkey::Pubkey; #[cfg(not(feature = "no-entrypoint"))] pub mod entrypoint; -pub mod account_map; pub mod accounts; pub mod balance; pub mod error; @@ -20,6 +19,7 @@ pub mod state; pub mod token; pub mod util; +pub mod big_vec; pub mod vote_state; /// Seed for reserve account that holds SOL. @@ -47,7 +47,7 @@ pub fn find_authority_program_address( Pubkey::find_program_address(&[&lido_address.to_bytes(), authority], program_id) } -/// The minimum amount to put in a stake account (1 SOL). +/// The minimum amount to put in a stake account (1 SOL + rent_exempt). /// /// For stake accounts, there is a minimum balance for the account to be /// rent-exempt, that depends on the size of the stake program's stake state @@ -58,7 +58,8 @@ pub fn find_authority_program_address( /// need to be able to merge stake accounts, we also need to make sure that they /// contain enough stake that they will earn at least one lamport per epoch. /// 1 SOL should be sufficient for that. -pub const MINIMUM_STAKE_ACCOUNT_BALANCE: token::Lamports = token::Lamports(1_000_000_000); +/// https://github.com/solana-labs/solana/issues/24357#issuecomment-1225776709 +pub const MINIMUM_STAKE_ACCOUNT_BALANCE: token::Lamports = token::Lamports(1_002_282_880); /// The maximum number of unstake accounts that a validator can have simultaneously. pub const MAXIMUM_UNSTAKE_ACCOUNTS: u64 = 3; diff --git a/program/src/logic.rs b/program/src/logic.rs index af7ce24ac..1fa6db94c 100644 --- a/program/src/logic.rs +++ b/program/src/logic.rs @@ -15,8 +15,8 @@ use crate::processor::StakeType; use crate::STAKE_AUTHORITY; use crate::{ error::LidoError, - instruction::{UnstakeAccountsInfo, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfo}, - state::Lido, + instruction::{UnstakeAccountsInfoV2, UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2}, + state::{AccountType, Lido, ListEntry, Validator}, token::{Lamports, StLamports}, MINT_AUTHORITY, RESERVE_ACCOUNT, }; @@ -42,6 +42,8 @@ pub(crate) fn check_mint( mint: &AccountInfo, mint_authority: &Pubkey, ) -> Result<(), ProgramError> { + check_account_owner(mint, &spl_token::id())?; + if !rent.is_exempt(mint.lamports(), mint.data_len()) { msg!("Mint is not rent-exempt"); return Err(ProgramError::AccountNotRentExempt); @@ -67,6 +69,15 @@ pub(crate) fn check_mint( msg!("Mint should have an authority."); return Err(LidoError::InvalidMint.into()); } + + if let COption::Some(authority) = spl_mint.freeze_authority { + msg!( + "Mint should not have a freeze authority, but it is set to {}.", + authority + ); + return Err(LidoError::InvalidMint.into()); + } + Ok(()) } @@ -227,7 +238,7 @@ pub fn mint_st_sol_to<'a>( /// * The account account must be an stSOL SPL token account. pub fn burn_st_sol<'a, 'b>( solido: &Lido, - accounts: &WithdrawAccountsInfo<'a, 'b>, + accounts: &WithdrawAccountsInfoV2<'a, 'b>, amount: StLamports, ) -> ProgramResult { solido.check_mint_is_st_sol_mint(accounts.st_sol_mint)?; @@ -271,7 +282,7 @@ pub fn burn_st_sol<'a, 'b>( // Set the stake and withdraw authority of the destination stake account to the // user’s pubkey. pub fn transfer_stake_authority( - accounts: &WithdrawAccountsInfo, + accounts: &WithdrawAccountsInfoV2, stake_authority_bump_seed: u8, ) -> ProgramResult { invoke_signed( @@ -381,21 +392,19 @@ pub fn distribute_fees( /// by the validator's seeds. Returns the destination bump seed. pub fn check_unstake_accounts( program_id: &Pubkey, - lido: &Lido, - accounts: &UnstakeAccountsInfo, + validator: &Validator, + accounts: &UnstakeAccountsInfoV2, ) -> Result { - let validator = lido.validators.get(accounts.validator_vote_account.key)?; - // If a validator doesn't have a stake account, it cannot be unstaked. - if !validator.entry.has_stake_accounts() { + if !validator.has_stake_accounts() { msg!( "Attempting to unstake from a validator {} that has no stake accounts.", - validator.pubkey + validator.pubkey() ); return Err(LidoError::InvalidStakeAccount.into()); } - let source_stake_seed = validator.entry.stake_seeds.begin; - let destination_stake_seed = validator.entry.unstake_seeds.end; + let source_stake_seed = validator.stake_seeds.begin; + let destination_stake_seed = validator.unstake_seeds.end; let (source_stake_account, _) = validator.find_stake_account_address( program_id, @@ -506,6 +515,61 @@ pub fn split_stake_account( Ok(()) } +/// Check first bytes are zeros, zero remaining bytes and check allocated size is correct. +pub fn check_account_data( + account: &AccountInfo, + expected_size: usize, + account_type: AccountType, +) -> ProgramResult { + // Take minimum to stay in a slice bounds and be under compute budget + let bytes_to_check = std::cmp::min(account.data_len(), Lido::get_bytes_to_check()); + + // Can't check all bytes because of compute limit + if !&account.data.borrow()[..bytes_to_check] + .iter() + .all(|byte| *byte == 0) + { + msg!( + "Account {} appears to be in use already, refusing to overwrite.", + account.key + ); + return Err(LidoError::AlreadyInUse.into()); + } + + // zero out remaining bytes + account.data.borrow_mut()[bytes_to_check..].fill(0); + + if account.data_len() < expected_size { + msg!( + "Incorrect allocated bytes for {:?} account: {}, should be at least {}", + account_type, + account.data_len(), + expected_size + ); + return Err(LidoError::InvalidAccountSize.into()); + } + + Ok(()) +} + +/// Check account owner is the given program +pub fn check_account_owner( + account_info: &AccountInfo, + program_id: &Pubkey, +) -> Result<(), ProgramError> { + if *program_id != *account_info.owner { + msg!( + "Expected account {} to be owned by program {}, received {}", + account_info.key, + program_id, + account_info.owner + ); + Err(LidoError::InvalidOwner.into()) + } else { + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/program/src/process_management.rs b/program/src/process_management.rs index 04998741c..8b8ff3f33 100644 --- a/program/src/process_management.rs +++ b/program/src/process_management.rs @@ -1,10 +1,10 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 +use solana_program::program::invoke_signed; use solana_program::rent::Rent; use solana_program::sysvar::Sysvar; use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey}; -use solana_program::{program::invoke_signed, program_error::ProgramError}; use crate::logic::check_rent_exempt; use crate::processor::StakeType; @@ -13,11 +13,11 @@ use crate::vote_state::PartialVoteState; use crate::{ error::LidoError, instruction::{ - AddMaintainerInfo, AddValidatorInfoV2, ChangeRewardDistributionInfo, - DeactivateValidatorIfCommissionExceedsMaxInfo, DeactivateValidatorInfo, MergeStakeInfo, - RemoveMaintainerInfo, RemoveValidatorInfo, SetMaxValidationCommissionInfo, + AddMaintainerInfoV2, AddValidatorInfoV2, ChangeRewardDistributionInfo, + DeactivateValidatorIfCommissionExceedsMaxInfo, DeactivateValidatorInfoV2, MergeStakeInfoV2, + RemoveMaintainerInfoV2, RemoveValidatorInfoV2, SetMaxValidationCommissionInfo, }, - state::{RewardDistribution, Validator}, + state::{ListEntry, Maintainer, RewardDistribution, Validator}, vote_state::get_vote_account_commission, STAKE_AUTHORITY, }; @@ -43,7 +43,7 @@ pub fn process_change_reward_distribution( pub fn process_add_validator(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { let accounts = AddValidatorInfoV2::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; let rent = &Rent::get()?; lido.check_manager(accounts.manager)?; @@ -53,18 +53,21 @@ pub fn process_add_validator(program_id: &Pubkey, accounts_raw: &[AccountInfo]) "Validator vote account", )?; // Deserialize also checks if the vote account is a valid Solido vote - // account: The vote account should be owned by the vote program, the - // withdraw authority should be set to the program_id, and it should + // account: The vote account should be owned by the vote program and it should // satisfy the commission limit. let _partial_vote_state = PartialVoteState::deserialize( accounts.validator_vote_account, lido.max_commission_percentage, )?; - lido.validators - .add(*accounts.validator_vote_account.key, Validator::new())?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; - lido.save(accounts.lido) + validators.push(Validator::new(*accounts.validator_vote_account.key)) } /// Remove a validator. @@ -75,20 +78,28 @@ pub fn process_add_validator(program_id: &Pubkey, accounts_raw: &[AccountInfo]) /// no more stake delegated to it, removing it from the list can be done by anybody. pub fn process_remove_validator( program_id: &Pubkey, + validator_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { - let accounts = RemoveValidatorInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let accounts = RemoveValidatorInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - let removed_validator = lido - .validators - .remove(accounts.validator_vote_account_to_remove.key)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let removed_validator = validators.remove( + validator_index, + accounts.validator_vote_account_to_remove.key, + )?; let result = removed_validator.check_can_be_removed(); Validator::show_removed_error_msg(&result); result?; - - lido.save(accounts.lido) + Ok(()) } /// Set the `active` flag to false for a given validator. @@ -97,78 +108,111 @@ pub fn process_remove_validator( /// removing the validator once no stake is delegated to it any more. pub fn process_deactivate_validator( program_id: &Pubkey, + validator_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { - let accounts = DeactivateValidatorInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let accounts = DeactivateValidatorInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_manager(accounts.manager)?; - let validator = lido - .validators - .get_mut(accounts.validator_vote_account_to_deactivate.key)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; - validator.entry.active = false; - msg!("Validator {} deactivated.", validator.pubkey); + let validator = validators.get_mut( + validator_index, + accounts.validator_vote_account_to_deactivate.key, + )?; - lido.save(accounts.lido) + validator.active = false; + msg!("Validator {} deactivated.", validator.pubkey()); + Ok(()) } -/// Set the `active` flag to false for a given validator if it's commission is -/// bigger then max allowed. It is permissionless. +/// Mark validator inactive if it's commission is bigger then max +/// allowed or if it's vote account is closed. It is permissionless. /// /// This prevents new funds from being staked with this validator, and enables /// removing the validator once no stake is delegated to it any more. pub fn process_deactivate_validator_if_commission_exceeds_max( program_id: &Pubkey, + validator_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { let accounts = DeactivateValidatorIfCommissionExceedsMaxInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - let data = accounts.validator_vote_account_to_deactivate.data.borrow(); - let commission = get_vote_account_commission(&data).ok_or(ProgramError::AccountDataTooSmall)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; - if commission <= lido.max_commission_percentage { + let validator = validators.get_mut( + validator_index, + accounts.validator_vote_account_to_deactivate.key, + )?; + + if !validator.active { return Ok(()); } - let validator = lido - .validators - .get_mut(accounts.validator_vote_account_to_deactivate.key)?; + if accounts.validator_vote_account_to_deactivate.owner == &solana_program::vote::program::id() { + let data = accounts.validator_vote_account_to_deactivate.data.borrow(); + let commission = get_vote_account_commission(&data)?; - if !validator.entry.active { - return Ok(()); + if commission <= lido.max_commission_percentage { + return Ok(()); + } + } else { + // The vote account is closed by node operator } - validator.entry.active = false; - msg!("Validator {} deactivated.", validator.pubkey); + validator.active = false; + msg!("Validator {} deactivated.", validator.pubkey()); - lido.save(accounts.lido) + Ok(()) } /// Adds a maintainer to the list of maintainers pub fn process_add_maintainer(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { - let accounts = AddMaintainerInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let accounts = AddMaintainerInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_manager(accounts.manager)?; - lido.maintainers.add(*accounts.maintainer.key, ())?; + let maintainer_list_data = &mut *accounts.maintainer_list.data.borrow_mut(); + let mut maintainers = lido.deserialize_account_list_info::( + program_id, + accounts.maintainer_list, + maintainer_list_data, + )?; - lido.save(accounts.lido) + maintainers.push(Maintainer::new(*accounts.maintainer.key)) } /// Removes a maintainer from the list of maintainers pub fn process_remove_maintainer( program_id: &Pubkey, + maintainer_index: u32, accounts_raw: &[AccountInfo], ) -> ProgramResult { - let accounts = RemoveMaintainerInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let accounts = RemoveMaintainerInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_manager(accounts.manager)?; - lido.maintainers.remove(accounts.maintainer.key)?; + let maintainer_list_data = &mut *accounts.maintainer_list.data.borrow_mut(); + let mut maintainers = lido.deserialize_account_list_info::( + program_id, + accounts.maintainer_list, + maintainer_list_data, + )?; - lido.save(accounts.lido) + maintainers.remove(maintainer_index, accounts.maintainer.key)?; + Ok(()) } /// Sets max validation commission for Lido. If validators exeed the threshold @@ -192,16 +236,6 @@ pub fn process_set_max_commission_percentage( lido.save(accounts.lido) } -/// TODO(#186) Allow validator to change fee account -/// Called by the validator, changes the fee account which the validator -/// receives tokens -pub fn _process_change_validator_fee_account( - _program_id: &Pubkey, - _accounts: &[AccountInfo], -) -> ProgramResult { - unimplemented!() -} - /// Merge two stake accounts from the beginning of the validator's stake /// accounts list. /// This function can be called by anybody. @@ -209,18 +243,28 @@ pub fn _process_change_validator_fee_account( /// exist and is merged with the stake defined by `stake_accounts_seed_begin + /// 1`, and `stake_accounts_seed_begin` is incremented by one. /// All fully active stake accounts precede the activating stake accounts. -pub fn process_merge_stake(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ProgramResult { - let accounts = MergeStakeInfo::try_from_slice(accounts_raw)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; +pub fn process_merge_stake( + program_id: &Pubkey, + validator_index: u32, + accounts_raw: &[AccountInfo], +) -> ProgramResult { + let accounts = MergeStakeInfoV2::try_from_slice(accounts_raw)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - let mut validator = lido - .validators - .get_mut(accounts.validator_vote_account.key)?; - let from_seed = validator.entry.stake_seeds.begin; - let to_seed = validator.entry.stake_seeds.begin + 1; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validator = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let validator = validator.get_mut(validator_index, accounts.validator_vote_account.key)?; + + let from_seed = validator.stake_seeds.begin; + let to_seed = validator.stake_seeds.begin + 1; // Check that there are at least two accounts to merge - if to_seed >= validator.entry.stake_seeds.end { + if to_seed >= validator.stake_seeds.end { msg!("Attempting to merge accounts in a validator that has fewer than two stake accounts."); return Err(LidoError::InvalidStakeAccount.into()); } @@ -257,7 +301,7 @@ pub fn process_merge_stake(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ); return Err(LidoError::InvalidStakeAccount.into()); } - validator.entry.stake_seeds.begin += 1; + validator.stake_seeds.begin += 1; // Merge `from_stake_addr` to `to_stake_addr`, at the end of the // instruction, `from_stake_addr` ceases to exist. let merge_instructions = solana_program::stake::instruction::merge( @@ -288,5 +332,5 @@ pub fn process_merge_stake(program_id: &Pubkey, accounts_raw: &[AccountInfo]) -> ]], )?; - lido.save(accounts.lido) + Ok(()) } diff --git a/program/src/processor.rs b/program/src/processor.rs index 284c1e37b..027d2cc61 100644 --- a/program/src/processor.rs +++ b/program/src/processor.rs @@ -8,15 +8,15 @@ use std::ops::{Add, Sub}; use crate::{ error::LidoError, instruction::{ - DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, StakeDepositAccountsInfo, - UnstakeAccountsInfo, UpdateExchangeRateAccountsInfo, UpdateStakeAccountBalanceInfo, - WithdrawAccountsInfo, + DepositAccountsInfo, InitializeAccountsInfo, LidoInstruction, MigrateStateToV2Info, + StakeDepositAccountsInfoV2, UnstakeAccountsInfoV2, UpdateExchangeRateAccountsInfoV2, + UpdateStakeAccountBalanceInfo, WithdrawAccountsInfoV2, }, logic::{ - burn_st_sol, check_mint, check_rent_exempt, check_unstake_accounts, - create_account_even_if_funded, distribute_fees, initialize_stake_account_undelegated, - mint_st_sol_to, split_stake_account, transfer_stake_authority, CreateAccountOptions, - SplitStakeAccounts, + burn_st_sol, check_account_data, check_account_owner, check_mint, check_rent_exempt, + check_unstake_accounts, create_account_even_if_funded, distribute_fees, + initialize_stake_account_undelegated, mint_st_sol_to, split_stake_account, + transfer_stake_authority, CreateAccountOptions, SplitStakeAccounts, }, metrics::Metrics, process_management::{ @@ -27,15 +27,15 @@ use crate::{ }, stake_account::{deserialize_stake_account, StakeAccount}, state::{ - ExchangeRate, FeeRecipients, Lido, Maintainers, RewardDistribution, Validator, Validators, - LIDO_CONSTANT_SIZE, LIDO_VERSION, + AccountType, ExchangeRate, FeeRecipients, Lido, LidoV1, ListEntry, Maintainer, + MaintainerList, RewardDistribution, StakeDeposit, Validator, ValidatorList, }, token::{Lamports, Rational, StLamports}, MAXIMUM_UNSTAKE_ACCOUNTS, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, }; -use solana_program::stake::{self as stake_program}; +use solana_program::stake as stake_program; use solana_program::stake_history::StakeHistory; use { borsh::BorshDeserialize, @@ -56,7 +56,6 @@ use { /// Program state handler. pub fn process_initialize( - version: u8, program_id: &Pubkey, reward_distribution: RewardDistribution, max_validators: u32, @@ -68,35 +67,58 @@ pub fn process_initialize( let rent = &Rent::get()?; check_rent_exempt(rent, accounts.lido, "Solido account")?; check_rent_exempt(rent, accounts.reserve_account, "Reserve account")?; + check_rent_exempt(rent, accounts.validator_list, "Validator list account")?; + check_rent_exempt(rent, accounts.maintainer_list, "Maintainer list account")?; + + check_account_owner(accounts.lido, program_id)?; + check_account_owner(accounts.validator_list, program_id)?; + check_account_owner(accounts.maintainer_list, program_id)?; + + check_account_data(accounts.lido, Lido::LEN, AccountType::Lido)?; + check_account_data( + accounts.validator_list, + ValidatorList::required_bytes(max_validators), + AccountType::Validator, + )?; + check_account_data( + accounts.maintainer_list, + MaintainerList::required_bytes(max_maintainers), + AccountType::Maintainer, + )?; - let is_uninitialized = accounts.lido.data.borrow()[..LIDO_CONSTANT_SIZE] - .iter() - .all(|byte| *byte == 0); - if !is_uninitialized { - msg!( - "Account {} appears to be in use already, refusing to overwrite.", - accounts.lido.key - ); + if accounts.lido.key == accounts.validator_list.key { + msg!("Cannot use same account for Lido and validator list"); return Err(LidoError::AlreadyInUse.into()); } - - // Bytes required for maintainers - let bytes_for_maintainers = Maintainers::required_bytes(max_maintainers as usize); - // Bytes required for validators - let bytes_for_validators = Validators::required_bytes(max_validators as usize); - // Calculate the expected lido's size - let bytes_sum = LIDO_CONSTANT_SIZE + bytes_for_validators + bytes_for_maintainers; - if bytes_sum != accounts.lido.data_len() { - msg!("Incorrect allocated bytes for the provided constrains: max_validator bytes: {}, max_maintainers bytes: {}, constant_size: {}, sum is {}, should be {}", bytes_for_validators, bytes_for_maintainers, LIDO_CONSTANT_SIZE, bytes_sum, accounts.lido.data_len()); - return Err(LidoError::InvalidLidoSize.into()); + if accounts.lido.key == accounts.maintainer_list.key { + msg!("Cannot use same account for Lido and maintainer list"); + return Err(LidoError::AlreadyInUse.into()); } + if accounts.validator_list.key == accounts.maintainer_list.key { + msg!("Cannot use same account for validator list and maintainer list"); + return Err(LidoError::AlreadyInUse.into()); + } + + let mut validators = ValidatorList::new_default(0); + validators.header.max_entries = max_validators; - let (_, reserve_bump_seed) = Pubkey::find_program_address( + let mut maintainers = MaintainerList::new_default(0); + maintainers.header.max_entries = max_maintainers; + + let (reserve_account_pda, reserve_bump_seed) = Pubkey::find_program_address( &[&accounts.lido.key.to_bytes(), RESERVE_ACCOUNT], program_id, ); + if &reserve_account_pda != accounts.reserve_account.key { + msg!( + "Resrve account {} is incorrect, should be {}", + accounts.reserve_account.key, + reserve_account_pda + ); + return Err(LidoError::IncorrectReserveAddress.into()); + } - let (_, deposit_bump_seed) = Pubkey::find_program_address( + let (_, stake_bump_seed) = Pubkey::find_program_address( &[&accounts.lido.key.to_bytes(), STAKE_AUTHORITY], program_id, ); @@ -112,21 +134,22 @@ pub fn process_initialize( // Initialize fee structure let lido = Lido { - lido_version: version, + lido_version: Lido::VERSION, + account_type: AccountType::Lido, manager: *accounts.manager.key, st_sol_mint: *accounts.st_sol_mint.key, exchange_rate: ExchangeRate::default(), sol_reserve_account_bump_seed: reserve_bump_seed, mint_authority_bump_seed: mint_bump_seed, - stake_authority_bump_seed: deposit_bump_seed, + stake_authority_bump_seed: stake_bump_seed, reward_distribution, fee_recipients: FeeRecipients { treasury_account: *accounts.treasury_account.key, developer_account: *accounts.developer_account.key, }, metrics: Metrics::new(), - maintainers: Maintainers::new(max_maintainers), - validators: Validators::new(max_validators), + validator_list: *accounts.validator_list.key, + maintainer_list: *accounts.maintainer_list.key, max_commission_percentage, }; @@ -134,6 +157,8 @@ pub fn process_initialize( lido.check_is_st_sol_account(accounts.treasury_account)?; lido.check_is_st_sol_account(accounts.developer_account)?; + validators.save(accounts.validator_list)?; + maintainers.save(accounts.maintainer_list)?; lido.save(accounts.lido) } @@ -189,67 +214,107 @@ pub fn process_deposit( pub fn process_stake_deposit( program_id: &Pubkey, amount: Lamports, + validator_index: u32, + maintainer_index: u32, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = StakeDepositAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = StakeDepositAccountsInfoV2::try_from_slice(raw_accounts)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; - lido.check_maintainer(accounts.maintainer)?; + lido.check_maintainer( + program_id, + accounts.maintainer_list, + maintainer_index, + accounts.maintainer, + )?; lido.check_reserve_account(program_id, accounts.lido.key, accounts.reserve)?; lido.check_stake_authority(program_id, accounts.lido.key, accounts.stake_authority)?; lido.check_can_stake_amount(accounts.reserve, amount)?; - let validator = lido.validators.get(accounts.validator_vote_account.key)?; - - if !validator.entry.active { - msg!( - "Validator {} is inactive, new deposits are not allowed", - validator.pubkey - ); - return Err(LidoError::StakeToInactiveValidator.into()); - } + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; // Confirm that there is no other active validator with a lower balance that // we could stake to. This alone is not sufficient to guarantee a uniform // stake balance, but it limits the power that maintainers have to disturb // the balance. More importantly, it ensures that when two maintainers create // the same StakeDeposit transaction, only one of them succeeds. - let minimum_stake_validator = lido - .validators - .iter_active_entries() - .min_by_key(|pair| pair.entry.effective_stake_balance()) + let minimum_stake_validator = validators + .iter() + .filter(|&v| v.active) + .min_by_key(|v| v.effective_stake_balance) .ok_or(LidoError::NoActiveValidators)?; + let minimum_stake_pubkey = *minimum_stake_validator.pubkey(); + let minimum_stake_balance = minimum_stake_validator.effective_stake_balance; + + let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; + + if !validator.active { + msg!( + "Validator {} is inactive, new deposits are not allowed", + validator.pubkey() + ); + return Err(LidoError::StakeToInactiveValidator.into()); + } // Note that we compare balances, not keys, because the minimum might not be unique. - if validator.entry.effective_stake_balance() - > minimum_stake_validator.entry.effective_stake_balance() - { + if validator.effective_stake_balance > minimum_stake_balance { msg!( "Refusing to stake with {}, who has {} stake, \ because {} has less stake: {}. Stake there instead.", - validator.pubkey, - validator.entry.effective_stake_balance(), - minimum_stake_validator.pubkey, - minimum_stake_validator.entry.effective_stake_balance(), + validator.pubkey(), + validator.effective_stake_balance, + minimum_stake_pubkey, + minimum_stake_balance, ); return Err(LidoError::ValidatorWithLessStakeExists.into()); } - // From now on we will not reference other Lido fields, so we can get the - // validator as mutable. This is a bit wasteful, but we can optimize when we - // need dozens of validators, for now we are under the compute limit. - let validator = lido - .validators - .get_mut(accounts.validator_vote_account.key)?; + let clock = Clock::get()?; + + // Now we have two options: + // + // 1. This was the first time we stake in this epoch, so we cannot merge the + // new account into anything. We need to delegate it, and "consume" the + // new stake account at this seed. + // + // 2. There already exists an activating stake account for the validator, + // and we can merge into it. The number of stake accounts does not change. + // + // We assume that the maintainer checked this, and we are in case 2 if the + // accounts passed differ, and in case 1 if they don't. Note, if the + // maintainer incorrectly opted for merge, the transaction will fail. If the + // maintainer incorrectly opted for append, we will consume one stake account + // that could have been avoided, but it can still be merged after activation. + let (approach, stake_account_end_authority) = + if accounts.stake_account_end.key == accounts.stake_account_merge_into.key { + (StakeDeposit::Append, VALIDATOR_STAKE_ACCOUNT.to_vec()) + } else { + ( + StakeDeposit::Merge, + // stake_account_end should be destroyed after a transaction, but a malicious + // maintainer could append an instruction to the transaction that + // transfers some SOL to this account and changes stake/withdraw authority thus making + // it a permanent account. This will make stake deposit fail. We create a temporary + // stake account tied to the current epoch so that stake account reviving could + // affect only the current epoch. And stake deposit should work in the next epoch after + // we remove the maintainer from Solido + [VALIDATOR_STAKE_ACCOUNT, &clock.epoch.to_le_bytes()[..]].concat(), + ) + }; let stake_account_bump_seed = Lido::check_stake_account( program_id, accounts.lido.key, validator, - validator.entry.stake_seeds.end, + validator.stake_seeds.end, accounts.stake_account_end, - VALIDATOR_STAKE_ACCOUNT, + &stake_account_end_authority, )?; if accounts.stake_account_end.data.borrow().len() > 0 { @@ -260,12 +325,12 @@ pub fn process_stake_deposit( return Err(LidoError::WrongStakeState.into()); } - let stake_account_seed = validator.entry.stake_seeds.end.to_le_bytes(); + let stake_account_seed = validator.stake_seeds.end.to_le_bytes(); let stake_account_bump_seed = [stake_account_bump_seed]; let stake_account_seeds = &[ accounts.lido.key.as_ref(), - validator.pubkey.as_ref(), - VALIDATOR_STAKE_ACCOUNT, + validator.vote_account_address.as_ref(), + &stake_account_end_authority, &stake_account_seed[..], &stake_account_bump_seed[..], ][..]; @@ -300,103 +365,93 @@ pub fn process_stake_deposit( // record that here; we will discover it later in `WithdrawInactiveStake`, // and then it will be treated as a donation. msg!("Staked {} out of the reserve.", amount); - validator.entry.stake_accounts_balance = (validator.entry.stake_accounts_balance + amount)?; + validator.stake_accounts_balance = (validator.stake_accounts_balance + amount)?; + validator.effective_stake_balance = validator.compute_effective_stake_balance(); - // Now we have two options: - // - // 1. This was the first time we stake in this epoch, so we cannot merge the - // new account into anything. We need to delegate it, and "consume" the - // new stake account at this seed. - // - // 2. There already exists an activating stake account for the validator, - // and we can merge into it. The number of stake accounts does not change. - // - // We assume that the maintainer checked this, and we are in case 2 if the - // accounts passed differ, and in case 1 if they don't. Note, if the - // maintainer incorrectly opted for merge, the transaction will fail. If the - // maintainer incorrectly opted for append, we will consume one stake account - // that could have been avoided, but it can still be merged after activation. - if accounts.stake_account_end.key == accounts.stake_account_merge_into.key { + match approach { // Case 1: we delegate, and we don't touch `stake_account_merge_into`. - msg!( - "Delegating stake account at seed {} ...", - validator.entry.stake_seeds.end - ); - invoke_signed( - &stake_program::instruction::delegate_stake( + StakeDeposit::Append => { + msg!( + "Delegating stake account at seed {} ...", + validator.stake_seeds.end + ); + invoke_signed( + &stake_program::instruction::delegate_stake( + accounts.stake_account_end.key, + accounts.stake_authority.key, + accounts.validator_vote_account.key, + ), + &[ + accounts.stake_account_end.clone(), + accounts.validator_vote_account.clone(), + accounts.sysvar_clock.clone(), + accounts.stake_history.clone(), + accounts.stake_program_config.clone(), + accounts.stake_authority.clone(), + accounts.stake_program.clone(), + ], + &[&[ + accounts.lido.key.as_ref(), + STAKE_AUTHORITY, + &[lido.stake_authority_bump_seed], + ]], + )?; + + // We now consumed this stake account, bump the index. + validator.stake_seeds.end += 1; + } + StakeDeposit::Merge => { + // Case 2: Merge the new undelegated stake account into the existing one. + if validator.stake_seeds.end <= validator.stake_seeds.begin { + msg!("Can only stake-merge if there is at least one stake account to merge into."); + return Err(LidoError::InvalidStakeAccount.into()); + } + Lido::check_stake_account( + program_id, + accounts.lido.key, + validator, + // Does not underflow, because end > begin >= 0. + validator.stake_seeds.end - 1, + accounts.stake_account_merge_into, + VALIDATOR_STAKE_ACCOUNT, + )?; + // The stake program checks that the two accounts can be merged; if we + // tried to merge, but the epoch is different, then this will fail. + msg!( + "Merging into existing stake account at seed {} ...", + validator.stake_seeds.end - 1 + ); + let merge_instructions = stake_program::instruction::merge( + accounts.stake_account_merge_into.key, accounts.stake_account_end.key, accounts.stake_authority.key, - accounts.validator_vote_account.key, - ), - &[ - accounts.stake_account_end.clone(), - accounts.validator_vote_account.clone(), - accounts.sysvar_clock.clone(), - accounts.stake_history.clone(), - accounts.stake_program_config.clone(), - accounts.stake_authority.clone(), - accounts.stake_program.clone(), - ], - &[&[ - accounts.lido.key.as_ref(), - STAKE_AUTHORITY, - &[lido.stake_authority_bump_seed], - ]], - )?; - - // We now consumed this stake account, bump the index. - validator.entry.stake_seeds.end += 1; - } else { - // Case 2: Merge the new undelegated stake account into the existing one. - if validator.entry.stake_seeds.end <= validator.entry.stake_seeds.begin { - msg!("Can only stake-merge if there is at least one stake account to merge into."); - return Err(LidoError::InvalidStakeAccount.into()); + ); + // For some reason, `merge` returns a `Vec` of instructions, but when + // you look at the implementation, it unconditionally returns a single + // instruction. + assert_eq!(merge_instructions.len(), 1); + let merge_instruction = &merge_instructions[0]; + + invoke_signed( + merge_instruction, + &[ + accounts.stake_account_merge_into.clone(), + accounts.stake_account_end.clone(), + accounts.sysvar_clock.clone(), + accounts.stake_history.clone(), + accounts.stake_authority.clone(), + accounts.stake_program.clone(), + ], + &[&[ + accounts.lido.key.as_ref(), + STAKE_AUTHORITY, + &[lido.stake_authority_bump_seed], + ]], + )?; } - Lido::check_stake_account( - program_id, - accounts.lido.key, - validator, - // Does not underflow, because end > begin >= 0. - validator.entry.stake_seeds.end - 1, - accounts.stake_account_merge_into, - VALIDATOR_STAKE_ACCOUNT, - )?; - // The stake program checks that the two accounts can be merged; if we - // tried to merge, but the epoch is different, then this will fail. - msg!( - "Merging into existing stake account at seed {} ...", - validator.entry.stake_seeds.end - 1 - ); - let merge_instructions = stake_program::instruction::merge( - accounts.stake_account_merge_into.key, - accounts.stake_account_end.key, - accounts.stake_authority.key, - ); - // For some reason, `merge` returns a `Vec` of instructions, but when - // you look at the implementation, it unconditionally returns a single - // instruction. - assert_eq!(merge_instructions.len(), 1); - let merge_instruction = &merge_instructions[0]; - - invoke_signed( - merge_instruction, - &[ - accounts.stake_account_merge_into.clone(), - accounts.stake_account_end.clone(), - accounts.sysvar_clock.clone(), - accounts.stake_history.clone(), - accounts.stake_authority.clone(), - accounts.stake_program.clone(), - ], - &[&[ - accounts.lido.key.as_ref(), - STAKE_AUTHORITY, - &[lido.stake_authority_bump_seed], - ]], - )?; - } + }; - lido.save(accounts.lido) + Ok(()) } /// Unstakes from a validator, the funds are moved to the stake defined by the @@ -404,24 +459,37 @@ pub fn process_stake_deposit( pub fn process_unstake( program_id: &Pubkey, amount: Lamports, + validator_index: u32, + maintainer_index: u32, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = UnstakeAccountsInfo::try_from_slice(raw_accounts)?; - let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; - lido.check_maintainer(accounts.maintainer)?; + let accounts = UnstakeAccountsInfoV2::try_from_slice(raw_accounts)?; + let lido = Lido::deserialize_lido(program_id, accounts.lido)?; + lido.check_maintainer( + program_id, + accounts.maintainer_list, + maintainer_index, + accounts.maintainer, + )?; + lido.check_stake_authority(program_id, accounts.lido.key, accounts.stake_authority)?; - let destination_bump_seed = check_unstake_accounts(program_id, &lido, &accounts)?; - let validator = lido.validators.get(accounts.validator_vote_account.key)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; + let destination_bump_seed = check_unstake_accounts(program_id, validator, &accounts)?; // Because `WithdrawInactiveStake` needs to reference all stake and unstake // accounts in a single transaction, we shouldn't have too many of them. // We should only need to do one unstake per epoch, right at the end, and in // the next epoch it should be fully inactive, we withdraw it and bump the // seed, and then we can unstake again. - if validator.entry.unstake_seeds.end - validator.entry.unstake_seeds.begin - >= MAXIMUM_UNSTAKE_ACCOUNTS - { + if validator.unstake_seeds.end - validator.unstake_seeds.begin >= MAXIMUM_UNSTAKE_ACCOUNTS { msg!("This validator already has 3 unstake accounts."); msg!("Please wait until the next epoch and withdraw them, then try to unstake again."); return Err(LidoError::MaxUnstakeAccountsReached.into()); @@ -431,7 +499,7 @@ pub fn process_unstake( &accounts.lido.key.to_bytes(), &accounts.validator_vote_account.key.to_bytes(), VALIDATOR_UNSTAKE_ACCOUNT, - &validator.entry.unstake_seeds.end.to_le_bytes()[..], + &validator.unstake_seeds.end.to_le_bytes()[..], &[destination_bump_seed], ]; @@ -474,11 +542,7 @@ pub fn process_unstake( ]], )?; - let validator = lido - .validators - .get_mut(accounts.validator_vote_account.key)?; - - if validator.entry.active { + if validator.active { // For active validators, we don't allow their stake accounts to contain // less than the minimum stake account balance. let new_source_balance = (source_balance - amount)?; @@ -505,20 +569,21 @@ pub fn process_unstake( ); return Err(LidoError::InvalidAmount.into()); } - validator.entry.stake_seeds.begin += 1; + validator.stake_seeds.begin += 1; } - validator.entry.unstake_accounts_balance = (validator.entry.unstake_accounts_balance + amount)?; - validator.entry.unstake_seeds.end += 1; + validator.unstake_accounts_balance = (validator.unstake_accounts_balance + amount)?; + validator.effective_stake_balance = validator.compute_effective_stake_balance(); + validator.unstake_seeds.end += 1; - lido.save(accounts.lido) + Ok(()) } pub fn process_update_exchange_rate( program_id: &Pubkey, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = UpdateExchangeRateAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = UpdateExchangeRateAccountsInfoV2::try_from_slice(raw_accounts)?; let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; lido.check_reserve_account(program_id, accounts.lido.key, accounts.reserve)?; @@ -534,8 +599,16 @@ pub fn process_update_exchange_rate( return Err(LidoError::ExchangeRateAlreadyUpToDate.into()); } + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + lido.exchange_rate.computed_in_epoch = clock.epoch; - lido.exchange_rate.sol_balance = lido.get_sol_balance(&rent, accounts.reserve)?; + lido.exchange_rate.sol_balance = + Lido::get_sol_balance(validators.iter(), &rent, accounts.reserve)?; lido.exchange_rate.st_sol_supply = lido.get_st_sol_supply(accounts.st_sol_mint)?; lido.save(accounts.lido) @@ -604,6 +677,7 @@ pub fn withdraw_inactive_sol( Ok(()) } +/* pub fn get_stake_account( withdraw_excess_opts: &WithdrawExcessOpts, ) -> Result { @@ -616,6 +690,7 @@ pub fn get_stake_account( withdraw_excess_opts.stake_account_seed, )) } + */ /// Checks that the `derived_stake_account_address` corresponds to the /// `provided_stake_account`. Returns the number of Lamports in the stake @@ -653,8 +728,11 @@ pub fn check_address_and_get_balance( /// This function is permissionless and can be called by anyone. pub fn process_update_stake_account_balance( program_id: &Pubkey, + validator_index: u32, raw_accounts: &[AccountInfo], ) -> ProgramResult { + Ok(()) + /* let accounts = UpdateStakeAccountBalanceInfo::try_from_slice(raw_accounts)?; let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; let stake_history = StakeHistory::from_account_info(accounts.sysvar_stake_history)?; @@ -665,15 +743,19 @@ pub fn process_update_stake_account_balance( // and confirm that they can receive stSOL. lido.check_reserve_account(program_id, accounts.lido.key, accounts.reserve)?; - let validator = lido - .validators - .get_mut(accounts.validator_vote_account.key)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; + + let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; let mut stake_observed_total = Lamports(0); let mut excess_removed = Lamports(0); - let n_stake_accounts = validator.entry.stake_seeds.end - validator.entry.stake_seeds.begin; - let n_unstake_accounts = - validator.entry.unstake_seeds.end - validator.entry.unstake_seeds.begin; + let n_stake_accounts = validator.stake_seeds.end - validator.stake_seeds.begin; + let n_unstake_accounts = validator.unstake_seeds.end - validator.unstake_seeds.begin; if accounts.stake_accounts.len() as u64 != n_stake_accounts + n_unstake_accounts { msg!("Wrong number of stake accounts provided, expected {} stake accounts and {} unstake accounts, \ @@ -685,11 +767,8 @@ pub fn process_update_stake_account_balance( accounts.stake_accounts.split_at(n_stake_accounts as usize); // Visit the stake accounts one by one, and check how much SOL is in there. - for (seed, provided_stake_account) in validator - .entry - .stake_seeds - .into_iter() - .zip(stake_accounts.iter()) + for (seed, provided_stake_account) in + validator.stake_seeds.into_iter().zip(stake_accounts.iter()) { let (stake_account_address, _bump_seed) = validator.find_stake_account_address( program_id, @@ -728,25 +807,14 @@ pub fn process_update_stake_account_balance( // balance. Validator::observe_balance( stake_observed_total, - validator.entry.effective_stake_balance(), + validator.effective_stake_balance, "Stake", )?; - // We tracked in `stake_accounts_balance` what we put in there ourselves, so - // the excess is a sum of a donation by some joker and staking rewards. - let donation = (stake_observed_total - validator.entry.effective_stake_balance()) - .expect("Does not underflow because observed_total >= stake_accounts_balance."); - msg!("{} in donations observed.", donation); - // Try to withdraw from unstake accounts. let mut unstake_removed = Lamports(0); let mut unstake_observed_total = Lamports(0); - for (seed, unstake_account) in validator - .entry - .unstake_seeds - .into_iter() - .zip(unstake_accounts) - { + for (seed, unstake_account) in validator.unstake_seeds.into_iter().zip(unstake_accounts) { let (unstake_account_address, _bump_seed) = validator.find_stake_account_address( program_id, accounts.lido.key, @@ -782,11 +850,11 @@ pub fn process_update_stake_account_balance( // account is 100% inactive. This means we would miss out on some // rewards, but in practice deactivation happens in a single epoch, and // this is not a concern. - if validator.entry.unstake_seeds.begin == seed + if validator.unstake_seeds.begin == seed && stake_account.balance.inactive == stake_account.balance.total() { withdraw_inactive_sol(&withdraw_opts, stake_account.balance.inactive)?; - validator.entry.unstake_seeds.begin += 1; + validator.unstake_seeds.begin += 1; unstake_removed = (unstake_removed + stake_account.balance.inactive)?; } unstake_observed_total = (unstake_observed_total + account_balance)?; @@ -794,31 +862,35 @@ pub fn process_update_stake_account_balance( Validator::observe_balance( unstake_observed_total, - validator.entry.unstake_accounts_balance, + validator.unstake_accounts_balance, "Unstake", )?; // we track stake_accounts_balance, so only rewards and // donations (which we consider rewards) can make a difference let stake_total_with_rewards = (stake_observed_total + unstake_observed_total)?; - let rewards = (stake_total_with_rewards - validator.entry.stake_accounts_balance) + let rewards = (stake_total_with_rewards - validator.stake_accounts_balance) .expect("Does not underflow, because tracked balance <= total."); + msg!("received rewards and donations {}.", rewards); // Store the new total. If we withdrew any inactive stake back to the // reserve, that is now no longer part of the stake accounts, so subtract // that + the total unstake removed. - validator.entry.unstake_accounts_balance = (unstake_observed_total - unstake_removed) + validator.unstake_accounts_balance = (unstake_observed_total - unstake_removed) .expect("Does not underflow, because excess <= total."); - validator.entry.stake_accounts_balance = stake_observed_total + validator.stake_accounts_balance = stake_observed_total .sub(excess_removed) .expect("Does not underflow, because excess <= total.") - .add(validator.entry.unstake_accounts_balance) + .add(validator.unstake_accounts_balance) .expect("If Solido has enough SOL to make this overflow, something has gone very wrong."); + validator.effective_stake_balance = validator.compute_effective_stake_balance(); + distribute_fees(&mut lido, &accounts, &clock, rewards)?; lido.save(accounts.lido) + */ } /// Splits a stake account from a validator's stake account. @@ -827,40 +899,51 @@ pub fn process_update_stake_account_balance( pub fn process_withdraw( program_id: &Pubkey, amount: StLamports, + validator_index: u32, raw_accounts: &[AccountInfo], ) -> ProgramResult { - let accounts = WithdrawAccountsInfo::try_from_slice(raw_accounts)?; + let accounts = WithdrawAccountsInfoV2::try_from_slice(raw_accounts)?; let mut lido = Lido::deserialize_lido(program_id, accounts.lido)?; let clock = Clock::get()?; lido.check_exchange_rate_last_epoch(&clock, "Withdraw")?; - // We should withdraw from the validator that has the most effective stake. - // With effective here we mean "total in stake accounts" - "total in unstake - // accounts", regardless of whether the stake in those accounts is active or not. - let validator = lido.validators.get(accounts.validator_vote_account.key)?; + let validator_list_data = &mut *accounts.validator_list.data.borrow_mut(); + let mut validators = lido.deserialize_account_list_info::( + program_id, + accounts.validator_list, + validator_list_data, + )?; // Confirm that there is no other validator with a higher balance that // we could withdraw from. This alone is not sufficient to guarantee a uniform // stake balance, but prevents things from becoming more unbalanced than // necessary. - let maximum_stake_validator = lido - .validators - .entries + let maximum_stake_validator = validators .iter() - .max_by_key(|pair| pair.entry.effective_stake_balance()) + .max_by_key(|pair| pair.effective_stake_balance) .ok_or(LidoError::NoActiveValidators)?; + let maximum_stake_pubkey = *maximum_stake_validator.pubkey(); + let maximum_stake_balance = maximum_stake_validator.effective_stake_balance; + + // We should withdraw from the validator that has the most effective stake. + // With effective here we mean "total in stake accounts" - "total in unstake + // accounts", regardless of whether the stake in those accounts is active or not. + let validator = validators.get_mut(validator_index, accounts.validator_vote_account.key)?; + + if validator.effective_stake_balance == Lamports(0) { + msg!("Validator {} has no stake", validator.pubkey()); + return Err(LidoError::ValidatorHasNoStake.into()); + } // Note that we compare balances, not keys, because the maximum might not be unique. - if validator.entry.effective_stake_balance() - < maximum_stake_validator.entry.effective_stake_balance() - { + if validator.effective_stake_balance < maximum_stake_balance { msg!( "Refusing to withdraw from {}, who has {} stake, \ because {} has more stake: {}. Withdraw from there instead.", - validator.pubkey, - validator.entry.effective_stake_balance(), - maximum_stake_validator.pubkey, - maximum_stake_validator.entry.effective_stake_balance(), + validator.pubkey(), + validator.effective_stake_balance, + maximum_stake_pubkey, + maximum_stake_balance, ); return Err(LidoError::ValidatorWithMoreStakeExists.into()); } @@ -868,7 +951,7 @@ pub fn process_withdraw( let (stake_account, _) = validator.find_stake_account_address( program_id, accounts.lido.key, - validator.entry.stake_seeds.begin, + validator.stake_seeds.begin, StakeType::Stake, ); if &stake_account != accounts.source_stake_account.key { @@ -885,9 +968,6 @@ pub fn process_withdraw( return Err(err.into()); } }; - let provided_validator = lido - .validators - .get_mut(accounts.validator_vote_account.key)?; let source_balance = Lamports(accounts.source_stake_account.lamports()); @@ -924,8 +1004,8 @@ pub fn process_withdraw( return Err(LidoError::InvalidAmount.into()); } - provided_validator.entry.stake_accounts_balance = - (provided_validator.entry.stake_accounts_balance - sol_to_withdraw)?; + validator.stake_accounts_balance = (validator.stake_accounts_balance - sol_to_withdraw)?; + validator.effective_stake_balance = validator.compute_effective_stake_balance(); // Burn stSol tokens burn_st_sol(&lido, &accounts, amount)?; @@ -958,6 +1038,105 @@ pub fn process_withdraw( lido.save(accounts.lido) } +/// Migrate Solido state to version 2 +pub fn processor_migrate_to_v2( + program_id: &Pubkey, + reward_distribution: RewardDistribution, + max_validators: u32, + max_maintainers: u32, + max_commission_percentage: u8, + accounts_raw: &[AccountInfo], +) -> ProgramResult { + let accounts = MigrateStateToV2Info::try_from_slice(accounts_raw)?; + let lido_v1 = LidoV1::deserialize_lido(program_id, accounts.lido)?; + lido_v1.check_manager(accounts.manager)?; + + if !(lido_v1.lido_version == 0 && Lido::VERSION == 1) { + return Err(LidoError::LidoVersionMismatch.into()); + } + + let rent = &Rent::get()?; + check_rent_exempt(rent, accounts.validator_list, "Validator list account")?; + check_rent_exempt(rent, accounts.maintainer_list, "Maintainer list account")?; + + check_account_owner(accounts.validator_list, program_id)?; + check_account_owner(accounts.maintainer_list, program_id)?; + + check_account_data( + accounts.validator_list, + ValidatorList::required_bytes(max_validators), + AccountType::Validator, + )?; + check_account_data( + accounts.maintainer_list, + MaintainerList::required_bytes(max_maintainers), + AccountType::Maintainer, + )?; + + if accounts.validator_list.key == accounts.maintainer_list.key { + msg!("Cannot use same account for validator list and maintainer list"); + return Err(LidoError::AlreadyInUse.into()); + } + + if lido_v1.maintainers.entries.len() > max_maintainers as usize { + msg!("max_maintainers is too small"); + return Err(LidoError::MaximumNumberOfAccountsExceeded.into()); + } + let mut maintainers = MaintainerList::new_default(0); + maintainers.header.max_entries = max_maintainers; + + for pe in lido_v1.maintainers.entries { + maintainers.entries.push(Maintainer::new(pe.pubkey)); + } + + if lido_v1.validators.entries.len() > max_validators as usize { + msg!("max_validators is too small"); + return Err(LidoError::MaximumNumberOfAccountsExceeded.into()); + } + let mut validators = ValidatorList::new_default(0); + validators.header.max_entries = max_validators; + + if !lido_v1.validators.entries.is_empty() { + msg!("There should be no validators in Solido state prior to update."); + msg!("You should first deactivate all validators and wait for epoch boundary."); + msg!("Then maintainers will withdraw inactive stake to reserve and remove validators."); + return Err(LidoError::ValidatorListNotEmpty.into()); + } + + if max_commission_percentage > 100 { + return Err(LidoError::ValidationCommissionOutOfBounds.into()); + } + + // Initialize Lido structure + let lido = Lido { + lido_version: Lido::VERSION, + account_type: AccountType::Lido, + validator_list: *accounts.validator_list.key, + maintainer_list: *accounts.maintainer_list.key, + max_commission_percentage, + fee_recipients: FeeRecipients { + treasury_account: lido_v1.fee_recipients.treasury_account, + developer_account: *accounts.developer_account.key, + }, + reward_distribution, + + manager: lido_v1.manager, + st_sol_mint: lido_v1.st_sol_mint, + exchange_rate: lido_v1.exchange_rate, + sol_reserve_account_bump_seed: lido_v1.sol_reserve_account_bump_seed, + mint_authority_bump_seed: lido_v1.mint_authority_bump_seed, + stake_authority_bump_seed: lido_v1.stake_authority_bump_seed, + metrics: lido_v1.metrics, + }; + + // Confirm that the fee recipients are actually stSOL accounts. + lido.check_is_st_sol_account(accounts.developer_account)?; + + validators.save(accounts.validator_list)?; + maintainers.save(accounts.maintainer_list)?; + lido.save(accounts.lido) +} + /// Processes [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = LidoInstruction::try_from_slice(input)?; @@ -968,7 +1147,6 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P max_maintainers, max_commission_percentage, } => process_initialize( - LIDO_VERSION, program_id, reward_distribution, max_validators, @@ -977,36 +1155,91 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P accounts, ), LidoInstruction::Deposit { amount } => process_deposit(program_id, amount, accounts), - LidoInstruction::StakeDeposit { amount } => { - process_stake_deposit(program_id, amount, accounts) - } - LidoInstruction::Unstake { amount } => process_unstake(program_id, amount, accounts), - LidoInstruction::UpdateExchangeRate => process_update_exchange_rate(program_id, accounts), - LidoInstruction::WithdrawInactiveStake - | LidoInstruction::CollectValidatorFee - | LidoInstruction::ClaimValidatorFee - | LidoInstruction::AddValidator => { - msg!("{:?} is no longer supported since v2. Please check the changelog in the repository for update instructions.", instruction); - Err(LidoError::InstructionIsDeprecated.into()) - } - LidoInstruction::UpdateStakeAccountBalance => { - process_update_stake_account_balance(program_id, accounts) + LidoInstruction::StakeDepositV2 { + amount, + validator_index, + maintainer_index, + } => process_stake_deposit( + program_id, + amount, + validator_index, + maintainer_index, + accounts, + ), + LidoInstruction::UnstakeV2 { + amount, + validator_index, + maintainer_index, + } => process_unstake( + program_id, + amount, + validator_index, + maintainer_index, + accounts, + ), + LidoInstruction::UpdateExchangeRateV2 => process_update_exchange_rate(program_id, accounts), + LidoInstruction::UpdateStakeAccountBalance { validator_index } => { + process_update_stake_account_balance(program_id, validator_index, accounts) } - LidoInstruction::Withdraw { amount } => process_withdraw(program_id, amount, accounts), + LidoInstruction::WithdrawV2 { + amount, + validator_index, + } => process_withdraw(program_id, amount, validator_index, accounts), LidoInstruction::ChangeRewardDistribution { new_reward_distribution, } => process_change_reward_distribution(program_id, new_reward_distribution, accounts), LidoInstruction::AddValidatorV2 => process_add_validator(program_id, accounts), - LidoInstruction::RemoveValidator => process_remove_validator(program_id, accounts), - LidoInstruction::DeactivateValidator => process_deactivate_validator(program_id, accounts), - LidoInstruction::AddMaintainer => process_add_maintainer(program_id, accounts), - LidoInstruction::RemoveMaintainer => process_remove_maintainer(program_id, accounts), - LidoInstruction::MergeStake => process_merge_stake(program_id, accounts), - LidoInstruction::DeactivateValidatorIfCommissionExceedsMax => { - process_deactivate_validator_if_commission_exceeds_max(program_id, accounts) + LidoInstruction::RemoveValidatorV2 { validator_index } => { + process_remove_validator(program_id, validator_index, accounts) + } + LidoInstruction::DeactivateValidatorV2 { validator_index } => { + process_deactivate_validator(program_id, validator_index, accounts) + } + LidoInstruction::AddMaintainerV2 => process_add_maintainer(program_id, accounts), + LidoInstruction::RemoveMaintainerV2 { maintainer_index } => { + process_remove_maintainer(program_id, maintainer_index, accounts) + } + LidoInstruction::MergeStakeV2 { validator_index } => { + process_merge_stake(program_id, validator_index, accounts) + } + LidoInstruction::DeactivateValidatorIfCommissionExceedsMax { validator_index } => { + process_deactivate_validator_if_commission_exceeds_max( + program_id, + validator_index, + accounts, + ) } LidoInstruction::SetMaxValidationCommission { max_commission_percentage, } => process_set_max_commission_percentage(program_id, max_commission_percentage, accounts), + LidoInstruction::MigrateStateToV2 { + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + } => processor_migrate_to_v2( + program_id, + reward_distribution, + max_validators, + max_maintainers, + max_commission_percentage, + accounts, + ), + LidoInstruction::WithdrawInactiveStake + | LidoInstruction::CollectValidatorFee + | LidoInstruction::ClaimValidatorFee + | LidoInstruction::UpdateExchangeRate + | LidoInstruction::MergeStake + | LidoInstruction::AddValidator + | LidoInstruction::AddMaintainer + | LidoInstruction::DeactivateValidator + | LidoInstruction::RemoveMaintainer + | LidoInstruction::RemoveValidator + | LidoInstruction::StakeDeposit { .. } + | LidoInstruction::Unstake { .. } + | LidoInstruction::Withdraw { .. } => { + msg!("{:?} is no longer supported since v2. Please check the changelog in the repository for update instructions.", instruction); + Err(LidoError::InstructionIsDeprecated.into()) + } } } diff --git a/program/src/stake_account.rs b/program/src/stake_account.rs index 880daac19..fcb32b6ef 100644 --- a/program/src/stake_account.rs +++ b/program/src/stake_account.rs @@ -66,7 +66,7 @@ impl StakeBalance { fn take_pubkey(data: &[u8]) -> (Pubkey, &[u8]) { let mut prefix = [0u8; 32]; prefix.copy_from_slice(&data[..32]); - (Pubkey::new(&prefix), &data[32..]) + (Pubkey::new_from_array(prefix), &data[32..]) } /// Consume a little-endian `u32` from the data start, return it and the remainder. @@ -184,6 +184,7 @@ impl StakeAccount { ) } /// Extract the stake balance from a delegated stake account. + /* pub fn from_delegated_account( account_lamports: Lamports, stake: &Stake, @@ -194,9 +195,10 @@ impl StakeAccount { let target_epoch = clock.epoch; let history = Some(stake_history); - let mut state = stake - .delegation - .stake_activating_and_deactivating(target_epoch, history); + let mut state = + stake + .delegation + .stake_activating_and_deactivating(target_epoch, history, None); // `stake_activating_and_deactivating` counts deactivating stake both as // part of the active lamports, and as part of the deactivating @@ -225,6 +227,7 @@ impl StakeAccount { seed, } } + */ /// Returns `true` if the stake account is active, `false` otherwise. pub fn is_active(&self) -> bool { @@ -267,7 +270,10 @@ impl StakeAccount { return true; } // Two activating accounts that share an activation epoch, during the activation epoch. - if self.is_activating() && merge_from.is_activating() { + if self.is_activating() + && merge_from.is_activating() + && self.activation_epoch == merge_from.activation_epoch + { return true; } } diff --git a/program/src/state.rs b/program/src/state.rs index bbdd11792..efd572b45 100644 --- a/program/src/state.rs +++ b/program/src/state.rs @@ -3,58 +3,552 @@ //! State transition types +use std::convert::TryFrom; +use std::fmt::Debug; +use std::marker::PhantomData; use std::ops::Range; use serde::Serialize; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; -use solana_program::borsh::{get_instance_packed_len, try_from_slice_unchecked}; -use solana_program::clock::Clock; use solana_program::{ - account_info::AccountInfo, clock::Epoch, entrypoint::ProgramResult, msg, - program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar, + account_info::AccountInfo, + borsh1::{get_instance_packed_len, try_from_slice_unchecked}, + clock::Clock, + clock::Epoch, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_memory::sol_memcmp, + program_pack::Pack, + program_pack::Sealed, + pubkey::{Pubkey, PUBKEY_BYTES}, + rent::Rent, + sysvar::Sysvar, }; use spl_token::state::Mint; +use crate::big_vec::BigVec; use crate::error::LidoError; -use crate::logic::get_reserve_available_balance; +use crate::logic::{check_account_owner, get_reserve_available_balance}; use crate::metrics::Metrics; use crate::processor::StakeType; use crate::token::{self, Lamports, Rational, StLamports}; use crate::util::serialize_b58; use crate::{ - account_map::{AccountMap, AccountSet, EntryConstantSize, PubkeyAndEntry}, MINIMUM_STAKE_ACCOUNT_BALANCE, MINT_AUTHORITY, RESERVE_ACCOUNT, STAKE_AUTHORITY, + VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT, }; -use crate::{VALIDATOR_STAKE_ACCOUNT, VALIDATOR_UNSTAKE_ACCOUNT}; -pub const LIDO_VERSION: u8 = 1; +/// Types of list entries +/// Uninitialized should always be a first enum field as it catches empty list data errors +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, BorshSchema)] +pub enum AccountType { + /// If the account has not been initialized, the enum will be 0 + Uninitialized, + Lido, + Validator, + Maintainer, +} -/// Size of a serialized `Lido` struct excluding validators and maintainers. -/// -/// To update this, run the tests and replace the value here with the test output. -pub const LIDO_CONSTANT_SIZE: usize = 353; -pub const VALIDATOR_CONSTANT_SIZE: usize = 49; +impl Default for AccountType { + fn default() -> Self { + AccountType::Uninitialized + } +} + +/// Storage list for accounts in the pool. +/// It is used to serialize account list on stake pool initialization +#[repr(C)] +#[derive( + Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub struct AccountList { + /// Data outside of the list, separated out for cheaper deserializations. + /// Must be a first field to avoid account confusion. + #[serde(skip_serializing)] + pub header: ListHeader, + + /// List of account in the pool + pub entries: Vec, +} + +pub type ValidatorList = AccountList; +pub type MaintainerList = AccountList; + +/// Helper type to deserialize just the start of AccountList +#[repr(C)] +#[derive( + Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub struct ListHeader { + /// Account type, must be a first field to avoid account confusion + pub account_type: AccountType, + + /// Version number for the Lido + pub lido_version: u8, + + /// Maximum allowable number of elements + pub max_entries: u32, + + phantom: PhantomData, +} + +/// Generic element of a list +pub trait ListEntry: Pack + Default + Clone + BorshSerialize + PartialEq + Debug { + const TYPE: AccountType; + + fn new(pubkey: Pubkey) -> Self; + fn pubkey(&self) -> &Pubkey; + + /// Performs a very cheap comparison, for checking if entry + /// info matches the account address. + /// First PUBKEY_BYTES of a ListEntry data should be the account address + fn memcmp_pubkey(data: &[u8], pubkey: &[u8]) -> bool { + sol_memcmp(&data[..PUBKEY_BYTES], pubkey, PUBKEY_BYTES) == 0 + } +} + +impl AccountList +where + T: Default + Clone + ListEntry + BorshSerialize, +{ + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Create an empty instance containing space for `max_entries` with default values + pub fn new_default(max_entries: u32) -> Self { + Self { + header: ListHeader:: { + account_type: T::TYPE, + max_entries, + lido_version: Lido::VERSION, + phantom: PhantomData, + }, + entries: vec![T::default(); max_entries as usize], + } + } + + /// Create a new list of accounts by copying from `data`. Do not use on-chain. + pub fn from(data: &mut [u8]) -> Result { + let (header, big_vec) = ListHeader::::deserialize_vec(data)?; + Ok(Self { + header, + entries: big_vec.iter().cloned().collect(), + }) + } + + pub fn iter(&self) -> impl Iterator { + self.entries.iter() + } + + fn header_size() -> usize { + // + 4 bytes for entries len + ListHeader::::LEN + std::mem::size_of::() + } + + /// Calculate the number of account entries that fit in the provided length + pub fn calculate_max_entries(buffer_length: usize) -> usize { + buffer_length.saturating_sub(Self::header_size()) / T::LEN + } + + /// Calculate the number of bytes required for max_entries + pub fn required_bytes(max_entries: u32) -> usize { + Self::header_size() + T::LEN * max_entries as usize + } + + /// Check if contains an account with particular pubkey + pub fn find(&self, pubkey: &Pubkey) -> Option<&T> { + self.entries.iter().find(|x| x.pubkey() == pubkey) + } + + /// Get index of list entry with pubkey. + /// Panics if list is too big, not used on-chain + pub fn position(&self, pubkey: &Pubkey) -> Option { + self.entries + .iter() + .position(|v| v.pubkey() == pubkey) + .map(u32::try_from) + .map(Result::unwrap) + } + + /// Serialize to AccountInfo data + pub fn save(&self, account: &AccountInfo) -> ProgramResult { + BorshSerialize::serialize(self, &mut *account.data.borrow_mut())?; + Ok(()) + } +} + +/// Check Lido version +pub fn check_lido_version(version: u8, account_type: AccountType) -> ProgramResult { + if version != Lido::VERSION { + msg!( + "Lido version mismatch for {:?}. Current version {}, should be {}", + account_type, + version, + Lido::VERSION + ); + return Err(LidoError::LidoVersionMismatch.into()); + } + Ok(()) +} + +/// Represents list of accounts as a view of raw bytes. +/// Main data structure to use on-chain for account lists +pub struct BigVecWithHeader<'data, T> { + pub header: ListHeader, + big_vec: BigVec<'data, T>, +} + +impl<'data, T: ListEntry> BigVecWithHeader<'data, T> { + pub fn new(header: ListHeader, big_vec: BigVec<'data, T>) -> Self { + Self { header, big_vec } + } + + pub fn len(&self) -> u32 { + self.big_vec.len() + } + + pub fn is_empty(&self) -> bool { + self.big_vec.is_empty() + } + + pub fn iter(&'data self) -> impl Iterator { + self.big_vec.iter() + } + + pub fn find(&'data self, pubkey: &Pubkey) -> Result<&'data T, LidoError> { + self.big_vec + .find(&pubkey.to_bytes(), T::memcmp_pubkey) + .ok_or(LidoError::InvalidAccountMember) + } + + /// Appends to the list only if unique + pub fn push(&mut self, value: T) -> ProgramResult { + if self.header.max_entries == self.len() { + msg!("Can't append to {:?} list as it has no free space", T::TYPE); + return Err(LidoError::MaximumNumberOfAccountsExceeded.into()); + } + + if self.find(value.pubkey()).is_ok() { + msg!( + "Pubkey {} is duplicated in a {:?} list", + value.pubkey(), + T::TYPE + ); + return Err(LidoError::DuplicatedEntry.into()); + }; + self.big_vec.push(value) + } + + /// Check if list element pubkey matches requested pubkey + fn check_pubkey(element: &T, pubkey: &Pubkey) -> ProgramResult { + if element.pubkey() != pubkey { + msg!( + "{:?} list index does not match pubkey. Please supply a valid index or try again.", + T::TYPE + ); + return Err(LidoError::PubkeyIndexMismatch.into()); + } + Ok(()) + } + + /// Get element with pubkey at index + pub fn get_mut( + &'data mut self, + index: u32, + pubkey: &Pubkey, + ) -> Result<&'data mut T, ProgramError> { + let element = self.big_vec.get_mut(index)?; + Self::check_pubkey(element, pubkey)?; + Ok(element) + } + + /// Removes element with pubkey at index + pub fn remove(&'data mut self, index: u32, pubkey: &Pubkey) -> Result { + let element = self.big_vec.get_mut(index)?; + Self::check_pubkey(element, pubkey)?; + self.big_vec.swap_remove(index) + } +} + +impl ListHeader { + const LEN: usize = + std::mem::size_of::() + std::mem::size_of::() + std::mem::size_of::(); + + pub fn deserialize_checked(data: &[u8]) -> Result { + let mut data = data; + let header = Self::deserialize(&mut data)?; + + check_lido_version(header.lido_version, T::TYPE)?; + + // check ListEntryType + if header.account_type != T::TYPE { + msg!( + "Invalid account type when deserializing list header, found {:?}, should be {:?}", + header.account_type, + T::TYPE + ); + return Err(LidoError::InvalidAccountType.into()); + } + Ok(header) + } -pub type Validators = AccountMap; + /// Extracts the account list into its header and internal BigVec + pub fn deserialize_vec(data: &mut [u8]) -> Result<(Self, BigVec), ProgramError> { + let header = Self::deserialize_checked(data)?; + let big_vec: BigVec = + BigVec::new(&mut data[Self::LEN..AccountList::::required_bytes(header.max_entries)]); + Ok((header, big_vec)) + } +} -impl Validators { +impl ValidatorList { pub fn iter_active(&self) -> impl Iterator { - self.iter_entries().filter(|&v| v.active) + self.entries.iter().filter(|&v| v.active) } +} - pub fn iter_active_entries(&self) -> impl Iterator> { - self.entries.iter().filter(|&v| v.entry.active) +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] +pub struct Validator { + /// Validator vote account address. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + #[serde(rename = "pubkey")] + pub vote_account_address: Pubkey, + + /// Seeds for active stake accounts. + pub stake_seeds: SeedRange, + /// Seeds for inactive stake accounts. + pub unstake_seeds: SeedRange, + + /// Sum of the balances of the stake accounts and unstake accounts. + pub stake_accounts_balance: Lamports, + + /// Sum of the balances of the unstake accounts. + pub unstake_accounts_balance: Lamports, + + /// Effective stake balance is stake_accounts_balance - unstake_accounts_balance. + /// The result is stored on-chain to optimize compute budget + pub effective_stake_balance: Lamports, + + /// Controls if a validator is allowed to have new stake deposits. + /// When removing a validator, this flag should be set to `false`. + pub active: bool, +} + +/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS +/// THERE'S AN EXTREMELY GOOD REASON. +/// +/// To save on BPF instructions, the serialized bytes are reinterpreted with an +/// unsafe pointer cast, which means that this structure cannot have any +/// undeclared alignment-padding in its representation. +#[repr(C)] +#[derive( + Clone, Default, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize, +)] +pub struct Maintainer { + /// Address of maintainer account. + /// Do not reorder this field, it should be first in the struct + #[serde(serialize_with = "serialize_b58")] + pub pubkey: Pubkey, +} + +impl Validator { + /// Return the balance in only the stake accounts, excluding the unstake accounts. + pub fn compute_effective_stake_balance(&self) -> Lamports { + (self.stake_accounts_balance - self.unstake_accounts_balance) + .expect("Unstake balance cannot exceed the validator's total stake balance.") + } + + pub fn observe_balance(observed: Lamports, tracked: Lamports, info: &str) -> ProgramResult { + if observed < tracked { + msg!( + "{}: observed balance of {} is less than tracked balance of {}.", + info, + observed, + tracked + ); + msg!("This should not happen, aborting ..."); + return Err(LidoError::ValidatorBalanceDecreased.into()); + } + Ok(()) + } + + pub fn has_stake_accounts(&self) -> bool { + self.stake_seeds.begin != self.stake_seeds.end + } + pub fn has_unstake_accounts(&self) -> bool { + self.unstake_seeds.begin != self.unstake_seeds.end + } + + pub fn check_can_be_removed(&self) -> Result<(), LidoError> { + if self.active { + return Err(LidoError::ValidatorIsStillActive); + } + if self.has_stake_accounts() { + return Err(LidoError::ValidatorShouldHaveNoStakeAccounts); + } + if self.has_unstake_accounts() { + return Err(LidoError::ValidatorShouldHaveNoUnstakeAccounts); + } + // If not, this is a bug. + assert_eq!(self.stake_accounts_balance, Lamports(0)); + Ok(()) + } + + pub fn show_removed_error_msg(error: &Result<(), LidoError>) { + if let Err(err) = error { + match err { + LidoError::ValidatorIsStillActive => { + msg!( + "Refusing to remove validator because it is still active, deactivate it first." + ); + } + LidoError::ValidatorHasUnclaimedCredit => { + msg!( + "Validator still has tokens to claim. Reclaim tokens before removing the validator" + ); + } + LidoError::ValidatorShouldHaveNoStakeAccounts => { + msg!("Refusing to remove validator because it still has stake accounts, unstake them first."); + } + LidoError::ValidatorShouldHaveNoUnstakeAccounts => { + msg!("Refusing to remove validator because it still has unstake accounts, withdraw them first."); + } + _ => { + msg!("Invalid error when removing a validator: shouldn't happen."); + } + } + } + } + + pub fn find_stake_account_address_with_authority( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + authority: &[u8], + seed: u64, + ) -> (Pubkey, u8) { + let seeds = [ + &solido_account.to_bytes(), + &self.vote_account_address.to_bytes(), + authority, + &seed.to_le_bytes()[..], + ]; + Pubkey::find_program_address(&seeds, program_id) + } + + pub fn find_stake_account_address( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + seed: u64, + stake_type: StakeType, + ) -> (Pubkey, u8) { + let authority = match stake_type { + StakeType::Stake => VALIDATOR_STAKE_ACCOUNT, + StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT, + }; + self.find_stake_account_address_with_authority(program_id, solido_account, authority, seed) + } + + /// Get stake account address that should be merged into another right after creation. + /// This function should be used to create temporary stake accounts + /// tied to the epoch that should be merged into another account and destroyed + /// after a transaction. So that each epoch would have a diferent + /// generation of stake accounts. This is done for security purpose + pub fn find_temporary_stake_account_address( + &self, + program_id: &Pubkey, + solido_account: &Pubkey, + seed: u64, + epoch: Epoch, + ) -> (Pubkey, u8) { + let authority = [VALIDATOR_STAKE_ACCOUNT, &epoch.to_le_bytes()[..]].concat(); + self.find_stake_account_address_with_authority(program_id, solido_account, &authority, seed) } } -pub type Maintainers = AccountSet; -impl EntryConstantSize for Validator { - const SIZE: usize = VALIDATOR_CONSTANT_SIZE; +impl Sealed for Validator {} + +impl Pack for Validator { + const LEN: usize = 89; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } } -impl EntryConstantSize for () { - const SIZE: usize = 0; +impl Default for Validator { + fn default() -> Self { + Validator { + stake_seeds: SeedRange { begin: 0, end: 0 }, + unstake_seeds: SeedRange { begin: 0, end: 0 }, + stake_accounts_balance: Lamports(0), + unstake_accounts_balance: Lamports(0), + effective_stake_balance: Lamports(0), + active: true, + vote_account_address: Pubkey::default(), + } + } +} + +impl ListEntry for Validator { + const TYPE: AccountType = AccountType::Validator; + + fn new(vote_account_address: Pubkey) -> Self { + Self { + vote_account_address, + ..Default::default() + } + } + + fn pubkey(&self) -> &Pubkey { + &self.vote_account_address + } +} + +impl Sealed for Maintainer {} + +impl Pack for Maintainer { + const LEN: usize = PUBKEY_BYTES; + fn pack_into_slice(&self, data: &mut [u8]) { + let mut data = data; + BorshSerialize::serialize(&self, &mut data).unwrap(); + } + fn unpack_from_slice(src: &[u8]) -> Result { + let unpacked = Self::try_from_slice(src)?; + Ok(unpacked) + } +} + +impl ListEntry for Maintainer { + const TYPE: AccountType = AccountType::Maintainer; + + fn new(pubkey: Pubkey) -> Self { + Self { pubkey } + } + + fn pubkey(&self) -> &Pubkey { + &self.pubkey + } } /// The exchange rate used for deposits and rewards distribution. @@ -185,6 +679,9 @@ impl ExchangeRate { Clone, Debug, Default, BorshDeserialize, BorshSerialize, BorshSchema, Eq, PartialEq, Serialize, )] pub struct Lido { + /// Account type, must be a first field to avoid account confusion + pub account_type: AccountType, + /// Version number for the Lido pub lido_version: u8, @@ -217,48 +714,76 @@ pub struct Lido { /// these metrics. pub metrics: Metrics, - /// Map of enrolled validators, maps their vote account to `Validator` details. - pub validators: Validators, - - /// Maximum validation commission percentage in [0, 100] - pub max_commission_percentage: u8, + /// Validator list account + #[serde(serialize_with = "serialize_b58")] + pub validator_list: Pubkey, - /// The set of maintainers. + /// Maintainer list account /// /// Maintainers are granted low security risk privileges. Maintainers are /// expected to run the maintenance daemon, that invokes the maintenance /// operations. These are gated on the signer being present in this set. /// In the future we plan to make maintenance operations callable by anybody. - pub maintainers: Maintainers, + #[serde(serialize_with = "serialize_b58")] + pub maintainer_list: Pubkey, + + /// Maximum validation commission percentage in [0, 100] + pub max_commission_percentage: u8, } impl Lido { + pub const VERSION: u8 = 1; + + /// Size of a serialized `Lido` struct excluding validators and maintainers. + /// + /// To update this, run the tests and replace the value here with the test output. + pub const LEN: usize = 418; + pub fn deserialize_lido(program_id: &Pubkey, lido: &AccountInfo) -> Result { - if lido.owner != program_id { + check_account_owner(lido, program_id)?; + + let lido = try_from_slice_unchecked::(&lido.data.borrow())?; + if lido.account_type != AccountType::Lido { msg!( - "Lido state is owned by {}, but should be owned by the Lido program ({}).", - lido.owner, - program_id + "Lido account type should be {:?}, but is {:?}", + AccountType::Lido, + lido.account_type ); - return Err(LidoError::InvalidOwner.into()); + return Err(LidoError::InvalidAccountType.into()); } - let lido = try_from_slice_unchecked::(&lido.data.borrow())?; + + check_lido_version(lido.lido_version, AccountType::Lido)?; + Ok(lido) } - /// Calculates the total size of Lido given two variables: `max_validators` - /// and `max_maintainers`, the maximum number of maintainers and validators, - /// respectively. It creates default structures for both and sum its sizes - /// with Lido's constant size. - pub fn calculate_size(max_validators: u32, max_maintainers: u32) -> usize { + /// Calculates the total size of Lido + pub fn calculate_size() -> usize { let lido_instance = Lido { - validators: Validators::new_fill_default(max_validators), - maintainers: Maintainers::new_fill_default(max_maintainers), ..Default::default() }; get_instance_packed_len(&lido_instance).unwrap() } + /// Get maximum number of bytes over all Solido owned accounts, including previuos + /// versions, that should be checked to be zero to initialize Solido instance + /// + /// This is also done to avoid account confusion that could cause an old, + /// not-yet-upgraded Lido instances to be overwritten with a new initialize instruction + pub fn get_bytes_to_check() -> usize { + *[ + LidoV1::LEN, + Lido::LEN, + // it's enough to check only bytes for `a list of size 1` to be zero, + // otherwize the list won't be deserializable + ValidatorList::required_bytes(1), + MaintainerList::required_bytes(1), + ] + .iter() + .max() + .unwrap() + } + /// Confirm that the given account is Solido's stSOL mint. pub fn check_mint_is_st_sol_mint(&self, mint_account_info: &AccountInfo) -> ProgramResult { if &self.st_sol_mint != mint_account_info.key { @@ -306,24 +831,11 @@ impl Lido { Ok(()) } - /// Checks if the passed manager is the same as the one stored in the state - pub fn check_manager(&self, manager: &AccountInfo) -> ProgramResult { - if &self.manager != manager.key { - msg!("Invalid manager, not the same as the one stored in state"); - return Err(LidoError::InvalidManager.into()); - } - Ok(()) - } - - /// Checks if the passed maintainer belong to the list of maintainers - pub fn check_maintainer(&self, maintainer: &AccountInfo) -> ProgramResult { - if self.maintainers.get(maintainer.key).is_err() { - msg!( - "Invalid maintainer, account {} is not present in the maintainers list.", - maintainer.key - ); - - return Err(LidoError::InvalidMaintainer.into()); + /// Checks if the passed manager is the same as the one stored in the state + pub fn check_manager(&self, manager: &AccountInfo) -> ProgramResult { + if &self.manager != manager.key { + msg!("Invalid manager, not the same as the one stored in state"); + return Err(LidoError::InvalidManager.into()); } Ok(()) } @@ -480,7 +992,7 @@ impl Lido { pub fn check_stake_account( program_id: &Pubkey, solido_address: &Pubkey, - validator: &PubkeyAndEntry, + validator: &Validator, stake_account_seed: u64, stake_account: &AccountInfo, authority: &[u8], @@ -527,19 +1039,19 @@ impl Lido { /// The computation is based on the amount of SOL per validator that we track /// ourselves, so if there are any unobserved rewards in the stake accounts, /// these will not be included. - pub fn get_sol_balance( - &self, + pub fn get_sol_balance<'data, I>( + validators: I, rent: &Rent, reserve: &AccountInfo, - ) -> Result { + ) -> Result + where + I: Iterator, + { let effective_reserve_balance = get_reserve_available_balance(rent, reserve)?; // The remaining SOL managed is all in stake accounts. - let validator_balance: token::Result = self - .validators - .iter_entries() - .map(|v| v.stake_accounts_balance) - .sum(); + let validator_balance: token::Result = + validators.map(|v| v.stake_accounts_balance).sum(); let result = validator_balance.and_then(|s| s + effective_reserve_balance)?; @@ -575,25 +1087,79 @@ impl Lido { } Ok(()) } -} -#[repr(C)] -#[derive(Clone, Debug, Eq, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema, Serialize)] -pub struct Validator { - /// Seeds for active stake accounts. - pub stake_seeds: SeedRange, - /// Seeds for inactive stake accounts. - pub unstake_seeds: SeedRange, + /// Checks if the maintainer belongs to the list of maintainers + pub fn check_maintainer( + &self, + program_id: &Pubkey, + maintainer_list: &AccountInfo, + maintainer_index: u32, + maintainer: &AccountInfo, + ) -> ProgramResult { + let data = &mut *maintainer_list.data.borrow_mut(); + let mut maintainer_list = + self.deserialize_account_list_info::(program_id, maintainer_list, data)?; + + if maintainer_list + .get_mut(maintainer_index, maintainer.key) + .is_err() + { + msg!( + "Invalid maintainer, account {} is not present in the maintainers list.", + maintainer.key + ); - /// Sum of the balances of the stake accounts and unstake accounts. - pub stake_accounts_balance: Lamports, + return Err(LidoError::InvalidMaintainer.into()); + } - /// Sum of the balances of the unstake accounts. - pub unstake_accounts_balance: Lamports, + Ok(()) + } - /// Controls if a validator is allowed to have new stake deposits. - /// When removing a validator, this flag should be set to `false`. - pub active: bool, + /// Checks if account list belongs to Lido + pub fn check_account_list_info( + &self, + program_id: &Pubkey, + list_address: &Pubkey, + account_list_info: &AccountInfo, + ) -> ProgramResult { + check_account_owner(account_list_info, program_id)?; + + // check account_list belongs to Lido + if list_address != account_list_info.key { + msg!( + "{:?} list address {} is different from Lido's {}", + T::TYPE, + account_list_info.key, + list_address + ); + return Err(LidoError::InvalidListAccount.into()); + } + + Ok(()) + } + + /// Check account list info and deserialize the account data + pub fn deserialize_account_list_info<'data, T: ListEntry>( + &self, + program_id: &Pubkey, + account_list_info: &AccountInfo, + account_list_data: &'data mut [u8], + ) -> Result, ProgramError> { + let solido_list_address = match T::TYPE { + AccountType::Validator => self.validator_list, + AccountType::Maintainer => self.maintainer_list, + _ => { + msg!( + "Invalid account type {:?} when deserializing account list", + T::TYPE, + ); + return Err(LidoError::InvalidAccountType.into()); + } + }; + self.check_account_list_info::(program_id, &solido_list_address, account_list_info)?; + let (header, big_vec) = ListHeader::::deserialize_vec(account_list_data)?; + Ok(BigVecWithHeader::new(header, big_vec)) + } } #[repr(C)] @@ -640,128 +1206,6 @@ impl IntoIterator for &SeedRange { } } -impl Validator { - pub fn new() -> Validator { - Validator { - ..Default::default() - } - } - - /// Return the balance in only the stake accounts, excluding the unstake accounts. - pub fn effective_stake_balance(&self) -> Lamports { - (self.stake_accounts_balance - self.unstake_accounts_balance) - .expect("Unstake balance cannot exceed the validator's total stake balance.") - } - - pub fn observe_balance(observed: Lamports, tracked: Lamports, info: &str) -> ProgramResult { - if observed < tracked { - msg!( - "{}: observed balance of {} is less than tracked balance of {}.", - info, - observed, - tracked - ); - msg!("This should not happen, aborting ..."); - return Err(LidoError::ValidatorBalanceDecreased.into()); - } - Ok(()) - } -} - -impl Default for Validator { - fn default() -> Self { - Validator { - stake_seeds: SeedRange { begin: 0, end: 0 }, - unstake_seeds: SeedRange { begin: 0, end: 0 }, - stake_accounts_balance: Lamports(0), - unstake_accounts_balance: Lamports(0), - active: true, - } - } -} - -impl Validator { - pub fn has_stake_accounts(&self) -> bool { - self.stake_seeds.begin != self.stake_seeds.end - } - pub fn has_unstake_accounts(&self) -> bool { - self.unstake_seeds.begin != self.unstake_seeds.end - } - - pub fn check_can_be_removed(&self) -> Result<(), LidoError> { - if self.active { - return Err(LidoError::ValidatorIsStillActive); - } - if self.has_stake_accounts() { - return Err(LidoError::ValidatorShouldHaveNoStakeAccounts); - } - if self.has_unstake_accounts() { - return Err(LidoError::ValidatorShouldHaveNoUnstakeAccounts); - } - // If not, this is a bug. - assert_eq!(self.stake_accounts_balance, Lamports(0)); - Ok(()) - } - - pub fn show_removed_error_msg(error: &Result<(), LidoError>) { - if let Err(err) = error { - match err { - LidoError::ValidatorIsStillActive => { - msg!( - "Refusing to remove validator because it is still active, deactivate it first." - ); - } - LidoError::ValidatorHasUnclaimedCredit => { - msg!( - "Validator still has tokens to claim. Reclaim tokens before removing the validator" - ); - } - LidoError::ValidatorShouldHaveNoStakeAccounts => { - msg!("Refusing to remove validator because it still has stake accounts, unstake them first."); - } - LidoError::ValidatorShouldHaveNoUnstakeAccounts => { - msg!("Refusing to remove validator because it still has unstake accounts, withdraw them first."); - } - _ => { - msg!("Invalid error when removing a validator: shouldn't happen."); - } - } - } - } -} - -impl PubkeyAndEntry { - pub fn find_stake_account_address_with_authority( - &self, - program_id: &Pubkey, - solido_account: &Pubkey, - authority: &[u8], - seed: u64, - ) -> (Pubkey, u8) { - let seeds = [ - &solido_account.to_bytes(), - &self.pubkey.to_bytes(), - authority, - &seed.to_le_bytes()[..], - ]; - Pubkey::find_program_address(&seeds, program_id) - } - - pub fn find_stake_account_address( - &self, - program_id: &Pubkey, - solido_account: &Pubkey, - seed: u64, - stake_type: StakeType, - ) -> (Pubkey, u8) { - let authority = match stake_type { - StakeType::Stake => VALIDATOR_STAKE_ACCOUNT, - StakeType::Unstake => VALIDATOR_UNSTAKE_ACCOUNT, - }; - self.find_stake_account_address_with_authority(program_id, solido_account, authority, seed) - } -} - /// Determines how rewards are split up among these parties, represented as the /// number of parts of the total. For example, if each party has 1 part, then /// they all get an equal share of the reward. @@ -856,6 +1300,98 @@ pub struct Fees { pub st_sol_appreciation_amount: Lamports, } +/// The different ways to stake some amount from the reserve. +pub enum StakeDeposit { + /// Stake into a new stake account, and delegate the new account. + /// + /// This consumes the end seed of the validator's stake accounts. + Append, + + /// Stake into temporary stake account, and immediately merge it. + /// + /// This merges into the stake account at `end_seed - 1`. + Merge, +} + +/////////////////////////////////////////////////// OLD STATE /////////////////////////////////////////////////// + +/// An entry in `AccountMap`. +#[derive(Clone, Debug, BorshDeserialize, BorshSchema)] +pub struct PubkeyAndEntry { + pub pubkey: Pubkey, + pub entry: T, +} + +/// A map from public key to `T`, implemented as a vector of key-value pairs. +#[derive(Clone, Debug, BorshDeserialize, BorshSchema)] +pub struct AccountMap { + pub entries: Vec>, + pub maximum_entries: u32, +} + +#[repr(C)] +#[derive(Clone, Debug, BorshDeserialize, BorshSchema)] +pub struct ValidatorV1 { + pub fee_credit: StLamports, + pub fee_address: Pubkey, + pub stake_seeds: SeedRange, + pub unstake_seeds: SeedRange, + pub stake_accounts_balance: Lamports, + pub unstake_accounts_balance: Lamports, + pub active: bool, +} + +#[derive(Clone, Debug, BorshDeserialize, BorshSchema)] +pub struct RewardDistributionV1 { + pub treasury_fee: u32, + pub validation_fee: u32, + pub developer_fee: u32, + pub st_sol_appreciation: u32, +} + +#[repr(C)] +#[derive(Clone, Debug, BorshDeserialize, BorshSchema)] +pub struct LidoV1 { + pub lido_version: u8, + pub manager: Pubkey, + pub st_sol_mint: Pubkey, + pub exchange_rate: ExchangeRate, + pub sol_reserve_account_bump_seed: u8, + pub stake_authority_bump_seed: u8, + pub mint_authority_bump_seed: u8, + pub rewards_withdraw_authority_bump_seed: u8, + pub reward_distribution: RewardDistributionV1, + pub fee_recipients: FeeRecipients, + pub metrics: Metrics, + pub validators: AccountMap, + pub maintainers: AccountMap<()>, +} + +impl LidoV1 { + pub const LEN: usize = 353; + + pub fn deserialize_lido( + program_id: &Pubkey, + lido: &AccountInfo, + ) -> Result { + check_account_owner(lido, program_id)?; + let lido = try_from_slice_unchecked::(&lido.data.borrow())?; + Ok(lido) + } + + /// Checks if the passed manager is the same as the one stored in the state + pub fn check_manager(&self, manager: &AccountInfo) -> ProgramResult { + if &self.manager != manager.key { + msg!("Invalid manager, not the same as the one stored in state"); + return Err(LidoError::InvalidManager.into()); + } + Ok(()) + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* #[cfg(test)] mod test_lido { use super::*; @@ -864,8 +1400,8 @@ mod test_lido { #[test] fn test_account_map_required_bytes_relates_to_maximum_entries() { for buffer_size in 0..8_000 { - let max_entries = Validators::maximum_entries(buffer_size); - let needed_size = Validators::required_bytes(max_entries); + let max_entries = ValidatorList::calculate_max_entries(buffer_size); + let needed_size = ValidatorList::required_bytes(max_entries as u32); assert!( needed_size <= buffer_size || max_entries == 0, "Buffer of len {} can fit {} validators which need {} bytes.", @@ -873,31 +1409,18 @@ mod test_lido { max_entries, needed_size, ); - - let max_entries = Maintainers::maximum_entries(buffer_size); - let needed_size = Maintainers::required_bytes(max_entries); - assert!( - needed_size <= buffer_size || max_entries == 0, - "Buffer of len {} can fit {} maintainers which need {} bytes.", - buffer_size, - max_entries, - needed_size, - ); } } #[test] fn test_validators_size() { let validator = get_instance_packed_len(&Validator::default()).unwrap(); - assert_eq!(validator, Validator::SIZE); - let one_len = get_instance_packed_len(&Validators::new_fill_default(1)).unwrap(); - let two_len = get_instance_packed_len(&Validators::new_fill_default(2)).unwrap(); - assert_eq!(one_len, Validators::required_bytes(1)); - assert_eq!(two_len, Validators::required_bytes(2)); - assert_eq!( - two_len - one_len, - std::mem::size_of::() + Validator::SIZE - ); + assert_eq!(validator, Validator::LEN); + let one_len = get_instance_packed_len(&ValidatorList::new_default(1)).unwrap(); + let two_len = get_instance_packed_len(&ValidatorList::new_default(2)).unwrap(); + assert_eq!(one_len, ValidatorList::required_bytes(1)); + assert_eq!(two_len, ValidatorList::required_bytes(2)); + assert_eq!(two_len - one_len, Validator::LEN); } #[test] @@ -907,28 +1430,49 @@ mod test_lido { let minimal = Lido::default(); let mut data = Vec::new(); BorshSerialize::serialize(&minimal, &mut data).unwrap(); - - let num_entries = 0; - let size_validators = Validators::required_bytes(num_entries); - let size_maintainers = Maintainers::required_bytes(num_entries); - - assert_eq!( - data.len() - size_validators - size_maintainers, - LIDO_CONSTANT_SIZE - ); + assert_eq!(data.len(), Lido::LEN); } #[test] fn test_lido_serialization_roundtrips() { use solana_sdk::borsh::try_from_slice_unchecked; - let mut validators = Validators::new(10_000); - validators - .add(Pubkey::new_unique(), Validator::new()) - .unwrap(); - let maintainers = Maintainers::new(1); + fn test_list() { + // create empty account list with Vec + let mut accounts = AccountList::::new_default(0); + accounts.header.max_entries = 100; + + // allocate space for future elements + let mut buffer: Vec = + vec![0; AccountList::::required_bytes(accounts.header.max_entries)]; + let mut slice = &mut buffer[..]; + // seriaslize empty list to buffer, which serializes a header and lenght + BorshSerialize::serialize(&accounts, &mut slice).unwrap(); + + // deserialize to BigVec + let slice = &mut buffer[..]; + let (header, big_vec) = ListHeader::::deserialize_vec(slice).unwrap(); + let mut account_list = BigVecWithHeader::new(header, big_vec); + + for _ in 0..accounts.header.max_entries { + // add same account to both Vec and BigVec + let new_account = T::new(Pubkey::new_unique()); + account_list.push(new_account.clone()).unwrap(); + accounts.entries.push(new_account); + } + + // restore from BigVec to Vec and compare + let slice = &mut buffer[..]; + let accounts_restored = AccountList::::from(slice).unwrap(); + assert_eq!(accounts_restored, accounts); + } + + test_list::(); + test_list::(); + let lido = Lido { lido_version: 0, + account_type: AccountType::Lido, manager: Pubkey::new_unique(), st_sol_mint: Pubkey::new_unique(), exchange_rate: ExchangeRate { @@ -949,15 +1493,15 @@ mod test_lido { developer_account: Pubkey::new_unique(), }, metrics: Metrics::new(), - validators: validators, - maintainers: maintainers, + validator_list: Pubkey::new_unique(), + maintainer_list: Pubkey::new_unique(), max_commission_percentage: 5, }; let mut data = Vec::new(); BorshSerialize::serialize(&lido, &mut data).unwrap(); - let lido_restored = try_from_slice_unchecked(&data[..]).unwrap(); - assert_eq!(lido, lido_restored); + let restored = try_from_slice_unchecked(&data[..]).unwrap(); + assert_eq!(lido, restored); } #[test] @@ -1074,14 +1618,14 @@ mod test_lido { use std::rc::Rc; let rent = &Rent::default(); - let mut lido = Lido::default(); + let mut validators = ValidatorList::new_default(0); let key = Pubkey::default(); let mut amount = rent.minimum_balance(0); let mut reserve_account = AccountInfo::new(&key, true, true, &mut amount, &mut [], &key, false, 0); assert_eq!( - lido.get_sol_balance(&rent, &reserve_account), + Lido::get_sol_balance(validators.iter(), &rent, &reserve_account), Ok(Lamports(0)) ); @@ -1089,24 +1633,24 @@ mod test_lido { reserve_account.lamports = Rc::new(RefCell::new(&mut new_amount)); assert_eq!( - lido.get_sol_balance(&rent, &reserve_account), + Lido::get_sol_balance(validators.iter(), &rent, &reserve_account), Ok(Lamports(10)) ); - lido.validators.maximum_entries = 1; - lido.validators - .add(Pubkey::new_unique(), Validator::new()) - .unwrap(); - lido.validators.entries[0].entry.stake_accounts_balance = Lamports(37); + validators.header.max_entries = 1; + validators + .entries + .push(Validator::new(Pubkey::new_unique())); + validators.entries[0].stake_accounts_balance = Lamports(37); assert_eq!( - lido.get_sol_balance(&rent, &reserve_account), + Lido::get_sol_balance(validators.iter(), &rent, &reserve_account), Ok(Lamports(10 + 37)) ); - lido.validators.entries[0].entry.stake_accounts_balance = Lamports(u64::MAX); + validators.entries[0].stake_accounts_balance = Lamports(u64::MAX); assert_eq!( - lido.get_sol_balance(&rent, &reserve_account), + Lido::get_sol_balance(validators.iter(), &rent, &reserve_account), Err(LidoError::CalculationFailure) ); @@ -1114,10 +1658,10 @@ mod test_lido { reserve_account.lamports = Rc::new(RefCell::new(&mut new_amount)); // The amount here is more than the rent exemption that gets discounted // from the reserve, causing an overflow. - lido.validators.entries[0].entry.stake_accounts_balance = Lamports(5_000_000); + validators.entries[0].stake_accounts_balance = Lamports(5_000_000); assert_eq!( - lido.get_sol_balance(&rent, &reserve_account), + Lido::get_sol_balance(validators.iter(), &rent, &reserve_account), Err(LidoError::CalculationFailure) ); } @@ -1229,9 +1773,12 @@ mod test_lido { fn test_n_val() { let n_validators: u64 = 10_000; let size = - get_instance_packed_len(&Validators::new_fill_default(n_validators as u32)).unwrap(); + get_instance_packed_len(&ValidatorList::new_default(n_validators as u32)).unwrap(); - assert_eq!(Validators::maximum_entries(size) as u64, n_validators); + assert_eq!( + ValidatorList::calculate_max_entries(size) as u64, + n_validators + ); } #[test] @@ -1246,7 +1793,7 @@ mod test_lido { let mut res: Vec = Vec::new(); BorshSerialize::serialize(&lido, &mut res).unwrap(); - assert_eq!(res[0], i); + assert_eq!(res[1], i); let lido_recovered = try_from_slice_unchecked(&res[..]).unwrap(); assert_eq!(lido, lido_recovered); @@ -1279,4 +1826,91 @@ mod test_lido { } assert!(result.is_err()); } + + #[test] + fn check_lido_version() { + // create empty account list with Vec + let mut accounts = ValidatorList::new_default(1); + accounts.header.lido_version = 0; + + // allocate space for future elements + let mut buffer: Vec = + vec![0; ValidatorList::required_bytes(accounts.header.max_entries)]; + let mut slice = &mut buffer[..]; + // seriaslize empty list to buffer, which serializes a header and lenght + BorshSerialize::serialize(&accounts, &mut slice).unwrap(); + + // deserialize to BigVec + let slice = &mut buffer[..]; + let err = ListHeader::::deserialize_vec(slice).unwrap_err(); + assert_eq!(err, LidoError::LidoVersionMismatch.into()); + } + + #[test] + fn check_account_type() { + // create empty validator list with Vec + let accounts = ValidatorList::new_default(1); + + // allocate space for future elements + let mut buffer: Vec = + vec![0; ValidatorList::required_bytes(accounts.header.max_entries)]; + let mut slice = &mut buffer[..]; + // seriaslize empty list to buffer, which serializes a header and lenght + BorshSerialize::serialize(&accounts, &mut slice).unwrap(); + + // deserialize to BigVec but with a different account type + let slice = &mut buffer[..]; + let err = ListHeader::::deserialize_vec(slice).unwrap_err(); + assert_eq!(err, LidoError::InvalidAccountType.into()); + } + + #[test] + fn check_deserialize_with_borsh() { + // create empty validator list with Vec + let mut accounts = ValidatorList::new_default(1); + accounts.header.max_entries = 2; + + let mut elem = &mut accounts.entries[0]; + elem.vote_account_address = Pubkey::new_unique(); + elem.effective_stake_balance = Lamports(34453); + elem.stake_accounts_balance = Lamports(234525); + elem.active = true; + + // allocate space for future elements + let mut buffer: Vec = + vec![0; ValidatorList::required_bytes(accounts.header.max_entries)]; + let mut slice = &mut buffer[..]; + BorshSerialize::serialize(&accounts, &mut slice).unwrap(); + + let slice = &mut buffer[..]; + let (big_vec, header) = ListHeader::::deserialize_vec(slice).unwrap(); + let mut bigvec = BigVecWithHeader::new(big_vec, header); + + let elem = Validator { + vote_account_address: Pubkey::new_unique(), + stake_seeds: SeedRange { + begin: 123, + end: 5455, + }, + unstake_seeds: SeedRange { + begin: 555, + end: 9886, + }, + stake_accounts_balance: Lamports(1111), + unstake_accounts_balance: Lamports(3333), + effective_stake_balance: Lamports(3465468), + active: false, + }; + + accounts.entries.push(elem.clone()); + + bigvec.push(elem).unwrap(); + + let mut slice = &buffer[..]; + let accounts2 = BorshDeserialize::deserialize(&mut slice).unwrap(); + + // test that BigVec does not break borsh deserialization + assert_eq!(accounts, accounts2); + } } +*/ \ No newline at end of file diff --git a/program/src/util.rs b/program/src/util.rs index a098423c5..17f62bcd3 100644 --- a/program/src/util.rs +++ b/program/src/util.rs @@ -36,6 +36,7 @@ impl Serialize for PubkeyBase58 { } } +/* #[cfg(test)] mod test { use super::*; @@ -87,3 +88,4 @@ mod test { ) } } + */ \ No newline at end of file diff --git a/program/src/vote_state.rs b/program/src/vote_state.rs index 85631815e..ecb9e6c94 100644 --- a/program/src/vote_state.rs +++ b/program/src/vote_state.rs @@ -61,7 +61,7 @@ impl PartialVoteState { pubkey_buf.copy_from_slice(&data[4..][..32]); let node_pubkey = Pubkey::new_from_array(pubkey_buf); - let commission = get_vote_account_commission(&data).ok_or(LidoError::InvalidVoteAccount)?; + let commission = get_vote_account_commission(&data)?; if commission > max_commission_percentage { msg!( "Vote Account's commission should be <= {}, is {} instead", @@ -78,8 +78,11 @@ impl PartialVoteState { } } -pub fn get_vote_account_commission(vote_account_data: &[u8]) -> Option { - vote_account_data.get(68).copied() // Read 1 byte for u8. +pub fn get_vote_account_commission(vote_account_data: &[u8]) -> Result { + vote_account_data + .get(68) + .copied() + .ok_or(LidoError::InvalidVoteAccount) // Read 1 byte for u8. } #[cfg(test)] diff --git a/program/tests/mod.rs b/program/tests/mod.rs index 1e20c20f4..bfe794402 100644 --- a/program/tests/mod.rs +++ b/program/tests/mod.rs @@ -12,4 +12,4 @@ // By putting everything in a single module, we sidestep this problem. pub mod tests; -extern crate testlib; +//extern crate testlib; diff --git a/program/tests/tests/add_remove_validator.rs b/program/tests/tests/add_remove_validator.rs index 7eec8e263..bf4e67b96 100644 --- a/program/tests/tests/add_remove_validator.rs +++ b/program/tests/tests/add_remove_validator.rs @@ -3,12 +3,14 @@ use solana_program_test::tokio; use solana_sdk::pubkey::Pubkey; +use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; use testlib::assert_solido_error; use testlib::solido_context::{Context, ValidatorAccounts}; use lido::error::LidoError; +use lido::state::ListEntry; use lido::token::Lamports; pub const TEST_DEPOSIT_AMOUNT: Lamports = Lamports(100_000_000_000); @@ -25,7 +27,10 @@ async fn test_successful_add_validator() { let solido = context.get_solido().await; assert_eq!(solido.validators.len(), 1); - assert_eq!(solido.validators.entries[0].pubkey, validator.vote_account); + assert_eq!( + solido.validators.entries[0].pubkey(), + &validator.vote_account + ); // Adding the validator a second time should fail. let result = context.try_add_validator(&validator).await; @@ -55,6 +60,7 @@ async fn test_add_validator_with_invalid_owner() { .try_add_validator(&ValidatorAccounts { node_account: node_account, vote_account: invalid_vote_account.pubkey(), + withdraw_authority: Keypair::new(), }) .await; assert_solido_error!(result, LidoError::ValidatorVoteAccountHasDifferentOwner); @@ -64,9 +70,9 @@ async fn test_add_validator_with_invalid_owner() { async fn test_successful_remove_validator() { let mut context = Context::new_with_maintainer_and_validator().await; let validator = &context.get_solido().await.validators.entries[0]; - context.deactivate_validator(validator.pubkey).await; + context.deactivate_validator(*validator.pubkey()).await; context - .try_remove_validator(validator.pubkey) + .try_remove_validator(*validator.pubkey()) .await .unwrap(); @@ -78,7 +84,7 @@ async fn test_successful_remove_validator() { async fn test_removing_validator_with_stake_accounts_should_fail() { let (mut context, _) = Context::new_with_two_stake_accounts().await; let validator = &context.get_solido().await.validators.entries[0]; - let result = context.try_remove_validator(validator.pubkey).await; + let result = context.try_remove_validator(*validator.pubkey()).await; // The validator should not be able to be removed if it is still active // (i.e. the active flag is set toe true OR it has stake accounts) @@ -94,14 +100,14 @@ async fn test_deactivate_validator() { // Initially, the validator should be active. let solido = context.get_solido().await; assert_eq!(solido.validators.len(), 1); - assert!(solido.validators.entries[0].entry.active); + assert!(solido.validators.entries[0].active); context.deactivate_validator(validator.vote_account).await; // After deactivation, it should be inactive. let solido = context.get_solido().await; assert_eq!(solido.validators.len(), 1); - assert!(!solido.validators.entries[0].entry.active); + assert!(!solido.validators.entries[0].active); // Deactivation is idempotent. context.deactivate_validator(validator.vote_account).await; diff --git a/program/tests/tests/change_reward_distribution.rs b/program/tests/tests/change_reward_distribution.rs index fdfecdd02..6508cb898 100644 --- a/program/tests/tests/change_reward_distribution.rs +++ b/program/tests/tests/change_reward_distribution.rs @@ -14,7 +14,7 @@ use testlib::solido_context::Context; async fn test_successful_change_reward_distribution() { let mut context = Context::new_with_maintainer().await; - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!(solido.reward_distribution, context.reward_distribution); assert_eq!( solido.fee_recipients.treasury_account, @@ -52,7 +52,7 @@ async fn test_successful_change_reward_distribution() { .await .expect("Failed to change fees."); - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!(solido.reward_distribution, new_fee); assert_eq!(solido.fee_recipients.treasury_account, new_treasury_addr,); assert_eq!(solido.fee_recipients.developer_account, new_developer_addr,); @@ -72,7 +72,7 @@ async fn test_change_reward_distribution_wrong_minter() { .create_st_sol_account(not_st_sol_owner.pubkey()) .await; - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; let result = context .try_change_reward_distribution( diff --git a/program/tests/tests/deposit.rs b/program/tests/tests/deposit.rs index 7808b50a2..b1e446c7e 100644 --- a/program/tests/tests/deposit.rs +++ b/program/tests/tests/deposit.rs @@ -29,7 +29,7 @@ async fn test_successful_deposit() { let st_sol_balance = context.get_st_sol_balance(recipient).await; assert_eq!(st_sol_balance.0, TEST_DEPOSIT_AMOUNT.0); - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!(solido.metrics.deposit_amount.total, TEST_DEPOSIT_AMOUNT); assert_eq!(solido.metrics.deposit_amount.num_observations(), 1); } diff --git a/program/tests/tests/limits.rs b/program/tests/tests/limits.rs index 1e02adf17..1353e0427 100644 --- a/program/tests/tests/limits.rs +++ b/program/tests/tests/limits.rs @@ -7,10 +7,12 @@ //! expectations; there is no "right" answer, but we would like to know what //! how many accounts Solido can handle. -use testlib::solido_context::{Context, StakeDeposit, ValidatorAccounts}; +use testlib::solido_context::Context; +use lido::state::StakeDeposit; use lido::token::Lamports; +use solana_program::pubkey::Pubkey; use solana_program_test::tokio; /// Test how many stake accounts per validator we can support. @@ -25,7 +27,7 @@ async fn test_update_stake_account_balance_max_accounts() { let validator = context.add_validator().await; // The maximum number of stake accounts per validator that we can support, - // before WithdrawInactiveStake fails. + // before UpdateStakeAccountBalance fails. let max_accounts = 9; for i in 0..=max_accounts { @@ -35,7 +37,7 @@ async fn test_update_stake_account_balance_max_accounts() { .stake_deposit(validator.vote_account, StakeDeposit::Append, amount) .await; - // Put some additional SOL in the stake account, so `WithdrawInactiveStake` + // Put some additional SOL in the stake account, so `UpdateStakeAccountBalance` // has something to withdraw. This consumes more compute units than a // no-op update, so we actually test the worst case. context.fund(stake_account, Lamports(100_000)).await; @@ -60,39 +62,50 @@ async fn test_update_stake_account_balance_max_accounts() { } #[tokio::test] +#[ignore] async fn test_max_validators_maintainers() { let mut context = Context::new_with_maintainer().await; // The maximum number of validators that we can support, before Deposit or // StakeDeposit fails. - let max_validators: u32 = 82; + let max_validators: u32 = 6_700; - let mut validator: Option = None; + let mut first_validator_vote_account = Pubkey::default(); for i in 0..max_validators { - context - .memo(&format!("Adding maintainer and validator {}.", i + 1)) - .await; - // Initially expect every validator to be a maintainer as well, so let's // add a maintainer for every validator. We set this to be the context's - // maintainer that is used to sign `stake_deposit`. We use a linear - // search, so the later maintainers are slightly more expensive to check. + // maintainer that is used to sign `stake_deposit`. let maintainer = context.add_maintainer().await; context.maintainer = Some(maintainer); - validator = Some(context.add_validator().await); + let validator = context.add_validator().await; + if i == 0 { + first_validator_vote_account = validator.vote_account; + } + // test with step 100 to reduce waiting + if (i + 1) % 100 == 0 { + context + .memo(&format!("Testing heavy load {}.", i + 1)) + .await; + + let amount = Lamports(2_000_000_000); + context.deposit(amount).await; + context + .stake_deposit(validator.vote_account, StakeDeposit::Append, amount) + .await; + context + .unstake(validator.vote_account, Lamports(1_000_000_000)) + .await; + // If we get here, then none of the transactions failed. + } } - // take out of the loop to reduce the time to wait - if let Some(validator) = validator { - let amount = Lamports(2_000_000_000); - context.deposit(amount).await; - context - .stake_deposit(validator.vote_account, StakeDeposit::Append, amount) - .await; - context - .unstake(validator.vote_account, Lamports(1_000_000_000)) - .await; - // If we get here, then none of the transactions failed. - } + // remove from the beginning of a list to test worst case + context + .deactivate_validator(first_validator_vote_account) + .await; + context + .try_remove_validator(first_validator_vote_account) + .await + .expect("Could not remove first validator"); } diff --git a/program/tests/tests/max_commission_percentage.rs b/program/tests/tests/max_commission_percentage.rs index 679877440..8436b29e3 100644 --- a/program/tests/tests/max_commission_percentage.rs +++ b/program/tests/tests/max_commission_percentage.rs @@ -1,6 +1,8 @@ use lido::error::LidoError; +use lido::state::ListEntry; use solana_program_test::tokio; +use solana_sdk::signature::Keypair; use testlib::assert_solido_error; use testlib::solido_context::Context; @@ -14,18 +16,18 @@ async fn test_set_max_commission_percentage() { let result = context.try_set_max_commission_percentage(context.max_commission_percentage + 1); assert_eq!(result.await.is_ok(), true); - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!( solido.max_commission_percentage, context.max_commission_percentage + 1 ); - let result = context.try_deactivate_validator_if_commission_exceeds_max(validator.pubkey); + let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); assert_eq!(result.await.is_ok(), true); // check validator is not deactivated let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.entry.active, true); + assert_eq!(validator.active, true); // Increase max_commission_percentage above 100% assert_solido_error!( @@ -37,10 +39,37 @@ async fn test_set_max_commission_percentage() { let result = context.try_set_max_commission_percentage(context.max_commission_percentage - 1); assert_eq!(result.await.is_ok(), true); - let result = context.try_deactivate_validator_if_commission_exceeds_max(validator.pubkey); + let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); assert_eq!(result.await.is_ok(), true); // check validator is deactivated let validator = &context.get_solido().await.validators.entries[0]; - assert_eq!(validator.entry.active, false); + assert_eq!(validator.active, false); +} + +#[tokio::test] +async fn test_close_vote_account() { + let mut context = Context::new_with_maintainer_and_validator().await; + let vote_account = context.validator.as_ref().unwrap().vote_account; + + let validator = &context.get_solido().await.validators.entries[0]; + assert_eq!(validator.active, true); + + let keypair_bytes = context + .validator + .as_ref() + .unwrap() + .withdraw_authority + .to_bytes(); + + let withdraw_authority = Keypair::from_bytes(&keypair_bytes).unwrap(); + + let result = context.try_close_vote_account(&vote_account, &withdraw_authority); + assert_eq!(result.await.is_ok(), true); + + let result = context.try_deactivate_validator_if_commission_exceeds_max(*validator.pubkey()); + assert_eq!(result.await.is_ok(), true); + + let validator = &context.get_solido().await.validators.entries[0]; + assert_eq!(validator.active, false); } diff --git a/program/tests/tests/merge_stake.rs b/program/tests/tests/merge_stake.rs index 4eb600285..a985d0147 100644 --- a/program/tests/tests/merge_stake.rs +++ b/program/tests/tests/merge_stake.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-3.0 use testlib::assert_solido_error; -use testlib::solido_context::{self, get_account_info, Context, StakeDeposit}; +use testlib::solido_context::{self, get_account_info, Context}; use lido::processor::StakeType; +use lido::state::{Lido, ListEntry, StakeDeposit}; use lido::{error::LidoError, token::Lamports}; use solana_program_test::tokio; use solana_sdk::signer::Signer; @@ -33,24 +34,24 @@ async fn test_successful_merge_activating_stake() { let solido_after = context.get_solido().await; let mut reserve_after = context.get_account(context.reserve_address).await; assert_eq!( - solido_after.validators.entries[0] - .entry - .stake_accounts_balance, + solido_after.validators.entries[0].stake_accounts_balance, Lamports(20_000_000_000) ); - let validator_before = &solido_before.validators.entries[0].entry; - let validator_after = &solido_after.validators.entries[0].entry; + let validator_before = &solido_before.validators.entries[0]; + let validator_after = &solido_after.validators.entries[0]; assert_eq!( validator_after.stake_seeds.begin, validator_before.stake_seeds.begin + 1, ); - let sol_before = solido_before.get_sol_balance( + let sol_before = Lido::get_sol_balance( + solido_before.validators.entries.iter(), &rent, &get_account_info(&context.reserve_address, &mut reserve_before), ); - let sol_after = solido_after.get_sol_balance( + let sol_after = Lido::get_sol_balance( + solido_after.validators.entries.iter(), &rent, &get_account_info(&context.reserve_address, &mut reserve_after), ); @@ -72,14 +73,22 @@ async fn test_merge_stake_combinations() { let validator = &context.get_solido().await.validators.entries[0]; context.deposit(Lamports(100_000_000_000)).await; context - .stake_deposit(validator.pubkey, StakeDeposit::Append, stake_deposit_amount) + .stake_deposit( + *validator.pubkey(), + StakeDeposit::Append, + stake_deposit_amount, + ) .await; context.advance_to_normal_epoch(1); // Create an activating stake account. context - .stake_deposit(validator.pubkey, StakeDeposit::Append, stake_deposit_amount) + .stake_deposit( + *validator.pubkey(), + StakeDeposit::Append, + stake_deposit_amount, + ) .await; let active_stake_account = context.get_stake_account_from_seed(&validator, 0).await; @@ -108,11 +117,13 @@ async fn test_merge_stake_combinations() { let solido_after = context.get_solido().await; let mut reserve_after = context.get_account(context.reserve_address).await; - let sol_before = solido_before.get_sol_balance( + let sol_before = Lido::get_sol_balance( + solido_before.validators.entries.iter(), &rent, &get_account_info(&context.reserve_address, &mut reserve_before), ); - let sol_after = solido_after.get_sol_balance( + let sol_after = Lido::get_sol_balance( + solido_after.validators.entries.iter(), &rent, &get_account_info(&context.reserve_address, &mut reserve_after), ); @@ -135,7 +146,7 @@ async fn test_merge_validator_with_zero_and_one_stake_account() { context .stake_deposit( - validator.pubkey, + *validator.pubkey(), StakeDeposit::Append, Lamports(10_000_000_000), ) diff --git a/program/tests/tests/solana_assumptions.rs b/program/tests/tests/solana_assumptions.rs index ec19508d7..b7db84f09 100644 --- a/program/tests/tests/solana_assumptions.rs +++ b/program/tests/tests/solana_assumptions.rs @@ -152,7 +152,7 @@ async fn test_deactivating_stake_earns_rewards() { // does not prevent rewards. assert_eq!(rewards_inactive, Lamports(0)); assert_eq!(rewards_active, Lamports(19_974_887_558)); - assert_eq!(rewards_deactivating, Lamports(19_974_887_558)); + assert_eq!(rewards_deactivating, Lamports(19_974_887_557)); } #[tokio::test] diff --git a/program/tests/tests/stake_deposit.rs b/program/tests/tests/stake_deposit.rs index 1830b4276..d1e904021 100644 --- a/program/tests/tests/stake_deposit.rs +++ b/program/tests/tests/stake_deposit.rs @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 -use testlib::solido_context::{id, Context, StakeDeposit}; +use testlib::solido_context::{id, Context}; use testlib::{assert_error_code, assert_solido_error}; use lido::error::LidoError; use lido::processor::StakeType; +use lido::state::{ListEntry, StakeDeposit}; use lido::token::Lamports; +use lido::MINIMUM_STAKE_ACCOUNT_BALANCE; use solana_program_test::tokio; use solana_sdk::signer::Signer; @@ -20,7 +22,7 @@ async fn test_stake_deposit_append() { // Sanity check before we start: the validator should have zero balance in zero stake accounts. let solido_before = context.get_solido().await; - let validator_before = &solido_before.validators.entries[0].entry; + let validator_before = &solido_before.validators.entries[0]; assert_eq!(validator_before.stake_accounts_balance, Lamports(0)); assert_eq!(validator_before.stake_seeds.begin, 0); assert_eq!(validator_before.stake_seeds.end, 0); @@ -46,7 +48,7 @@ async fn test_stake_deposit_append() { // has balance in a stake account. let solido_after = context.get_solido().await; - let validator_after = &solido_after.validators.entries[0].entry; + let validator_after = &solido_after.validators.entries[0]; assert_eq!( validator_after.stake_accounts_balance, TEST_STAKE_DEPOSIT_AMOUNT @@ -96,7 +98,7 @@ async fn test_stake_deposit_merge() { // We should also have recorded in the Solido state that this validator now // has balance in a stake account. let solido_after = context.get_solido().await; - let validator_after = &solido_after.validators.entries[0].entry; + let validator_after = &solido_after.validators.entries[0]; assert_eq!( validator_after.stake_accounts_balance, (TEST_STAKE_DEPOSIT_AMOUNT * 2).unwrap(), @@ -166,7 +168,7 @@ async fn test_stake_deposit_succeeds_despite_donation() { context.deposit(TEST_DEPOSIT_AMOUNT).await; context .stake_deposit( - validator.pubkey, + *validator.pubkey(), StakeDeposit::Append, TEST_STAKE_DEPOSIT_AMOUNT, ) @@ -174,15 +176,17 @@ async fn test_stake_deposit_succeeds_despite_donation() { // The state does not record the additional balance yet though. let solido = context.get_solido().await; - let validator_entry = &solido.validators.entries[0].entry; + let validator_entry = &solido.validators.entries[0]; assert_eq!( validator_entry.stake_accounts_balance, TEST_STAKE_DEPOSIT_AMOUNT ); - context.update_stake_account_balance(validator.pubkey).await; + context + .update_stake_account_balance(*validator.pubkey()) + .await; let solido = context.get_solido().await; - let validator_entry = &solido.validators.entries[0].entry; + let validator_entry = &solido.validators.entries[0]; assert_eq!( validator_entry.stake_accounts_balance, (TEST_STAKE_DEPOSIT_AMOUNT + Lamports(107_000_000)).unwrap() @@ -193,6 +197,8 @@ async fn test_stake_deposit_succeeds_despite_donation() { async fn test_stake_deposit_fails_for_inactive_validator() { let mut context = Context::new_with_maintainer().await; let validator = context.add_validator().await; + // let one validator be active + context.add_validator().await; context.deactivate_validator(validator.vote_account).await; @@ -223,7 +229,7 @@ async fn test_stake_deposit_fails_if_validator_with_less_stake_exists() { .stake_deposit( v1.vote_account, StakeDeposit::Append, - Lamports(1_000_000_000), + MINIMUM_STAKE_ACCOUNT_BALANCE, ) .await; @@ -233,7 +239,7 @@ async fn test_stake_deposit_fails_if_validator_with_less_stake_exists() { .try_stake_deposit( v1.vote_account, StakeDeposit::Append, - Lamports(1_000_000_000), + MINIMUM_STAKE_ACCOUNT_BALANCE, ) .await; assert_solido_error!(result, LidoError::ValidatorWithLessStakeExists); @@ -243,7 +249,7 @@ async fn test_stake_deposit_fails_if_validator_with_less_stake_exists() { .stake_deposit( v2.vote_account, StakeDeposit::Append, - Lamports(1_000_000_000), + MINIMUM_STAKE_ACCOUNT_BALANCE, ) .await; @@ -252,7 +258,7 @@ async fn test_stake_deposit_fails_if_validator_with_less_stake_exists() { .stake_deposit( v2.vote_account, StakeDeposit::Append, - Lamports(1_000_000_000), + MINIMUM_STAKE_ACCOUNT_BALANCE, ) .await; } diff --git a/program/tests/tests/unstake.rs b/program/tests/tests/unstake.rs index 239580a04..66067fc06 100644 --- a/program/tests/tests/unstake.rs +++ b/program/tests/tests/unstake.rs @@ -1,12 +1,11 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 -#![cfg(feature = "test-bpf")] - use testlib::assert_solido_error; -use testlib::solido_context::{self, Context, StakeDeposit}; +use testlib::solido_context::{self, Context}; use lido::processor::StakeType; +use lido::state::{ListEntry, StakeDeposit}; use lido::MINIMUM_STAKE_ACCOUNT_BALANCE; use lido::{error::LidoError, token::Lamports}; use solana_program::stake::state::StakeState; @@ -60,7 +59,7 @@ async fn test_successful_unstake() { let validator = &solido.validators.entries[0]; let stake_account_before = context.get_stake_account_from_seed(&validator, 0).await; - context.unstake(validator.pubkey, unstake_lamports).await; + context.unstake(*validator.pubkey(), unstake_lamports).await; let stake_account_after = context.get_stake_account_from_seed(&validator, 0).await; assert_eq!( (stake_account_before.balance.total() - stake_account_after.balance.total()).unwrap(), @@ -169,8 +168,8 @@ async fn test_unstake_from_inactive_validator() { let solido_after = context.get_solido().await; assert_eq!( - solido_before.validators.entries[0].entry.stake_seeds.begin + 1, - solido_after.validators.entries[0].entry.stake_seeds.begin, + solido_before.validators.entries[0].stake_seeds.begin + 1, + solido_after.validators.entries[0].stake_seeds.begin, "Unstaking the full stake account amount should have bumped the steed.", ); @@ -179,17 +178,17 @@ async fn test_unstake_from_inactive_validator() { let validator = &context.get_solido().await.validators.entries[0]; assert_eq!( - validator.entry.stake_seeds.begin, validator.entry.stake_seeds.end, + validator.stake_seeds.begin, validator.stake_seeds.end, "No stake accounts should be left after unstaking both." ); assert_eq!( - validator.entry.stake_accounts_balance, validator.entry.unstake_accounts_balance, + validator.stake_accounts_balance, validator.unstake_accounts_balance, "The full balance should be in unstake accounts after unstaking both." ); let (stake_account, _) = validator.find_stake_account_address( &solido_context::id(), &context.solido.pubkey(), - validator.entry.stake_seeds.begin, + validator.stake_seeds.begin, StakeType::Stake, ); let account = context.try_get_account(stake_account).await; @@ -212,7 +211,7 @@ async fn test_unstake_with_funded_destination_stake() { context.fund(unstake_address, Lamports(500_000_000)).await; let unstake_lamports = Lamports(1_000_000_000); - context.unstake(validator.pubkey, unstake_lamports).await; + context.unstake(*validator.pubkey(), unstake_lamports).await; let unstake_account = context.get_unstake_account_from_seed(&validator, 0).await; // Since we already had something in the account that paid for the rent, we // can unstake all the requested amount. @@ -238,7 +237,7 @@ async fn test_unstake_allows_at_most_three_unstake_accounts() { context.advance_to_normal_epoch(1); let solido_before = context.get_solido().await; - let validator_before = &solido_before.validators.entries[0].entry; + let validator_before = &solido_before.validators.entries[0]; assert_eq!(validator_before.unstake_seeds.begin, 0); assert_eq!(validator_before.unstake_seeds.end, 3); @@ -247,7 +246,7 @@ async fn test_unstake_allows_at_most_three_unstake_accounts() { context.update_stake_account_balance(vote_account).await; let solido_after = context.get_solido().await; - let validator_after = &solido_after.validators.entries[0].entry; + let validator_after = &solido_after.validators.entries[0]; assert_eq!(validator_after.unstake_seeds.begin, 3); assert_eq!(validator_after.unstake_seeds.end, 3); @@ -266,7 +265,7 @@ async fn test_unstake_activating() { context.deposit(Lamports(10_000_000_000)).await; context .stake_deposit( - validator.pubkey, + *validator.pubkey(), StakeDeposit::Append, Lamports(10_000_000_000), ) @@ -282,7 +281,7 @@ async fn test_unstake_activating() { (Lamports(10_000_000_000) - Lamports(stake_rent)).unwrap() ); - context.unstake(validator.pubkey, unstake_lamports).await; + context.unstake(*validator.pubkey(), unstake_lamports).await; let stake_account_after = context.get_stake_account_from_seed(&validator, 0).await; assert_eq!( (stake_account_before.balance.total() - stake_account_after.balance.total()).unwrap(), diff --git a/program/tests/tests/update_exchange_rate.rs b/program/tests/tests/update_exchange_rate.rs index 98ec61ee7..d47b51284 100644 --- a/program/tests/tests/update_exchange_rate.rs +++ b/program/tests/tests/update_exchange_rate.rs @@ -20,7 +20,7 @@ async fn test_update_exchange_rate() { let start_epoch = context.get_clock().await.epoch; // Initially the balance is zero, and we haven't minted any stSOL. - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!( solido.exchange_rate, ExchangeRate { @@ -49,7 +49,7 @@ async fn test_update_exchange_rate() { // There was one deposit, the exchange rate was 1:1, we should now have the // same amount of SOL and stSOL. - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!( solido.exchange_rate, ExchangeRate { @@ -81,7 +81,7 @@ async fn test_update_exchange_rate() { context.update_exchange_rate().await; - let solido = context.get_solido().await; + let solido = context.get_solido().await.lido; assert_eq!( solido.exchange_rate, ExchangeRate { diff --git a/program/tests/tests/update_stake_account_balance.rs b/program/tests/tests/update_stake_account_balance.rs index 5b6929b6b..9acf44695 100644 --- a/program/tests/tests/update_stake_account_balance.rs +++ b/program/tests/tests/update_stake_account_balance.rs @@ -2,10 +2,12 @@ // SPDX-License-Identifier: GPL-3.0 use lido::error::LidoError; +use lido::state::StakeDeposit; use lido::token::Lamports; +use lido::MINIMUM_STAKE_ACCOUNT_BALANCE; use solana_program_test::tokio; use testlib::assert_solido_error; -use testlib::solido_context::{Context, StakeDeposit}; +use testlib::solido_context::Context; #[tokio::test] async fn test_update_stake_account_balance() { @@ -24,7 +26,7 @@ async fn test_update_stake_account_balance() { assert_eq!(solido_before, solido_after); // Deposit and stake the deposit with the validator. This creates one stake account. - let initial_amount = Lamports(1_000_000_000); + let initial_amount = MINIMUM_STAKE_ACCOUNT_BALANCE; context.deposit(initial_amount).await; let stake_account = context .stake_deposit(validator.vote_account, StakeDeposit::Append, initial_amount) @@ -95,7 +97,7 @@ async fn test_update_stake_account_balance() { let solido_before = context.get_solido().await; let validator_before = solido_before .validators - .get(&validator.vote_account) + .find(&validator.vote_account) .unwrap(); let account = context.get_account(validator.vote_account).await; @@ -129,11 +131,11 @@ async fn test_update_stake_account_balance() { let solido_after = context.get_solido().await; let validator_after = solido_after .validators - .get(&validator.vote_account) + .find(&validator.vote_account) .unwrap(); - let rewards = (validator_after.entry.stake_accounts_balance - - validator_before.entry.stake_accounts_balance) + let rewards = (validator_after.stake_accounts_balance + - validator_before.stake_accounts_balance) .expect("Does not underflow, because we received rewards."); assert_eq!(rewards, Lamports(arbitrary_rewards)); @@ -148,6 +150,7 @@ async fn test_update_stake_account_balance() { // to 3% of the rewards. Three lamports differ due to rounding errors. let treasury_fee = (treasury_after - treasury_before).unwrap(); let treasury_fee_sol = solido_after + .lido .exchange_rate .exchange_st_sol(treasury_fee) .unwrap(); @@ -157,6 +160,7 @@ async fn test_update_stake_account_balance() { // to 2% of the rewards. Two lamport differ due to rounding errors. let developer_fee = (developer_after - developer_before).unwrap(); let developer_fee_sol = solido_after + .lido .exchange_rate .exchange_st_sol(developer_fee) .unwrap(); diff --git a/program/tests/tests/withdrawals.rs b/program/tests/tests/withdrawals.rs index be1ca3d06..51458e6b6 100644 --- a/program/tests/tests/withdrawals.rs +++ b/program/tests/tests/withdrawals.rs @@ -12,12 +12,13 @@ use solana_sdk::transport; use lido::{ error::LidoError, + state::StakeDeposit, token::{Lamports, StLamports}, MINIMUM_STAKE_ACCOUNT_BALANCE, }; use testlib::{ assert_solido_error, - solido_context::{send_transaction, Context, StakeDeposit}, + solido_context::{send_transaction, Context}, }; /// Shared context for tests where a given amount has been deposited and staked. @@ -85,6 +86,18 @@ async fn test_withdraw_less_than_rent_fails() { assert!(result.is_ok()); } +#[tokio::test] +async fn test_withdraw_from_inactive_validator() { + let mut context = WithdrawContext::new((MINIMUM_STAKE_ACCOUNT_BALANCE * 2).unwrap()).await; + + let validator = context.context.validator.as_ref().unwrap(); + let vote_account = validator.vote_account.clone(); + context.context.deactivate_validator(vote_account).await; + + let result = context.try_withdraw(StLamports(MINIMUM_STAKE_ACCOUNT_BALANCE.0 - 1)); + assert!(result.await.is_ok()); +} + #[tokio::test] async fn test_withdraw_beyond_min_balance_fails() { let mut context = WithdrawContext::new((MINIMUM_STAKE_ACCOUNT_BALANCE * 2).unwrap()).await; @@ -146,7 +159,7 @@ async fn test_withdrawal_result() { let split_stake_account = context.try_withdraw(test_withdraw_amount).await.unwrap(); let split_stake_sol_balance = context.context.get_sol_balance(split_stake_account).await; - let solido = context.context.get_solido().await; + let solido = context.context.get_solido().await.lido; let amount_lamports = solido .exchange_rate .exchange_st_sol(test_withdraw_amount) @@ -169,7 +182,7 @@ async fn test_withdrawal_result() { assert_eq!(stake_account_balance_after, Lamports(99_997_717_119)); // Test if we updated the metrics - let solido_after = context.context.get_solido().await; + let solido_after = context.context.get_solido().await.lido; assert_eq!( solido_after.metrics.withdraw_amount.total_st_sol_amount, test_withdraw_amount diff --git a/scripts/copy_targets.sh b/scripts/copy_targets.sh new file mode 100755 index 000000000..0ab24ebf2 --- /dev/null +++ b/scripts/copy_targets.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +function copy_dir() { + cargo build --release --manifest-path $1/Cargo.toml + cargo build-bpf --manifest-path $1/Cargo.toml + + scp -r $1/target/deploy/serum_multisig.so $2/deploy + scp -r $1/target/deploy/lido.so $2/deploy + scp -r $1/target/release/solido $2/debug +} + +copy_dir solido_old build:/home/guyos/test_setup/solido_old/target +copy_dir solido build:/home/guyos/test_setup/solido/target diff --git a/scripts/install_solido.py b/scripts/install_solido.py new file mode 100755 index 000000000..491bb2c89 --- /dev/null +++ b/scripts/install_solido.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sys +import os.path +from typing import Any + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +from tests.util import solido, solana, run # type: ignore + + +def check_env(param): + buf = param + " = " + os.getenv(param) + if os.getenv(param) != None: + buf += " [OK]" + else: + buf += " [BAD]" + print(buf) + + +def verify_installation(): + check_env("PWD") + check_env("SOLIDO_V1") + check_env("SOLIDO_V2") + check_env("SOLIDO_CONFIG") + + +def install_solido(): + pathStr = os.getenv("PWD") + + # install solido v1 + if not os.path.isdir(pathStr + "/solido_v1/"): + outout = os.system( + "git clone --recurse-submodules -b v1.3.6 https://github.com/lidofinance/solido solido_v1" + ) + output = os.chdir(pathStr + "/solido_v1/") + outout = os.system("cargo build --release") + if os.path.isfile(pathStr + "/solido_v1/target/release/solido"): + os.environ["SOLIDO_V1"] = pathStr + "/solido_v1/target/release/solido" + else: + print("Program not exist: " + pathStr + "/solido_v1/target/release/solido") + output = os.chdir(pathStr) + + # install solido v2 + if not os.path.isdir(pathStr + "/solido_v2/"): + outout = os.system( + "git clone --recurse-submodules -b v2.0.0 https://github.com/lidofinance/solido solido_v2" + ) + output = os.chdir(pathStr + "/solido_v2/") + outout = os.system("cargo build --release") + if os.path.isfile(pathStr + "/solido_v2/target/release/solido"): + os.environ["SOLIDO_V2"] = pathStr + "/solido_v2/target/release/solido" + else: + print("Program not exist: " + pathStr + "/solido_v2/target/release/solido") + output = os.chdir(pathStr) + + # install config + if not os.path.isfile(pathStr + "/solido_config.json"): + outout = os.system("cp ./solido_v2/solido_config.json solido_config.json") + os.environ["SOLIDO_CONFIG"] = pathStr + "/solido_config.json" + + # verify installation + verify_installation() diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100755 index 000000000..8fa191d4f --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,154 @@ +#!/bin/bash + +############################################################################### +# EPOCH 0 # +############################################################################### + +cd home + +# start local validator +rm -rf tests/.keys/ test-ledger/ tests/__pycache__/ && \ + solana-test-validator --slots-per-epoch 150 + +# withdraw SOLs from local validator vote account to start fresh +solana withdraw-from-vote-account test-ledger/vote-account-keypair.json \ + $(solana-keygen pubkey) \ + 999999.9 --authorized-withdrawer test-ledger/vote-account-keypair.json + +# create instance +./tests/deploy_test_solido.py --verbose + +# optional for test +cp ./solido_test.json ./solido_config.json + +# start maintainer +./solido_v1/target/release/solido --config ./solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + run-maintainer --max-poll-interval-seconds 1 + +# deposit some SOL +./solido_v1/target/release/solido --config ./solido_config.json deposit --amount-sol 100 + +############################################################################### +# EPOCH 1 # +############################################################################### + +# receive some rewards + +############################################################################### +# EPOCH 2 # +############################################################################### + +# create new v2 accounts +./solido_v2/target/release/solido \ + --output json \ + --config ./solido_test.json create-v2-accounts \ + --developer-account-owner 2d7gxHrVHw2grzWBdRQcWS7T1r9KnaaGXZBtzPBbzHEF \ + > v2_new_accounts.json +jq -s '.[0] * .[1]' v2_new_accounts.json ./solido_test.json > ./temp.json +mv ./temp.json ./solido_test.json + +# load program to a buffer account +./solido_v2/scripts/operation.py \ + load-program --program-filepath ./solido_v2/target/deploy/lido.so --outfile buffer + +# deactivate validators +./solido_v2/scripts/operation.py \ + deactivate-validators --keypair-path ./solido_v1/tests/.keys/maintainer.json --outfile deactivation_trx.txt + +# verify transaction +./solido_v2/scripts/operation.py \ + check-transactions --phase deactivation --transactions-path deactivation_trx.txt + + +# batch sign transactions +./solido_v2/target/release/solido --config solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + multisig approve-batch --silent --transaction-addresses-path deactivation_trx.txt +# execute transactions one by one +./solido_v2/scripts/operation.py \ + execute-transactions --transactions deactivation_trx.txt \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + --phase deactivation + +# create a new validator keys with a 5% commission +solana-keygen new --no-bip39-passphrase --force --silent \ + --outfile ./solido_v1/tests/.keys/vote-account-key.json +solana-keygen new --no-bip39-passphrase --force --silent \ + --outfile ./solido_v1/tests/.keys/vote-account-withdrawer-key.json +solana create-vote-account \ + ./solido_v1/tests/.keys/vote-account-key.json \ + ./solido_v1/test-ledger/validator-keypair.json \ + ./solido_v1/tests/.keys/vote-account-withdrawer-key.json --commission 5 + +############################################################################### +# EPOCH 3 # +############################################################################### + + +# propose program upgrade +./solido_v2/target/release/solido --output json --config ./solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + multisig propose-upgrade \ + --spill-address $(solana-keygen pubkey) \ + --buffer-address "$(< ./buffer)" \ + --program-address $(jq -r .solido_program_id ./solido_config.json) > tempfile +awk '/{/,/}/' tempfile | jq -r .transaction_address >> upgrade_trx.txt + +# propose migration +./solido_v2/target/release/solido --output json --config ./solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json\ + migrate-state-to-v2 --developer-fee-share 1 \ + --treasury-fee-share 4 \ + --st-sol-appreciation-share 95 \ + --max-commission-percentage 5 > tempfile +awk '/{/,/}/' tempfile | jq -r .transaction_address >> upgrade_trx.txt + +# verify transaction +./solido_v2/scripts/operation.py \ + check-transactions --phase upgrade --transactions-path upgrade_trx.txt + +# wait for maintainers to remove validators, approve program update and migration +./solido_v2/target/release/solido --config ./solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + multisig approve-batch --transaction-addresses-path upgrade_trx.txt + +# start a new maintainer +./target/debug/solido --config ../solido_test.json \ + --keypair-path ../solido_old/tests/.keys/maintainer.json \ + run-maintainer --max-poll-interval-seconds 1 \ + --end-of-epoch-threshold 75 + +# add validators +solana-keygen pubkey ./solido_v1/tests/.keys/vote-account-key.json > validators.txt +./solido_v2/scripts/operation.py \ + add-validators --outfile adding_trx.txt \ + --vote-accounts validators.txt \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json + +# verify transaction +./solido_v2/scripts/operation.py \ + check-transactions --phase adding --transactions-path adding_trx.txt + +# batch sign transactions +./solido_v2/target/release/solido --config ./solido_config.json \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + multisig approve-batch --silent --transaction-addresses-path adding_trx.txt +# execute transactions one by one +./solido_v2/scripts/operation.py \ + execute-transactions --transactions adding_trx.txt \ + --keypair-path ./solido_v1/tests/.keys/maintainer.json \ + --phase adding + +############################################################################### +# EPOCH 4 # +############################################################################### + + +# try to withdraw +./solido_v2/target/release/solido --config ./solido_config.json withdraw --amount-st-sol 1.1 + +# withdraw developer some fee to self +spl-token transfer --from DEVELOPER_FEE_ADDRESS STSOL_MINT_ADDRESS \ + 0.0001 $(solana-keygen pubkey) --owner ~/developer_fee_key.json +# spl-token account-info --address DEVELOPER_FEE_ADDRESS diff --git a/scripts/operation.py b/scripts/operation.py new file mode 100755 index 000000000..f98d8c51a --- /dev/null +++ b/scripts/operation.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +""" +This script has multiple options to to interact with Solido +""" + + +import argparse +import json +import sys +import os.path +from typing import Any, Optional +import verify_transaction +from install_solido import install_solido + + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +from tests.util import solido, solana, run # type: ignore + + +def set_solido_cli_path(strData): + if os.path.isfile(strData): + os.environ["SOLPATH"] = strData + else: + print("Program does not exist: " + strData) + + +def eprint(*args: Any, **kwargs: Any) -> None: + print(*args, file=sys.stderr, **kwargs) + + +def get_signer() -> Any: + return run('solana-keygen', 'pubkey').strip() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + + subparsers = parser.add_subparsers(title='subcommands', dest="command") + + current_parser = subparsers.add_parser( + 'deactivate-validators', + help='Create and output multisig transactions to deactivate all validators', + ) + current_parser.add_argument( + "--keypair-path", + type=str, + help='Signer keypair or a ledger path', + required=True, + ) + current_parser.add_argument( + "--outfile", type=str, help='Output file path', required=True + ) + + current_parser = subparsers.add_parser( + 'load-program', + help='Write program from `program-filepath` to a random buffer address.', + ) + current_parser.add_argument( + "--program-filepath", help='/path/to/program.so', required=True + ) + current_parser.add_argument( + "--outfile", type=str, help='Output file path', required=True + ) + + current_parser = subparsers.add_parser( + 'add-validators', + help='Create add-validator transactions from file and print them to stdout', + ) + current_parser.add_argument( + "--vote-accounts", + type=str, + help='List of validator vote account file path', + required=True, + ) + current_parser.add_argument( + "--keypair-path", + type=str, + help='Signer keypair or a ledger path', + required=True, + ) + current_parser.add_argument( + "--outfile", type=str, help='Output file path', required=True + ) + + current_parser = subparsers.add_parser( + 'execute-transactions', help='Execute transactions from file one by one' + ) + current_parser.add_argument( + "--keypair-path", + type=str, + help='Signer keypair or a ledger path', + required=True, + ) + current_parser.add_argument( + "--transactions", + type=str, + help='Transactions file path. Each transaction per line', + required=True, + ) + current_parser.add_argument( + "--phase", + type=str, + help='Phase of deploy: preparation, deactivation, upgrade, adding', + required=True, + ) + + current_parser = subparsers.add_parser( + 'check-transactions', help='Check transactions from a file' + ) + current_parser.add_argument( + "--phase", + type=str, + help='Phase of deploy: preparation, deactivation, upgrade, adding', + required=True, + ) + current_parser.add_argument( + "--transactions-path", type=str, help='Path to transactions file', required=True + ) + + current_parser = subparsers.add_parser( + 'install-solido', + help='Install solido_v1 and solido_v2 for deploy actions', + ) + + current_parser = subparsers.add_parser('test', help='`Command for tests`') + + args = parser.parse_args() + + sys.argv.append('--verbose') + + install_solido() + with open(str(os.getenv("SOLIDO_CONFIG"))) as f: + config = json.load(f) + cluster = config.get("cluster") + if cluster: + os.environ['NETWORK'] = cluster + + if args.command == "deactivate-validators": + set_solido_cli_path(os.getenv("SOLIDO_V1")) + lido_state = solido('--config', os.getenv("SOLIDO_CONFIG"), 'show-solido') + validators = lido_state['solido']['validators']['entries'] + print("vote accounts:") + with open(args.outfile, 'w') as ofile: + for validator in validators: + print(validator['pubkey']) + result = solido( + '--config', + os.getenv("SOLIDO_CONFIG"), + 'deactivate-validator', + '--validator-vote-account', + validator['pubkey'], + keypair_path=args.keypair_path, + ) + address = result.get('transaction_address') + if address is None: + eprint(result) + else: + ofile.write(address + '\n') + + elif args.command == "add-validators": + set_solido_cli_path(os.getenv("SOLIDO_V2")) + print("vote accounts:") + with open(args.vote_accounts) as infile, open(args.outfile, 'w') as ofile: + for pubkey in infile: + print(pubkey) + result = solido( + '--config', + os.getenv("SOLIDO_CONFIG"), + 'add-validator', + '--validator-vote-account', + pubkey.strip(), + keypair_path=args.keypair_path, + ) + address = result.get('transaction_address') + if address is None: + eprint(result) + else: + ofile.write(address + '\n') + + elif args.command == "execute-transactions": + with open(args.transactions) as infile: + if args.phase == "deactivation": + set_solido_cli_path(os.getenv("SOLIDO_V1")) + elif args.phase == "adding": + print(args.phase) + set_solido_cli_path(os.getenv("SOLIDO_V2")) + else: + print("Unknown phase") + + for transaction in infile: + transaction = transaction.strip() + transaction_info = solido( + '--config', + os.getenv("SOLIDO_CONFIG"), + 'multisig', + 'show-transaction', + '--transaction-address', + transaction, + ) + if not transaction_info['did_execute']: + print(f"Executing transaction {transaction}") + result = solido( + '--config', + os.getenv("SOLIDO_CONFIG"), + 'multisig', + 'execute-transaction', + '--transaction-address', + transaction, + keypair_path=args.keypair_path, + ) + print(f"Transaction {transaction} executed") + + elif args.command == "load-program": + set_solido_cli_path(os.getenv("SOLIDO_V1")) + lido_state = solido('--config', os.getenv("SOLIDO_CONFIG"), 'show-solido') + write_result = solana( + '--output', + 'json', + 'program', + 'write-buffer', + '--buffer-authority', + lido_state['solido']['manager'], + args.program_filepath, + ) + write_result = json.loads(write_result) + + solana( + 'program', + 'set-buffer-authority', + '--new-buffer-authority', + lido_state['solido']['manager'], + write_result['buffer'], + ) + with open(args.outfile, 'w') as ofile: + ofile.write(write_result['buffer']) + + elif args.command == "check-transactions": + with open(args.transactions_path, 'r') as ifile: + if args.phase == "deactivation": + print(args.phase) + set_solido_cli_path(os.getenv("SOLIDO_V1")) + verify_transaction.verify_solido_state() + verify_transaction.verify_transactions(ifile) + elif args.phase == "preparation": + print(args.phase) + elif args.phase == "upgrade": + print(args.phase) + set_solido_cli_path(os.getenv("SOLIDO_V1")) + verify_transaction.verify_solido_state() + set_solido_cli_path(os.getenv("SOLIDO_V2")) + verify_transaction.verify_transactions(ifile) + elif args.phase == "adding": + print(args.phase) + set_solido_cli_path(os.getenv("SOLIDO_V2")) + verify_transaction.verify_solido_state() + verify_transaction.verify_transactions(ifile) + else: + print("Unknown phase") + elif args.command == "install-solido": + print("Install solido...") + else: + eprint("Unknown command %s" % args.command) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 000000000..ac2a467d3 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,2 @@ +solana==0.34.3 +solders==0.21.0 diff --git a/scripts/update_exchange_rate.py b/scripts/update_exchange_rate.py new file mode 100644 index 000000000..9c5f8708c --- /dev/null +++ b/scripts/update_exchange_rate.py @@ -0,0 +1,57 @@ +from solana.rpc.api import Client +from solders import sysvar +from solders.instruction import AccountMeta, Instruction +from solders.keypair import Keypair +from solders.pubkey import Pubkey +from solders.transaction import Transaction + +# need abs path for this +PAYER_PATH = "/TODO/PATH/TO.JSON" + +RPC = "https://api.mainnet-beta.solana.com" + +LIDO_PROGRAM_ID = Pubkey.from_string("CrX7kMhLC3cSsXJdT7JDgqrRVWGnUpX3gfEfxxU2NVLi") + +LIDO_STATE = Pubkey.from_string("49Yi1TKkNyYjPAFdR9LBvoHcUjuPX4Df5T5yv39w2XTn") +LIDO_RESERVE = Pubkey.from_string("3Kwv3pEAuoe4WevPB4rgMBTZndGDb53XT7qwQKnvHPfX") +STSOL_MINT = Pubkey.from_string("7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj") +LIDO_VALIDATOR_LIST = Pubkey.from_string("GL9kqRNUTUosW3RsDoXHCuXUZn73SgQQmBvtp1ng2co4") + +UPDATE_EXCHANGE_RATE_V2_IX_DATA = bytes([0x16]) # Note: not 0x17 like in main branch. Refer to V2.1 branch + +UPDATE_EXCHANGE_RATE_V2_IX_METAS = [ + AccountMeta(LIDO_STATE, False, True), + AccountMeta(LIDO_RESERVE, False, False), + AccountMeta(STSOL_MINT, False, False), + AccountMeta(LIDO_VALIDATOR_LIST, False, False), + AccountMeta(sysvar.CLOCK, False, False), + AccountMeta(sysvar.RENT, False, False), +] + +UPDATE_IX = Instruction(LIDO_PROGRAM_ID, UPDATE_EXCHANGE_RATE_V2_IX_DATA, UPDATE_EXCHANGE_RATE_V2_IX_METAS) + +def load_keypair(fname): + with open(fname, "r") as f: + return Keypair.from_json(f.read()) + +def make_tx(payer, rbh): + return Transaction.new_signed_with_payer( + [UPDATE_IX], + payer.pubkey(), + [payer], + rbh, + ) + +if __name__ == "__main__": + rpc = Client(RPC) + payer = load_keypair(PAYER_PATH) + + rbh = rpc.get_latest_blockhash().value.blockhash + tx = make_tx( + payer, + rbh, + ) + tx_bytes = bytes(tx) + sig = rpc.send_raw_transaction(tx_bytes).value + print(f"{sig}") + rpc.confirm_transaction(sig, commitment="finalized") diff --git a/scripts/validator_onboarding.py b/scripts/validator_onboarding.py index 72f59adc2..00a9df059 100644 --- a/scripts/validator_onboarding.py +++ b/scripts/validator_onboarding.py @@ -19,7 +19,6 @@ class ValidatorResponse(NamedTuple): validator_name: str keybase_username: str vote_account_address: Address - withdraw_authority_check: Address commission_check: str will_vote_check: str added_to_keybase_check: str diff --git a/scripts/verify_transaction.py b/scripts/verify_transaction.py new file mode 100755 index 000000000..3db887a7e --- /dev/null +++ b/scripts/verify_transaction.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 + +""" +This script has multiple options to to interact with Solido +""" + +import sys +import os.path +from typing import Any, Dict, Set, List + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(os.path.dirname(SCRIPT_DIR)) + +from tests.util import solido, solana, run # type: ignore + +Sample: Dict[str, Any] = { + 'solido_instance': '2i2crMWRb9nUY6HpDDp3R1XAXXB9UNdWAdtD9Kp9eUNT', # "solido_address": "49Yi1TKkNyYjPAFdR9LBvoHcUjuPX4Df5T5yv39w2XTn", + 'program_to_upgrade': '2QYdJZhBrg5wAvkVA98WM2JtTXngsTpBXSq2LXnVUa33', # solido_config.json : solido_program_id + 'program_data_address': 'HZe59cxGy7irFUtcmcUwkmvERrwyCUKaJQavwt7VrVTg', + 'buffer_address': '2LCfqfcQBjKEpvyA54vwAGaYTUXt1L13MwEsDbrzuJbw', # buffer adres account + 'validator_list': 'HDLRixNLF3PLBMfxhKgKxeZEDhA84RiRUSZFm2zwimeE', + 'maintainer_list': '2uLFh1Ec8NP1fftKD2MLnF12Kw4CTXNHhDtqsWVz7f9K', + 'developer_account': '5vgbVafXQiVb9ftDix1NadV7D6pgP5H9YPCaoKcPrBxZ', + 'reward_distribution': { + 'treasury_fee': 4, + 'developer_fee': 1, + 'st_sol_appreciation': 95, + }, + 'max_validators': 6700, + 'max_maintainers': 5000, + 'max_commission_percentage': 5, +} + + +ValidatorVoteAccSet = set() +VerificationStatus = True +ValidatorSetV1 = set() +ValidatorSetV2: Set[str] = set() # will be filled later +SolidoVersion = -1 +SolidoState = "Unknown state" +TransOrder: List[str] = list() + + +def printSolution(flag: bool) -> str: + if flag: + return " [OK]\n" + else: + global VerificationStatus + VerificationStatus = False + return " [BAD]\n" + + +def checkSolidoState(state: str) -> bool: + return SolidoState == state + + +def checkVoteInV1Set(address: str) -> bool: + return address in ValidatorSetV1 + + +def checkVoteInV2Set(address: str) -> bool: + return address in ValidatorSetV2 + + +def checkVoteUnic(address: str) -> bool: + if address not in ValidatorVoteAccSet: + ValidatorVoteAccSet.add(address) + return True + else: + return False + + +def ValidateSolidoState(state: str) -> str: + return ": Solido state " + state + printSolution(SolidoState == state) + + +def ValidateField(dataDict: Any, key: str) -> str: + value = dataDict.get(key) + retbuf = key + " " + str(value) + if key in dataDict.keys(): + retbuf += printSolution(value == Sample.get(key)) + else: + retbuf += printSolution(False) + return retbuf + + +def ValidateRewardField(dataDict: Any, key: str) -> str: + value = dataDict.get(key) + retbuf = key + " " + str(value) + if key in dataDict.keys(): + reward_distribution = Sample.get('reward_distribution') + if reward_distribution is not None: + sampleValue = reward_distribution.get(key) + if sampleValue != None: + retbuf += printSolution(value == sampleValue) + return retbuf + + retbuf += printSolution(False) + return retbuf + + +def ValidateDeactivateV1VoteAccount(dataDict: Any, key: str) -> str: + value = dataDict.get(key) + retbuf = key + " " + str(value) + if key in dataDict.keys(): + retbuf += printSolution(checkVoteUnic(value) and checkVoteInV1Set(value)) + else: + retbuf += printSolution(False) + return retbuf + + +def ValidateAddV2VoteAccount(dataDict: Any, key: str) -> str: + value = dataDict.get(key) + retbuf = key + " " + str(value) + if key in dataDict.keys(): + retbuf += printSolution(checkVoteUnic(value) and checkVoteInV2Set(value)) + else: + retbuf += printSolution(False) + return retbuf + + +def ValidateTransOrder(trans): + retbuf = "Transaction order " + if trans == "BpfLoaderUpgrade": + retbuf += "BpfLoaderUpgrade" + retbuf += printSolution(len(TransOrder) == 0) + elif trans == "MigrateStateToV2": + retbuf += "MigrateStateToV2" + retbuf += printSolution( + len(TransOrder) == 1 and TransOrder[0] == "BpfLoaderUpgrade" + ) + else: + retbuf += printSolution(False) + return retbuf + + +def verify_solido_state() -> None: + # get solido state + json_data = solido('--config', os.getenv("SOLIDO_CONFIG"), 'show-solido') + + # parse solido state + l1_keys = json_data.get('solido') + global SolidoVersion + SolidoVersion = l1_keys.get('lido_version') + validators = l1_keys.get('validators') + if validators != None: + for validator in validators.get('entries'): + vote_acc = validator.get('pubkey') + if validator.get('entry').get('active') == True: + ValidatorSetV1.add(vote_acc) + + # detect current state + global SolidoState + if SolidoVersion == 0: + if len(ValidatorSetV1) == 21: + SolidoState = "Deactivate validators" + elif len(ValidatorSetV1) == 0: + SolidoState = "Upgrade program" + else: + SolidoState = "Unknown state - solido version = " + SolidoState += str(SolidoVersion) + SolidoState += " active validators count = " + SolidoState += str(len(ValidatorSetV1)) + elif SolidoVersion == 1 and len(ValidatorSetV1) == 0: + SolidoState = "Add validators" + else: + SolidoState = "Unknown state - solido version = " + SolidoState += str(SolidoVersion) + SolidoState += " active validators count = " + SolidoState += str(len(ValidatorSetV1)) + + # output result + print("\nCurrent migration state: " + SolidoState) + + +def verify_transaction_data(json_data: Any) -> bool: + l1_keys = json_data['parsed_instruction'] + output_buf = "" + global VerificationStatus + VerificationStatus = True + if 'SolidoInstruction' in l1_keys.keys(): + output_buf += "SolidoInstruction " + l2_data = l1_keys['SolidoInstruction'] + if 'DeactivateValidator' in l2_data.keys(): + output_buf += "DeactivateValidator" + output_buf += ValidateSolidoState("Deactivate validators") + trans_data = l2_data['DeactivateValidator'] + output_buf += ValidateField(trans_data, 'solido_instance') + output_buf += ValidateField(trans_data, 'manager') + output_buf += ValidateDeactivateV1VoteAccount( + trans_data, 'validator_vote_account' + ) + elif 'AddValidator' in l2_data.keys(): + output_buf += "AddValidator" + output_buf += ValidateSolidoState("Add validators") + trans_data = l2_data['AddValidator'] + output_buf += ValidateField(trans_data, 'solido_instance') + output_buf += ValidateField(trans_data, 'manager') + output_buf += ValidateAddV2VoteAccount(trans_data, 'validator_vote_account') + elif 'MigrateStateToV2' in l2_data.keys(): + output_buf += ValidateTransOrder("MigrateStateToV2") + output_buf += ValidateSolidoState("Upgrade program") + trans_data = l2_data.get('MigrateStateToV2') + output_buf += ValidateField(trans_data, 'solido_instance') + output_buf += ValidateField(trans_data, 'manager') + output_buf += ValidateField(trans_data, 'validator_list') + output_buf += ValidateField(trans_data, 'maintainer_list') + output_buf += ValidateField(trans_data, 'developer_account') + output_buf += ValidateField(trans_data, 'max_maintainers') + output_buf += ValidateField(trans_data, 'max_validators') + output_buf += ValidateField(trans_data, 'max_commission_percentage') + + reward_distribution = trans_data.get('reward_distribution') + output_buf += ValidateRewardField(reward_distribution, 'treasury_fee') + output_buf += ValidateRewardField(reward_distribution, 'developer_fee') + output_buf += ValidateRewardField( + reward_distribution, 'st_sol_appreciation' + ) + else: + output_buf += "Unknown instruction\n" + VerificationStatus = False + elif 'BpfLoaderUpgrade' in l1_keys.keys(): + output_buf += ValidateTransOrder("BpfLoaderUpgrade") + TransOrder.append("BpfLoaderUpgrade") + output_buf += ValidateSolidoState("Upgrade program") + l2_data = l1_keys['BpfLoaderUpgrade'] + output_buf += ValidateField(l2_data, 'program_to_upgrade') + output_buf += ValidateField(l2_data, 'program_data_address') + output_buf += ValidateField(l2_data, 'buffer_address') + else: + output_buf += "Unknown instruction\n" + VerificationStatus = False + + print(output_buf) + return VerificationStatus + + +def verify_transactions(ifile): + Counter = 0 + Success = 0 + for transaction in ifile: + result = solido( + '--config', + os.getenv("SOLIDO_CONFIG"), + 'multisig', + 'show-transaction', + '--transaction-address', + transaction.strip(), + ) + Counter += 1 + print("Transaction #" + str(Counter) + ": " + transaction.strip()) + if verify_transaction_data(result): + Success += 1 + print( + "Summary: successfully verified " + + str(Success) + + " from " + + str(Counter) + + " transactions" + ) + + +if __name__ == '__main__': + print("main") diff --git a/scripts/verify_validator_submissions.py b/scripts/verify_validator_submissions.py index 15cc73328..4c317cef2 100755 --- a/scripts/verify_validator_submissions.py +++ b/scripts/verify_validator_submissions.py @@ -32,10 +32,10 @@ from validator_onboarding import print_ok, print_warn, print_error -SOLIDO_AUTHORIZED_WITHDAWER = 'GgrQiJ8s2pfHsfMbEFtNcejnzLegzZ16c9XtJ2X2FpuF' ST_SOL_MINT = '7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj' VOTE_PROGRAM = 'Vote111111111111111111111111111111111111111' SPL_TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' +MAX_VALIDATION_COMMISSION_PERCENTAGE = 5 def solana(*args: str) -> Any: @@ -85,10 +85,7 @@ def get_token_account(address: Address) -> Optional[TokenAccount]: try: process = subprocess.run(cmd, check=True, capture_output=True, encoding='utf-8') result = json.loads(process.stdout) - return TokenAccount( - mint_address=result['mint'], - state=result['state'], - ) + return TokenAccount(mint_address=result['mint'], state=result['state']) except subprocess.CalledProcessError: return None @@ -164,20 +161,23 @@ def check_validator_response( print_error('Vote account address does not hold a vote account.') return - if vote_account.authorized_withdrawer == SOLIDO_AUTHORIZED_WITHDAWER: - print_ok('Authorized withdrawer set to Solido.') - else: - print_error('Wrong authorized withdrawer.') - if vote_account.num_votes > 0: print_ok('Vote account has votes.') else: print_warn('Vote account has not voted yet.') - if vote_account.commission == 100: - print_ok('Vote account commission is 100%.') + if vote_account.commission <= MAX_VALIDATION_COMMISSION_PERCENTAGE: + print_ok( + 'Vote account commission is less than {}%.'.format( + MAX_VALIDATION_COMMISSION_PERCENTAGE + ) + ) else: - print_error('Vote account commission is not 100%.') + print_error( + 'Vote account commission is more than {}%.'.format( + MAX_VALIDATION_COMMISSION_PERCENTAGE + ) + ) validator_info = validators_by_identity.get(vote_account.validator_identity_address) if validator_info is None: diff --git a/solido_config.json b/solido_config.json new file mode 100755 index 000000000..1f6112343 --- /dev/null +++ b/solido_config.json @@ -0,0 +1,11 @@ +{ + "validator_list_address": "GL9kqRNUTUosW3RsDoXHCuXUZn73SgQQmBvtp1ng2co4", + "maintainer_list_address": "5dvtr16i34hwXuCtTNHXXJ5ojeidVPXbceN9pXxrE8bn", + "developer_fee_address": "5Y5LVTXbtMYsibjp9uQMmCyZbtSru8zktuxGPV9eHu3m", + "cluster": "https://api.mainnet-beta.solana.com", + "multisig_program_id": "AAHT26ecV3FEeFmL2gDZW6FfEqjPkghHbAkNZGqwT8Ww", + "multisig_address": "3cXyJbjoAUNLpQsFrFJTTTp8GD3uPeabYbsCVobkQpD1", + "solido_program_id": "CrX7kMhLC3cSsXJdT7JDgqrRVWGnUpX3gfEfxxU2NVLi", + "solido_address": "49Yi1TKkNyYjPAFdR9LBvoHcUjuPX4Df5T5yv39w2XTn", + "st_sol_mint": "7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj" +} diff --git a/testlib/Cargo.toml b/testlib/Cargo.toml index d6c42707a..39b59e264 100644 --- a/testlib/Cargo.toml +++ b/testlib/Cargo.toml @@ -6,7 +6,6 @@ name = "testlib" version = "1.2.0" [dependencies] -anker = { path = "../anker" } borsh = "0.9.3" lido = { path = "../program" } num-derive = "0.3" diff --git a/testlib/src/anker_context.rs b/testlib/src/anker_context.rs deleted file mode 100644 index 65b1e6227..000000000 --- a/testlib/src/anker_context.rs +++ /dev/null @@ -1,774 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -//! Test context for testing Anker, the Anchor Protocol integration. - -use solana_program::borsh::try_from_slice_unchecked; -use solana_program::program_pack::Pack; -use solana_sdk::pubkey::Pubkey; -use solana_sdk::signature::{Keypair, Signer}; -use solana_sdk::transport; - -use anker::instruction; -use anker::token::{BLamports, MicroUst}; -use lido::token::Lamports; -use lido::token::StLamports; -use spl_token_swap::curve::base::{CurveType, SwapCurve}; -use spl_token_swap::curve::constant_product::ConstantProductCurve; -use spl_token_swap::instruction::Swap; - -use crate::solido_context::send_transaction; -use crate::solido_context::{self}; -use anker::{ - find_reserve_authority, find_st_sol_reserve_account, - state::{POOL_PRICE_MIN_SAMPLE_DISTANCE, POOL_PRICE_NUM_SAMPLES}, - wormhole::TerraAddress, -}; - -// Program id for the Anker program. Only used for tests. -solana_program::declare_id!("Anker111111111111111111111111111111111111117"); - -pub struct TokenPoolContext { - pub swap_account: Keypair, - pub mint_address: Pubkey, - pub token_address: Pubkey, - pub fee_address: Pubkey, - pub token_a: Pubkey, - pub token_b: Pubkey, - - pub ust_mint_authority: Keypair, - pub ust_mint_address: Pubkey, -} - -impl TokenPoolContext { - pub fn get_token_pool_authority(&self) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[&self.swap_account.pubkey().to_bytes()[..]], - &anker::orca_token_swap_v2::id(), - ) - } - - /// Mint `amount` UST to `account`. - pub async fn mint_ust( - &self, - solido_context: &mut solido_context::Context, - account: &Pubkey, - amount: MicroUst, - ) { - let mint_instruction = spl_token::instruction::mint_to( - &spl_token::id(), - &self.ust_mint_address, - account, - &self.ust_mint_authority.pubkey(), - &[], - amount.0, - ) - .expect("Failed to generate UST mint instruction."); - send_transaction( - &mut solido_context.context, - &[mint_instruction], - vec![&self.ust_mint_authority], - ) - .await - .expect("Failed to mint UST tokens."); - } - - /// Returns the pair of tokens from the token pool that correspond to the - /// UST and stSOL, respectively. - pub async fn get_ust_stsol_addresses( - &self, - solido_context: &mut solido_context::Context, - ) -> (Pubkey, Pubkey) { - let token_a_account = solido_context.get_account(self.token_a).await; - let token_a = - spl_token::state::Account::unpack_from_slice(token_a_account.data.as_slice()).unwrap(); - - if token_a.mint == self.ust_mint_address { - (self.token_a, self.token_b) - } else { - (self.token_b, self.token_a) - } - } - - // Put StSOL and UST to the liquidity provider - pub async fn provide_liquidity( - &self, - solido_context: &mut solido_context::Context, - st_sol_amount: StLamports, - ust_amount: MicroUst, - ) { - let (ust_address, st_sol_address) = self.get_ust_stsol_addresses(solido_context).await; - - // Transfer some UST and StSOL to the pool. - self.mint_ust(solido_context, &ust_address, ust_amount) - .await; - - let solido = solido_context.get_solido().await; - let sol_amount = solido - .exchange_rate - .exchange_st_sol(st_sol_amount) - .expect("Some StSol should have been minted at this point."); - let (st_sol_keypair, token_st_sol) = solido_context.deposit(sol_amount).await; - let balance = solido_context.get_st_sol_balance(token_st_sol).await; - solido_context - .transfer_spl_token(&token_st_sol, &st_sol_address, &st_sol_keypair, balance.0) - .await; - } - - // Initialize token pool. - pub async fn initialize_token_pool(&mut self, solido_context: &mut solido_context::Context) { - self.provide_liquidity( - solido_context, - StLamports(10_000_000_000), - MicroUst(10_000_000_000), - ) - .await; - let fees = spl_token_swap::curve::fees::Fees { - trade_fee_numerator: 0, - trade_fee_denominator: 10, - owner_trade_fee_numerator: 0, - owner_trade_fee_denominator: 10, - owner_withdraw_fee_numerator: 0, - owner_withdraw_fee_denominator: 10, - host_fee_numerator: 0, - host_fee_denominator: 10, - }; - let swap_curve = SwapCurve { - curve_type: CurveType::ConstantProduct, - calculator: Box::new(ConstantProductCurve), - }; - - let (authority_pubkey, authority_bump_seed) = Pubkey::find_program_address( - &[&self.swap_account.pubkey().to_bytes()[..]], - &anker::orca_token_swap_v2::id(), - ); - - let pool_instruction = spl_token_swap::instruction::initialize( - &anker::orca_token_swap_v2::id(), - &spl_token::id(), - &self.swap_account.pubkey(), - &authority_pubkey, - &self.token_a, - &self.token_b, - &self.mint_address, - &self.fee_address, - &self.token_address, - authority_bump_seed, - fees, - swap_curve, - ) - .expect("Failed to create token pool initialization instruction."); - - send_transaction( - &mut solido_context.context, - &[pool_instruction], - vec![&self.swap_account], - ) - .await - .expect("Failed to initialize token pool."); - } - - /// Get the Token Swap Pool authority. - pub fn get_authority(&self) -> Pubkey { - let (authority, _bump_seed) = Pubkey::find_program_address( - &[&self.swap_account.pubkey().to_bytes()[..]], - &anker::orca_token_swap_v2::id(), - ); - authority - } -} - -pub struct Context { - pub solido_context: solido_context::Context, - pub anker: Pubkey, - pub b_sol_mint: Pubkey, - pub b_sol_mint_authority: Pubkey, - pub st_sol_reserve: Pubkey, - pub ust_reserve: Pubkey, - - pub token_pool_context: TokenPoolContext, - pub rewards_owner: Keypair, - pub terra_rewards_destination: TerraAddress, - pub reserve_authority: Pubkey, - - pub token_swap_program_id: Pubkey, -} - -const INITIAL_DEPOSIT: Lamports = Lamports(1_000_000_000); - -impl Context { - pub async fn new_with_undefined_exchange_rate() -> Self { - let mut solido_context = solido_context::Context::new_with_maintainer().await; - let (anker, _seed) = anker::find_instance_address(&id(), &solido_context.solido.pubkey()); - - let (st_sol_reserve, _seed) = anker::find_st_sol_reserve_account(&id(), &anker); - let (ust_reserve, _seed) = anker::find_ust_reserve_account(&id(), &anker); - let (reserve_authority, _seed) = anker::find_reserve_authority(&id(), &anker); - let (b_sol_mint_authority, _seed) = anker::find_mint_authority(&id(), &anker); - - let b_sol_mint = solido_context.create_mint(b_sol_mint_authority).await; - let payer = solido_context.context.payer.pubkey(); - - let token_pool_context = setup_token_pool(&mut solido_context).await; - - let rewards_owner = solido_context.deterministic_keypair.new_keypair(); - let terra_rewards_destination = TerraAddress::default(); - - // In the tests, by default we set no bound on slippage when selling rewards. - // The min out amount is 0% of the expected amount. - let sell_rewards_min_out_bps = 0; - - send_transaction( - &mut solido_context.context, - &[instruction::initialize( - &id(), - &instruction::InitializeAccountsMeta { - fund_rent_from: payer, - anker, - solido: solido_context.solido.pubkey(), - solido_program: solido_context::id(), - wormhole_core_bridge_program_id: Pubkey::new_unique(), - wormhole_token_bridge_program_id: Pubkey::new_unique(), - st_sol_mint: solido_context.st_sol_mint, - b_sol_mint, - st_sol_reserve_account: st_sol_reserve, - ust_reserve_account: ust_reserve, - reserve_authority, - token_swap_pool: token_pool_context.swap_account.pubkey(), - ust_mint: token_pool_context.ust_mint_address, - }, - terra_rewards_destination.clone(), - sell_rewards_min_out_bps, - )], - vec![], - ) - .await - .expect("Failed to initialize Anker instance."); - - Self { - solido_context, - anker, - b_sol_mint, - b_sol_mint_authority, - st_sol_reserve, - ust_reserve, - token_pool_context, - rewards_owner, - terra_rewards_destination, - reserve_authority, - token_swap_program_id: anker::orca_token_swap_v2::id(), - } - } - - /// Create a new test context, where Solido has some balance, and the exchange - /// rate has been updated once. - pub async fn new() -> Self { - let mut ctx = Context::new_with_undefined_exchange_rate().await; - - ctx.solido_context.deposit(INITIAL_DEPOSIT).await; - ctx.solido_context.advance_to_normal_epoch(0); - ctx.solido_context.update_exchange_rate().await; - - ctx - } - - // Start a new Anker context with `amount` Lamports donated to Solido's - // reserve. Also update the exchange rate. Usually used when testing a - // different 1:1 exchange rate. - pub async fn new_different_exchange_rate(amount: Lamports) -> Context { - let mut context = Context::new().await; - context - .solido_context - .fund(context.solido_context.reserve_address, amount) - .await; - context.solido_context.advance_to_normal_epoch(1); - context.solido_context.update_exchange_rate().await; - context - } - - pub async fn initialize_token_pool_and_deposit(&mut self, deposit_amount: Lamports) { - self.token_pool_context - .initialize_token_pool(&mut self.solido_context) - .await; - self.deposit(deposit_amount).await; - // Donate something to Solido's reserve so we can see some rewards. - self.solido_context - .fund(self.solido_context.reserve_address, deposit_amount) - .await; - // Update the exchange rate so we see some rewards. - self.solido_context.advance_to_normal_epoch(1); - self.solido_context.update_exchange_rate().await; - } - - /// Create a new SPL token account holding bSOL, return its address. - pub async fn create_b_sol_account(&mut self, owner: Pubkey) -> Pubkey { - self.solido_context - .create_spl_token_account(self.b_sol_mint, owner) - .await - } - - /// Deposit some of the stSOL in the `from_st_sol` account, get bSOL. - /// - /// Returns the resulting bSOL account. - pub async fn try_deposit_st_sol( - &mut self, - user: &Keypair, - from_st_sol: Pubkey, - amount: StLamports, - ) -> transport::Result { - let recipient = self.create_b_sol_account(user.pubkey()).await; - - send_transaction( - &mut self.solido_context.context, - &[instruction::deposit( - &id(), - &instruction::DepositAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - from_account: from_st_sol, - user_authority: user.pubkey(), - to_reserve_account: self.st_sol_reserve, - b_sol_user_account: recipient, - b_sol_mint: self.b_sol_mint, - b_sol_mint_authority: self.b_sol_mint_authority, - }, - amount, - )], - vec![user], - ) - .await?; - - Ok(recipient) - } - - /// Deposit `amount` into Solido to get stSOL, deposit that into Anker to get bSOL. - /// - /// Returns the owner, and the bSOL account. - pub async fn try_deposit(&mut self, amount: Lamports) -> transport::Result<(Keypair, Pubkey)> { - // Note, we use `deposit` here, not `try_deposit`, because we assume in these - // tests that the Solido part does not fail. If we intentionally make a transaction - // fail, it should fail when calling Anker, not Solido. - let (user, st_sol_account) = self.solido_context.deposit(amount).await; - let balance = self.solido_context.get_st_sol_balance(st_sol_account).await; - let b_sol_account = self - .try_deposit_st_sol(&user, st_sol_account, balance) - .await?; - Ok((user, b_sol_account)) - } - - /// Deposit `amount` into Solido to get stSOL, deposit that into Anker to get bSOL. - /// - /// Returns the owner, and the bSOL account. - pub async fn deposit(&mut self, amount: Lamports) -> (Keypair, Pubkey) { - self.try_deposit(amount) - .await - .expect("Failed to call Deposit on Anker instance.") - } - - /// Create a new stSOL account owned by the user, and withdraw into it. - pub async fn try_withdraw( - &mut self, - user: &Keypair, - b_sol_account: Pubkey, - amount: BLamports, - ) -> transport::Result { - let recipient = self - .solido_context - .create_st_sol_account(user.pubkey()) - .await; - - send_transaction( - &mut self.solido_context.context, - &[instruction::withdraw( - &id(), - &instruction::WithdrawAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - from_b_sol_account: b_sol_account, - from_b_sol_authority: user.pubkey(), - to_st_sol_account: recipient, - reserve_account: self.st_sol_reserve, - reserve_authority: self.reserve_authority, - b_sol_mint: self.b_sol_mint, - }, - amount, - )], - vec![user], - ) - .await?; - - Ok(recipient) - } - - /// Create a new stSOL account owned by the user, and withdraw into it. - pub async fn withdraw( - &mut self, - user: &Keypair, - b_sol_account: Pubkey, - amount: BLamports, - ) -> Pubkey { - self.try_withdraw(user, b_sol_account, amount) - .await - .expect("Failed to call Withdraw on Anker instance.") - } - - /// Get the bSOL balance from an SPL token account. - pub async fn get_b_sol_balance(&mut self, address: Pubkey) -> BLamports { - let token_account = self.solido_context.get_account(address).await; - let account_info: spl_token::state::Account = - spl_token::state::Account::unpack_from_slice(token_account.data.as_slice()).unwrap(); - - assert_eq!(account_info.mint, self.b_sol_mint); - BLamports(account_info.amount) - } - - /// Swap StSol for UST - pub async fn swap_st_sol_for_ust( - &mut self, - source: &Pubkey, - destination: &Pubkey, - authority: &Keypair, - amount_in: StLamports, - minimum_amount_out: MicroUst, - ) { - let (ust_address, st_sol_address) = self - .token_pool_context - .get_ust_stsol_addresses(&mut self.solido_context) - .await; - let swap_instruction = spl_token_swap::instruction::swap( - &self.token_swap_program_id, - &spl_token::id(), - &self.token_pool_context.swap_account.pubkey(), - &self.token_pool_context.get_token_pool_authority().0, - &authority.pubkey(), - source, - &st_sol_address, - &ust_address, - destination, - &self.token_pool_context.mint_address, - &self.token_pool_context.fee_address, - None, - Swap { - amount_in: amount_in.0, - minimum_amount_out: minimum_amount_out.0, - }, - ) - .expect("Could not create swap instruction."); - send_transaction( - &mut self.solido_context.context, - &[swap_instruction], - vec![authority], - ) - .await - .expect("Failed to swap StSol for UST tokens."); - } - - pub async fn sell_rewards(&mut self) { - self.try_sell_rewards() - .await - .expect("Failed to call SellRewards on Anker instance.") - } - - pub async fn try_sell_rewards(&mut self) -> transport::Result<()> { - let (st_sol_reserve_account, _reserve_account_bump_seed) = - find_st_sol_reserve_account(&id(), &self.anker); - let (reserve_authority, _reserve_authority_bump_seed) = - find_reserve_authority(&id(), &self.anker); - let (token_swap_authority, _token_pool_authority_bump_seed) = - self.token_pool_context.get_token_pool_authority(); - - let (ust_address, st_sol_address) = self - .token_pool_context - .get_ust_stsol_addresses(&mut self.solido_context) - .await; - - send_transaction( - &mut self.solido_context.context, - &[instruction::sell_rewards( - &id(), - &instruction::SellRewardsAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - st_sol_reserve_account, - b_sol_mint: self.b_sol_mint, - token_swap_pool: self.token_pool_context.swap_account.pubkey(), - pool_st_sol_account: st_sol_address, - pool_ust_account: ust_address, - pool_mint: self.token_pool_context.mint_address, - st_sol_mint: self.solido_context.st_sol_mint, - ust_mint: self.token_pool_context.ust_mint_address, - pool_fee_account: self.token_pool_context.fee_address, - token_swap_authority, - reserve_authority, - ust_reserve_account: self.ust_reserve, - token_swap_program_id: self.token_swap_program_id, - }, - )], - vec![], - ) - .await - } - - /// Call the `SendRewards` instruction. Note that this will fail, because we - /// don't have the Wormhole programs available in the test context. But we - /// can still test everything up to the point where we call Wormhole. - pub async fn try_send_rewards(&mut self) -> transport::Result<()> { - let solido_address = self.solido_context.solido.pubkey(); - - let (anker_instance, _anker_bump_seed) = - anker::find_instance_address(&id(), &solido_address); - - let (ust_reserve_account, _ust_reserve_bump_seed) = - anker::find_ust_reserve_account(&id(), &anker_instance); - - let (reserve_authority, _reserve_authority_bump_seed) = - find_reserve_authority(&id(), &anker_instance); - - let anker = self.get_anker().await; - let message = self.solido_context.deterministic_keypair.new_keypair(); - - let transfer_args = anker::wormhole::WormholeTransferArgs::new( - anker.wormhole_parameters.token_bridge_program_id, - anker.wormhole_parameters.core_bridge_program_id, - self.token_pool_context.ust_mint_address, - self.solido_context.context.payer.pubkey(), - ust_reserve_account, - reserve_authority, - message.pubkey(), - ); - - let wormhole_nonce = 1; - - send_transaction( - &mut self.solido_context.context, - &[instruction::send_rewards( - &id(), - &instruction::SendRewardsAccountsMeta { - anker: anker_instance, - solido: solido_address, - reserve_authority, - wormhole_token_bridge_program_id: transfer_args.token_bridge_program_id, - wormhole_core_bridge_program_id: transfer_args.core_bridge_program_id, - payer: transfer_args.payer, - config_key: transfer_args.config_key, - ust_reserve_account, - wrapped_meta_key: transfer_args.wrapped_meta_key, - ust_mint: self.token_pool_context.ust_mint_address, - authority_signer_key: transfer_args.authority_signer_key, - bridge_config: transfer_args.bridge_config, - message: message.pubkey(), - emitter_key: transfer_args.emitter_key, - sequence_key: transfer_args.sequence_key, - fee_collector_key: transfer_args.fee_collector_key, - }, - wormhole_nonce, - )], - vec![&message], - ) - .await - } - - /// Return the value of the given amount of stSOL in SOL. - pub async fn exchange_st_sol(&mut self, amount: StLamports) -> Lamports { - let solido = self.solido_context.get_solido().await; - solido.exchange_rate.exchange_st_sol(amount).unwrap() - } - - /// Return the current amount of bSOL in existence. - pub async fn get_b_sol_supply(&mut self) -> BLamports { - let mint_account = self.solido_context.get_account(self.b_sol_mint).await; - let mint: spl_token::state::Mint = - spl_token::state::Mint::unpack_from_slice(mint_account.data.as_slice()).unwrap(); - BLamports(mint.supply) - } - - /// Return the `MicroUst` balance of the account in `address`. - pub async fn get_ust_balance(&mut self, address: Pubkey) -> MicroUst { - let ust_account = self.solido_context.get_account(address).await; - let ust_spl_account: spl_token::state::Account = - spl_token::state::Account::unpack_from_slice(ust_account.data.as_slice()) - .expect("UST account does not exist"); - MicroUst(ust_spl_account.amount) - } - - // Create a new UST token account. - pub async fn create_ust_token_account(&mut self, owner: Pubkey) -> Pubkey { - self.solido_context - .create_spl_token_account(self.token_pool_context.ust_mint_address, owner) - .await - } - - pub async fn try_change_terra_rewards_destination( - &mut self, - manager: &Keypair, - terra_rewards_destination: TerraAddress, - ) -> transport::Result<()> { - send_transaction( - &mut self.solido_context.context, - &[instruction::change_terra_rewards_destination( - &id(), - &instruction::ChangeTerraRewardsDestinationAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - manager: manager.pubkey(), - }, - terra_rewards_destination, - )], - vec![manager], - ) - .await - } - - pub async fn try_change_token_swap_pool( - &mut self, - token_swap_pool: Pubkey, - ) -> transport::Result<()> { - let anker = self.get_anker().await; - send_transaction( - &mut self.solido_context.context, - &[instruction::change_token_swap_pool( - &id(), - &instruction::ChangeTokenSwapPoolAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - manager: self.solido_context.manager.pubkey(), - current_token_swap_pool: anker.token_swap_pool, - new_token_swap_pool: token_swap_pool, - }, - )], - vec![&self.solido_context.manager], - ) - .await?; - Ok(()) - } - - pub async fn try_change_sell_rewards_min_out_bps( - &mut self, - manager: &Keypair, - sell_rewards_min_out_bps: u64, - ) -> transport::Result<()> { - send_transaction( - &mut self.solido_context.context, - &[instruction::change_sell_rewards_min_out_bps( - &id(), - &instruction::ChangeSellRewardsMinOutBpsAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - manager: manager.pubkey(), - }, - sell_rewards_min_out_bps, - )], - vec![manager], - ) - .await - } - - /// Return the `MicroUst` balance of the account in `address`. - pub async fn try_fetch_pool_price(&mut self) -> transport::Result<()> { - let (ust_address, st_sol_address) = self - .token_pool_context - .get_ust_stsol_addresses(&mut self.solido_context) - .await; - - send_transaction( - &mut self.solido_context.context, - &[instruction::fetch_pool_price( - &id(), - &instruction::FetchPoolPriceAccountsMeta { - anker: self.anker, - solido: self.solido_context.solido.pubkey(), - token_swap_pool: self.token_pool_context.swap_account.pubkey(), - pool_st_sol_account: st_sol_address, - pool_ust_account: ust_address, - }, - )], - vec![], - ) - .await - } - - pub async fn fetch_pool_price(&mut self) { - self.try_fetch_pool_price() - .await - .expect("Could not send transaction to fetch pool price.") - } - - pub async fn fill_historical_st_sol_price_array(&mut self) { - for _ in 0..POOL_PRICE_NUM_SAMPLES { - let current_slot = self.solido_context.get_clock().await.slot; - self.fetch_pool_price().await; - self.solido_context - .context - .warp_to_slot(current_slot + POOL_PRICE_MIN_SAMPLE_DISTANCE) - .unwrap(); - } - } - - pub async fn get_anker(&mut self) -> anker::state::Anker { - let anker_account = self.solido_context.get_account(self.anker).await; - // This returns a Result because it can cause an IO error, but that should - // not happen in the test environment. (And if it does, then the test just - // fails.) - try_from_slice_unchecked::(anker_account.data.as_slice()).unwrap() - } -} - -/// Create a new token pool using `CurveType::ConstantProduct`. -/// -/// The stake pool is not initialized at the end of this function. To -/// initialize the token swap instance, it requires funded token pairs on the -/// liquidity pool. -/// To get a new Context with an initialized token pool, call -/// `Context::new_with_token_pool_rewards`. -pub async fn setup_token_pool(solido_context: &mut solido_context::Context) -> TokenPoolContext { - let admin = solido_context.deterministic_keypair.new_keypair(); - - // When packing the SwapV1 structure, `SwapV1::pack(swap_info, &mut - // dst[1..])` is called. But the program also wants the size of the data - // to be `spl_token_swap::state::SwapV1::LEN`. `LATEST_LEN` is 1 + - // SwapV1::LEN. - let swap_account = solido_context - .create_account( - &anker::orca_token_swap_v2::id(), - spl_token_swap::state::SwapVersion::LATEST_LEN, - ) - .await; - - let (authority_pubkey, _authority_bump_seed) = Pubkey::find_program_address( - &[&swap_account.pubkey().to_bytes()[..]], - &anker::orca_token_swap_v2::id(), - ); - - let pool_mint_pubkey = solido_context.create_mint(authority_pubkey).await; - let pool_token_pubkey = solido_context - .create_spl_token_account(pool_mint_pubkey, admin.pubkey()) - .await; - let pool_fee_pubkey = solido_context - .create_spl_token_account(pool_mint_pubkey, admin.pubkey()) - .await; - - // Create UST token - let ust_mint_authority = solido_context.deterministic_keypair.new_keypair(); - let ust_mint_address = solido_context - .create_mint(ust_mint_authority.pubkey()) - .await; - - // UST and StSOL token accounts for the pool. - let token_a = solido_context - .create_spl_token_account(ust_mint_address, authority_pubkey) - .await; - let token_b = solido_context - .create_spl_token_account(solido_context.st_sol_mint, authority_pubkey) - .await; - - TokenPoolContext { - swap_account, - mint_address: pool_mint_pubkey, - token_address: pool_token_pubkey, - fee_address: pool_fee_pubkey, - token_a, - token_b, - ust_mint_authority, - ust_mint_address, - } -} diff --git a/testlib/src/lib.rs b/testlib/src/lib.rs index ca94919f7..660c0c1d7 100644 --- a/testlib/src/lib.rs +++ b/testlib/src/lib.rs @@ -1,6 +1,4 @@ // SPDX-FileCopyrightText: 2021 Chorus One AG // SPDX-License-Identifier: GPL-3.0 -pub mod anker_context; pub mod solido_context; -mod util; diff --git a/testlib/src/solido_context.rs b/testlib/src/solido_context.rs index 4e41e75b8..ee53ca12c 100644 --- a/testlib/src/solido_context.rs +++ b/testlib/src/solido_context.rs @@ -3,6 +3,7 @@ //! Holds a test context, which makes it easier to test with a Solido instance set up. +use borsh::BorshSerialize; use num_traits::cast::FromPrimitive; use rand::prelude::StdRng; use rand::SeedableRng; @@ -25,21 +26,19 @@ use solana_sdk::transport; use solana_sdk::transport::TransportError; use solana_vote_program::vote_instruction; use solana_vote_program::vote_state::{VoteInit, VoteState}; -use std::sync::Once; -use anker::error::AnkerError; -use lido::account_map::PubkeyAndEntry; use lido::processor::StakeType; use lido::stake_account::StakeAccount; use lido::token::{Lamports, StLamports}; use lido::{error::LidoError, instruction, RESERVE_ACCOUNT, STAKE_AUTHORITY}; use lido::{ - state::{FeeRecipients, Lido, RewardDistribution, Validator}, + state::{ + AccountList, FeeRecipients, Lido, ListEntry, Maintainer, RewardDistribution, StakeDeposit, + Validator, + }, MINT_AUTHORITY, }; -static INIT: Once = Once::new(); - pub struct DeterministicKeypairGen { rng: StdRng, } @@ -85,6 +84,8 @@ pub struct Context { pub st_sol_mint: Pubkey, pub maintainer: Option, pub validator: Option, + pub validator_list: Keypair, + pub maintainer_list: Keypair, pub treasury_st_sol_account: Pubkey, pub developer_st_sol_account: Pubkey, @@ -100,6 +101,7 @@ pub struct Context { pub struct ValidatorAccounts { pub node_account: Keypair, pub vote_account: Pubkey, + pub withdraw_authority: Keypair, } /// Sign and send a transaction with a fresh block hash. @@ -172,31 +174,16 @@ pub async fn send_transaction( ), None => println!("This error is not a known Solido error."), } - // Even though this is the Solido context, we also check for the Anker error, - // because the Anker context builds on this. - match AnkerError::from_u32(error_code) { - Some(err) => println!( - "If this error originated from Anker, it was this variant: {:?}", - err, - ), - None => println!("This error is not a known Anker error."), - } } result } -/// The different ways to stake some amount from the reserve. -pub enum StakeDeposit { - /// Stake into a new stake account, and delegate the new account. - /// - /// This consumes the end seed of the validator's stake accounts. - Append, - - /// Stake into temporary stake account, and immediately merge it. - /// - /// This merges into the stake account at `end_seed - 1`. - Merge, +#[derive(PartialEq, Debug)] +pub struct SolidoWithLists { + pub lido: Lido, + pub validators: AccountList, + pub maintainers: AccountList, } impl Context { @@ -207,6 +194,8 @@ impl Context { let mut deterministic_keypair = DeterministicKeypairGen::new(); let manager = deterministic_keypair.new_keypair(); let solido = deterministic_keypair.new_keypair(); + let validator_list = deterministic_keypair.new_keypair(); + let maintainer_list = deterministic_keypair.new_keypair(); let reward_distribution = RewardDistribution { treasury_fee: 3, @@ -235,30 +224,14 @@ impl Context { crate::solido_context::id(), processor!(lido::processor::process), ); - program_test.add_program( - "anker", - crate::anker_context::id(), - processor!(anker::processor::process), - ); - - // Add the actual Orca token swap program, so we test against the real thing. - // If we don't have it locally, download it from the chain. - INIT.call_once(|| { - // call it once so that Solana rpc would not block us by IP - crate::util::ensure_orca_program_exists(); - }); - program_test.add_program("orca_token_swap_v2", anker::orca_token_swap_v2::id(), None); - program_test.add_program( - "orca_token_swap_v2", - anker::orca_token_swap_v2_fake::id(), - None, - ); let mut result = Self { context: program_test.start_with_context().await, nonce: 0, manager, solido, + validator_list, + maintainer_list, st_sol_mint: Pubkey::default(), maintainer: None, validator: None, @@ -283,12 +256,18 @@ impl Context { result.create_st_sol_account(developer_owner.pubkey()).await; let max_validators = 10_000; - let max_maintainers = 1000; - let solido_size = Lido::calculate_size(max_validators, max_maintainers); + let max_maintainers = 10_000; + let solido_size = Lido::calculate_size(); let rent = result.context.banks_client.get_rent().await.unwrap(); let rent_solido = rent.minimum_balance(solido_size); let rent_reserve = rent.minimum_balance(0); + let validator_list_size = AccountList::::required_bytes(max_validators); + let rent_validator_list = rent.minimum_balance(validator_list_size); + + let maintainer_list_size = AccountList::::required_bytes(max_maintainers); + let rent_maintainer_list = rent.minimum_balance(maintainer_list_size); + result .fund(result.reserve_address, Lamports(rent_reserve)) .await; @@ -304,6 +283,20 @@ impl Context { solido_size as u64, &id(), ), + system_instruction::create_account( + &payer, + &result.validator_list.pubkey(), + rent_validator_list, + validator_list_size as u64, + &id(), + ), + system_instruction::create_account( + &payer, + &result.maintainer_list.pubkey(), + rent_maintainer_list, + maintainer_list_size as u64, + &id(), + ), instruction::initialize( &id(), result.reward_distribution.clone(), @@ -317,10 +310,16 @@ impl Context { treasury_account: result.treasury_st_sol_account, developer_account: result.developer_st_sol_account, reserve_account: result.reserve_address, + validator_list: result.validator_list.pubkey(), + maintainer_list: result.maintainer_list.pubkey(), }, ), ], - vec![&result.solido], + vec![ + &result.solido, + &result.validator_list, + &result.maintainer_list, + ], ) .await .expect("Failed to initialize Solido instance."); @@ -658,10 +657,11 @@ impl Context { &mut self.context, &[lido::instruction::add_maintainer( &id(), - &lido::instruction::AddMaintainerMeta { + &lido::instruction::AddMaintainerMetaV2 { lido: self.solido.pubkey(), manager: self.manager.pubkey(), maintainer, + maintainer_list: self.maintainer_list.pubkey(), }, )], vec![&self.manager], @@ -679,15 +679,19 @@ impl Context { } pub async fn try_remove_maintainer(&mut self, maintainer: Pubkey) -> transport::Result<()> { + let solido = self.get_solido().await; + let maintainer_index = solido.maintainers.position(&maintainer).unwrap(); send_transaction( &mut self.context, &[lido::instruction::remove_maintainer( &id(), - &lido::instruction::RemoveMaintainerMeta { + &lido::instruction::RemoveMaintainerMetaV2 { lido: self.solido.pubkey(), manager: self.manager.pubkey(), maintainer, + maintainer_list: self.maintainer_list.pubkey(), }, + maintainer_index, )], vec![&self.manager], ) @@ -706,6 +710,7 @@ impl Context { lido: self.solido.pubkey(), manager: self.manager.pubkey(), validator_vote_account: accounts.vote_account, + validator_list: self.validator_list.pubkey(), }, )], vec![&self.manager], @@ -716,10 +721,11 @@ impl Context { /// Create a new key pair and add it as maintainer. pub async fn add_validator(&mut self) -> ValidatorAccounts { let node_account = self.deterministic_keypair.new_keypair(); + let withdraw_authority = self.deterministic_keypair.new_keypair(); let vote_account = self .create_vote_account( &node_account, - Pubkey::new_unique(), + withdraw_authority.pubkey(), self.max_commission_percentage, ) .await; @@ -727,6 +733,7 @@ impl Context { let accounts = ValidatorAccounts { node_account, vote_account, + withdraw_authority, }; self.try_add_validator(&accounts) @@ -737,15 +744,19 @@ impl Context { } pub async fn deactivate_validator(&mut self, vote_account: Pubkey) { + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&vote_account).unwrap(); send_transaction( &mut self.context, &[lido::instruction::deactivate_validator( &id(), - &lido::instruction::DeactivateValidatorMeta { + &lido::instruction::DeactivateValidatorMetaV2 { lido: self.solido.pubkey(), manager: self.manager.pubkey(), validator_vote_account_to_deactivate: vote_account, + validator_list: self.validator_list.pubkey(), }, + validator_index, )], vec![&self.manager], ) @@ -754,14 +765,18 @@ impl Context { } pub async fn try_remove_validator(&mut self, vote_account: Pubkey) -> transport::Result<()> { + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&vote_account).unwrap(); send_transaction( &mut self.context, &[lido::instruction::remove_validator( &id(), - &lido::instruction::RemoveValidatorMeta { + &lido::instruction::RemoveValidatorMetaV2 { lido: self.solido.pubkey(), validator_vote_account_to_remove: vote_account, + validator_list: self.validator_list.pubkey(), }, + validator_index, )], vec![], ) @@ -818,11 +833,13 @@ impl Context { // Where the new stake will live. let new_stake = self.deterministic_keypair.new_keypair(); + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&validator_vote_account).unwrap(); send_transaction( &mut self.context, &[instruction::withdraw( &id(), - &instruction::WithdrawAccountsMeta { + &instruction::WithdrawAccountsMetaV2 { lido: self.solido.pubkey(), st_sol_mint: self.st_sol_mint, st_sol_account_owner: user.pubkey(), @@ -831,8 +848,10 @@ impl Context { source_stake_account, destination_stake_account: new_stake.pubkey(), stake_authority: self.stake_authority, + validator_list: self.validator_list.pubkey(), }, amount, + validator_index, )], vec![user, &new_stake], ) @@ -869,41 +888,54 @@ impl Context { ) -> transport::Result { let solido = self.get_solido().await; - let validator_entry = solido + let validator = solido .validators - .get(&validator_vote_account) + .find(&validator_vote_account) .expect("Trying to stake with a non-member validator."); - let (stake_account_end, _) = validator_entry.find_stake_account_address( - &id(), - &self.solido.pubkey(), - validator_entry.entry.stake_seeds.end, - StakeType::Stake, - ); + let validator_index = solido.validators.position(&validator_vote_account).unwrap(); + let (stake_account_end, stake_account_merge_into) = match approach { + StakeDeposit::Append => { + let (stake_account_end, _) = validator.find_stake_account_address( + &id(), + &self.solido.pubkey(), + validator.stake_seeds.end, + StakeType::Stake, + ); + (stake_account_end, stake_account_end) + } + StakeDeposit::Merge => { + let (stake_account_end, _) = validator.find_temporary_stake_account_address( + &id(), + &self.solido.pubkey(), + validator.stake_seeds.end, + self.get_clock().await.epoch, + ); - let (stake_account_merge_into, _) = validator_entry.find_stake_account_address( - &id(), - &self.solido.pubkey(), - match approach { - StakeDeposit::Append => validator_entry.entry.stake_seeds.end, - // We do a wrapping sub here, so we can call stake-merge initially, - // when end is 0, such that the account to merge into is not the - // same as the end account. - StakeDeposit::Merge => validator_entry.entry.stake_seeds.end.wrapping_sub(1), - }, - StakeType::Stake, - ); + let (stake_account_merge_into, _) = validator.find_stake_account_address( + &id(), + &self.solido.pubkey(), + // We do a wrapping sub here, so we can call stake-merge initially, + // when end is 0, such that the account to merge into is not the + // same as the end account. + validator.stake_seeds.end.wrapping_sub(1), + StakeType::Stake, + ); + (stake_account_end, stake_account_merge_into) + } + }; let maintainer = self .maintainer .as_ref() .expect("Must have maintainer to call StakeDeposit."); + let maintainer_index = solido.maintainers.position(&maintainer.pubkey()).unwrap(); send_transaction( &mut self.context, &[instruction::stake_deposit( &id(), - &instruction::StakeDepositAccountsMeta { + &instruction::StakeDepositAccountsMetaV2 { lido: self.solido.pubkey(), maintainer: maintainer.pubkey(), validator_vote_account, @@ -911,8 +943,12 @@ impl Context { stake_account_merge_into, stake_account_end, stake_authority: self.stake_authority, + validator_list: self.validator_list.pubkey(), + maintainer_list: self.maintainer_list.pubkey(), }, amount, + validator_index, + maintainer_index, )], vec![maintainer], ) @@ -941,34 +977,41 @@ impl Context { ) -> transport::Result<()> { // Where the new stake will live. let solido = self.get_solido().await; - let validator = solido.validators.get(&validator_vote_account).unwrap(); + let validator = solido.validators.find(&validator_vote_account).unwrap(); let (source_stake_account, _) = validator.find_stake_account_address( &id(), &self.solido.pubkey(), - validator.entry.stake_seeds.begin, + validator.stake_seeds.begin, StakeType::Stake, ); let (destination_unstake_account, _) = validator.find_stake_account_address( &id(), &self.solido.pubkey(), - validator.entry.unstake_seeds.end, + validator.unstake_seeds.end, StakeType::Unstake, ); + let validator_index = solido.validators.position(&validator_vote_account).unwrap(); + let maintainer = self.maintainer.as_ref().unwrap(); + let maintainer_index = solido.maintainers.position(&maintainer.pubkey()).unwrap(); send_transaction( &mut self.context, &[instruction::unstake( &id(), - &instruction::UnstakeAccountsMeta { + &instruction::UnstakeAccountsMetaV2 { lido: self.solido.pubkey(), validator_vote_account, source_stake_account, destination_unstake_account, stake_authority: self.stake_authority, - maintainer: self.maintainer.as_ref().unwrap().pubkey(), + maintainer: maintainer.pubkey(), + validator_list: self.validator_list.pubkey(), + maintainer_list: self.maintainer_list.pubkey(), }, amount, + validator_index, + maintainer_index, )], vec![self.maintainer.as_ref().unwrap()], ) @@ -1011,10 +1054,11 @@ impl Context { &mut self.context, &[instruction::update_exchange_rate( &id(), - &instruction::UpdateExchangeRateAccountsMeta { + &instruction::UpdateExchangeRateAccountsMetaV2 { lido: self.solido.pubkey(), reserve: self.reserve_address, st_sol_mint: self.st_sol_mint, + validator_list: self.validator_list.pubkey(), }, )], vec![], @@ -1033,7 +1077,7 @@ impl Context { /// Returns the address that stake was merged into. pub async fn try_merge_stake( &mut self, - validator: &PubkeyAndEntry, + validator: &Validator, from_seed: u64, to_seed: u64, ) -> transport::Result { @@ -1051,17 +1095,21 @@ impl Context { StakeType::Stake, ); + let solido = self.get_solido().await; + let validator_index = solido.validators.position(validator.pubkey()).unwrap(); send_transaction( &mut self.context, &[instruction::merge_stake( &id(), - &instruction::MergeStakeMeta { + &instruction::MergeStakeMetaV2 { lido: self.solido.pubkey(), - validator_vote_account: validator.pubkey, + validator_vote_account: *validator.pubkey(), stake_authority: self.stake_authority, from_stake: from_stake_account, to_stake: to_stake_account, + validator_list: self.validator_list.pubkey(), }, + validator_index, )], vec![], ) @@ -1073,7 +1121,7 @@ impl Context { /// Merge two accounts of a given validator. pub async fn merge_stake( &mut self, - validator: &PubkeyAndEntry, + validator: &Validator, from_seed: u64, to_seed: u64, ) -> Pubkey { @@ -1089,21 +1137,23 @@ impl Context { validator_vote_account: Pubkey, ) -> transport::Result<()> { let solido = self.get_solido().await; - let validator = solido.validators.get(&validator_vote_account).unwrap(); + let validator = solido.validators.find(&validator_vote_account).unwrap(); let mut stake_account_addrs: Vec = Vec::new(); - stake_account_addrs.extend(validator.entry.stake_seeds.into_iter().map(|seed| { + stake_account_addrs.extend(validator.stake_seeds.into_iter().map(|seed| { validator .find_stake_account_address(&id(), &self.solido.pubkey(), seed, StakeType::Stake) .0 })); - stake_account_addrs.extend(validator.entry.unstake_seeds.into_iter().map(|seed| { + stake_account_addrs.extend(validator.unstake_seeds.into_iter().map(|seed| { validator .find_stake_account_address(&id(), &self.solido.pubkey(), seed, StakeType::Unstake) .0 })); + let validator_index = solido.validators.position(&validator_vote_account).unwrap(); + send_transaction( &mut self.context, &[instruction::update_stake_account_balance( @@ -1118,7 +1168,9 @@ impl Context { mint_authority: self.mint_authority, treasury_st_sol_account: self.treasury_st_sol_account, developer_st_sol_account: self.developer_st_sol_account, + validator_list: self.validator_list.pubkey(), }, + validator_index, )], vec![], ) @@ -1154,6 +1206,14 @@ impl Context { .unwrap_or_else(|| panic!("Account {} does not exist.", address)) } + pub async fn get_account_list(&mut self, address: Pubkey) -> Option> + where + T: ListEntry + Clone + Default + BorshSerialize, + { + let mut list_account = self.get_account(address).await; + AccountList::from(&mut list_account.data).ok() + } + pub async fn get_sol_balance(&mut self, address: Pubkey) -> Lamports { self.try_get_sol_balance(address) .await @@ -1194,12 +1254,26 @@ impl Context { .expect("Failed to transfer tokens."); } - pub async fn get_solido(&mut self) -> Lido { + pub async fn get_solido(&mut self) -> SolidoWithLists { let lido_account = self.get_account(self.solido.pubkey()).await; // This returns a Result because it can cause an IO error, but that should // not happen in the test environment. (And if it does, then the test just // fails.) - try_from_slice_unchecked::(lido_account.data.as_slice()).unwrap() + let lido = try_from_slice_unchecked::(lido_account.data.as_slice()).unwrap(); + let validators = self + .get_account_list::(lido.validator_list) + .await + .unwrap_or_else(|| AccountList::::new_default(0)); + let maintainers = self + .get_account_list::(lido.maintainer_list) + .await + .unwrap_or_else(|| AccountList::::new_default(0)); + + SolidoWithLists { + lido, + validators, + maintainers, + } } pub async fn get_rent(&mut self) -> Rent { @@ -1242,7 +1316,7 @@ impl Context { pub async fn get_stake_account_from_seed( &mut self, - validator: &PubkeyAndEntry, + validator: &Validator, seed: u64, ) -> StakeAccount { let (stake_address, _) = validator.find_stake_account_address( @@ -1261,7 +1335,7 @@ impl Context { pub async fn get_unstake_account_from_seed( &mut self, - validator: &PubkeyAndEntry, + validator: &Validator, seed: u64, ) -> StakeAccount { let (stake_address, _) = validator.find_stake_account_address( @@ -1306,6 +1380,8 @@ impl Context { &mut self, vote_account: Pubkey, ) -> transport::Result<()> { + let solido = self.get_solido().await; + let validator_index = solido.validators.position(&vote_account).unwrap(); send_transaction( &mut self.context, &[ @@ -1314,13 +1390,35 @@ impl Context { &lido::instruction::DeactivateValidatorIfCommissionExceedsMaxMeta { lido: self.solido.pubkey(), validator_vote_account_to_deactivate: vote_account, + validator_list: self.validator_list.pubkey(), }, + validator_index, ), ], vec![], ) .await } + + pub async fn try_close_vote_account( + &mut self, + vote_account: &Pubkey, + withdraw_authority: &Keypair, + ) -> transport::Result<()> { + let vote_info = self.get_account(*vote_account).await; + + send_transaction( + &mut self.context, + &[solana_vote_program::vote_instruction::withdraw( + vote_account, + &withdraw_authority.pubkey(), + vote_info.lamports, + &Pubkey::new_unique(), + )], + vec![withdraw_authority], + ) + .await + } } /// Return an `AccountInfo` for the given account, with `is_signer` and `is_writable` set to false. diff --git a/testlib/src/util.rs b/testlib/src/util.rs deleted file mode 100644 index 102addd55..000000000 --- a/testlib/src/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Chorus One AG -// SPDX-License-Identifier: GPL-3.0 - -use std::env; -use std::process::Command; - -/// Download the Orca Token Swap program from the chain and put it in `target/deploy`. -/// -/// If the file already exists, this will not download it again. -pub fn ensure_orca_program_exists() { - let mut path = env::current_exe().expect("Failed to get executable path."); - - // The executable path of the test driver is something like - // /repo/target/debug/deps/mod-8d7ddfb574f4dee2 - // So to get to "target/deploy", we drop 3 components and then add "deploy". - path.pop(); - path.pop(); - path.pop(); - path.push("deploy"); - path.push("orca_token_swap_v2.so"); - - if path.exists() { - // Program already there, we are not going to download it again. - return; - } - - println!("Orca program not found at {:?}, downloading ...", path); - let result = Command::new("solana") - .args(&["--url", "https://api.mainnet-beta.solana.com"]) - .args(&["program", "dump"]) - .arg(anker::orca_token_swap_v2::id().to_string()) - .arg(&path) - .status(); - - match result { - Ok(status) if status.success() => { /* Ok */ } - _ => { - panic!( - "Failed to obtain Orca program from chain. \ - Please run 'solana program dump {} target/deploy/orca_token_swap_v2.so'.", - anker::orca_token_swap_v2::id(), - ); - } - } - - assert!(path.exists(), "{:?} should exist by now.", path); -} diff --git a/tests/coverage.py b/tests/coverage.py index e081a7d94..8359686c8 100755 --- a/tests/coverage.py +++ b/tests/coverage.py @@ -155,7 +155,6 @@ def generate_report(executables: List[str]) -> None: ['test', '--no-run', '--manifest-path', 'cli/maintainer/Cargo.toml'] ), *build_binaries(['test', '--no-run', '--manifest-path', 'program/Cargo.toml']), - *build_binaries(['test', '--no-run', '--manifest-path', 'anker/Cargo.toml']), *build_binaries(['build']), ] diff --git a/tests/deploy_test_anker.py b/tests/deploy_test_anker.py deleted file mode 100755 index fd138b727..000000000 --- a/tests/deploy_test_anker.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: 2021 Chorus One AG -# SPDX-License-Identifier: GPL-3.0 - -""" -Set up a Solido and Anker instance on Solana devnet or a local testnet, and print -its details. Anker requires a Wormhole deployment for full functionality, which -does exist on devnet, but which is difficult to recreate locally. Everything -aside from sending rewards can be tested locally though. - -""" - -import json -import os -import subprocess -import sys - -from uuid import uuid4 - -from util import ( - create_test_account, - get_approve_and_execute, - get_network, - get_solido_program_path, - multisig, - rpc_get_account_info, - solana, - solana_program_deploy, - solido, - spl_token, -) - -DEVNET_ORCA_PROGRAM_ID = '3xQ8SWv2GaFXXpHZNqkXsdxq5DZciHBz6ZFoPPfbFd7U' -DEVNET_WORMHOLE_UST_MINT = '5Dmmc5CC6ZpKif8iN5DSY9qNYrWJvEKcX2JrxGESqRMu' -DEVNET_TERRA_REWARDS_ADDRESS = 'terra1uwlxcas745mwjte8wwu2l0fcs483twujnt8j5l' -DEVNET_WORMHOLE_CORE_BRIDGE_PROGRAM_ID = '3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5' -DEVNET_WORMHOLE_TOKEN_BRIDGE_PROGRAM_ID = 'DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe' - -# Create a fresh directory where we store all the keys and configuration for this -# deployment. -run_id = uuid4().hex[:10] -test_dir = f'tests/.keys/{run_id}' -os.makedirs(test_dir, exist_ok=True) -print(f'Keys directory: {test_dir}') - -# Before we start, check our current balance. We also do this at the end, -# and then we know how much the deployment cost. -sol_balance_pre = float(solana('balance').split(' ')[0]) - -# Start with the UST accounts, because on devnet we can't create UST, we need to -# receive it externally, and if we don't have the UST yet, then don't waste time -# by uploading programs (which is slow) and only later failing, we can fail fast. -# If we are on devnet, then Wormhole UST exists already, and we use that address. -# Otherwise, we create a new SPL token and pretend that that's UST. -ust_mint_keypair = None -if rpc_get_account_info(DEVNET_WORMHOLE_UST_MINT) is None: - print('\nSetting up UST mint ...') - ust_mint_keypair = create_test_account(f'{test_dir}/ust-mint.json', fund=False) - spl_token('create-token', f'{test_dir}/ust-mint.json', '--decimals', '6') - ust_mint_address = ust_mint_keypair.pubkey -else: - print('\nFound existing devnet Wormhole UST mint.') - ust_mint_address = DEVNET_WORMHOLE_UST_MINT -print(f'> UST mint is {ust_mint_address}.') - -try: - ust_account_info_json = spl_token( - 'create-account', ust_mint_address, '--output', 'json' - ) -except subprocess.CalledProcessError: - # "spl-token create-account" fails if the associated token account exists - # already. It would be nice to check whether it exists before we try to - # create it, but unfortunately there appears to be no way to get the address - # of the associated token account, either through the Solana RPC, or through - # one of the command-line tools. The associated token account address remains - # implicit everywhere :/ - pass - -# If we control the UST mint (on a local test validator), we can mint ourselves -# 0.1 UST. But if we don't control the mint, then we need to be sure that we have -# some to start with. -if ust_mint_keypair is not None: - spl_token('mint', ust_mint_keypair.pubkey, '0.1') - print('> Minted ourselves 0.1 UST.') -else: - ust_balance_json = spl_token('balance', ust_mint_address, '--output', 'json') - ust_balance_dict = json.loads(ust_balance_json) - ust_balance_micro_ust = int(ust_balance_dict['amount']) - if ust_balance_micro_ust < 100_000: - print('Please ensure that your associated token account has at least 0.1 UST.') - owner_addr = solana('address').strip() - print(f'It should go into the associated token account of {owner_addr}.') - sys.exit(1) - else: - print( - '> We have sufficient UST to proceed: ' - f'{ust_balance_micro_ust / 1e6:.6f} >= 0.1.' - ) - -print('\nUploading Multisig program ...') -multisig_program_id = solana_program_deploy( - get_solido_program_path() + '/serum_multisig.so' -) -print(f'> Multisig program id is {multisig_program_id}') - -print('\nUploading Solido program ...') -solido_program_id = solana_program_deploy(get_solido_program_path() + '/lido.so') -print(f'> Solido program id is {solido_program_id}') - -print('\nUploading Anker program ...') -anker_program_id = solana_program_deploy(get_solido_program_path() + '/anker.so') -print(f'> Anker program id is {anker_program_id}') - -# If the Orca program exists, use that, otherwise upload it at a new address. -orca_info = rpc_get_account_info(DEVNET_ORCA_PROGRAM_ID) -if orca_info is not None: - print('\nFound existing instance of Orca Token Swap program.') - token_swap_program_id = DEVNET_ORCA_PROGRAM_ID -else: - print('\nUploading Orca Token Swap program ...') - token_swap_program_id = solana_program_deploy( - get_solido_program_path() + '/orca_token_swap_v2.so' - ) -print(f'> Token swap program id is {token_swap_program_id}') - -maintainer = create_test_account(test_dir + '/maintainer.json') -st_sol_accounts_owner = create_test_account(test_dir + '/st-sol-accounts-owner.json') - -print('\nCreating new multisig ...') -multisig_data = multisig( - 'create-multisig', - '--multisig-program-id', - multisig_program_id, - '--threshold', - '1', - '--owners', - maintainer.pubkey, -) -multisig_instance = multisig_data['multisig_address'] -multisig_pda = multisig_data['multisig_program_derived_address'] -print(f'> Created instance at {multisig_instance}') - -print('\nCreating Solido instance ...') -result = solido( - 'create-solido', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--max-validators', - '9', - '--max-maintainers', - '3', - '--max-commission-percentage', - '5', - '--treasury-fee-share', - '5', - '--developer-fee-share', - '2', - '--st-sol-appreciation-share', - '93', - '--treasury-account-owner', - st_sol_accounts_owner.pubkey, - '--developer-account-owner', - st_sol_accounts_owner.pubkey, - '--multisig-address', - multisig_instance, - keypair_path=maintainer.keypair_path, -) - -solido_address = result['solido_address'] -st_sol_mint_address = result['st_sol_mint_address'] -print(f'> Created instance at {solido_address}') - -approve_and_execute = get_approve_and_execute( - multisig_program_id=multisig_program_id, - multisig_instance=multisig_instance, - signer_keypair_paths=[maintainer.keypair_path], -) - -print('\nAdding maintainer ...') -transaction_result = solido( - 'add-maintainer', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--maintainer-address', - maintainer.pubkey, - '--multisig-address', - multisig_instance, - keypair_path=maintainer.keypair_path, -) -approve_and_execute(transaction_result['transaction_address']) -print(f'> Maintainer {maintainer.pubkey} added.') - -# Next up is the token pool, but to be able to set that up, -# we need some stSOL (and some UST, which we have already), -# and then we need to put that in some new accounts that the -# pool will take ownership of. -print('\nSetting up stSOL-UST pool ...') -solido( - 'deposit', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--amount-sol', - '0.1', -) -pool_ust_keypair = create_test_account(f'{test_dir}/pool-ust.json', fund=False) -pool_st_sol_keypair = create_test_account(f'{test_dir}/pool-st-sol.json', fund=False) -spl_token('create-account', ust_mint_address, pool_ust_keypair.keypair_path) -spl_token('create-account', st_sol_mint_address, pool_st_sol_keypair.keypair_path) -spl_token('transfer', ust_mint_address, '0.1', pool_ust_keypair.pubkey) -spl_token('transfer', st_sol_mint_address, '0.1', pool_st_sol_keypair.pubkey) -result = solido( - 'anker', - 'create-token-pool', - '--token-swap-program-id', - token_swap_program_id, - '--ust-mint-address', - ust_mint_address, - '--ust-account', - pool_ust_keypair.pubkey, - '--st-sol-account', - pool_st_sol_keypair.pubkey, -) -token_pool_address = result['pool_address'] -print(f'Pool address is {token_pool_address}.') - -print('\nCreating Anker instance ...') -result = solido( - 'anker', - 'create', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--anker-program-id', - anker_program_id, - '--ust-mint-address', - ust_mint_address, - '--token-swap-pool', - token_pool_address, - '--wormhole-core-bridge-program-id', - DEVNET_WORMHOLE_CORE_BRIDGE_PROGRAM_ID, - '--wormhole-token-bridge-program-id', - DEVNET_WORMHOLE_TOKEN_BRIDGE_PROGRAM_ID, - '--terra-rewards-address', - DEVNET_TERRA_REWARDS_ADDRESS, -) -anker_address = result['anker_address'] -print(f'> Created instance at {anker_address}.') - -sol_balance_post = float(solana('balance').split(' ')[0]) -total_cost_sol = sol_balance_pre - sol_balance_post -print(f'\nDeployment cost {total_cost_sol:.3f} SOL.') - -# Save the configuration to a file, to make it easier to run the maintainer -# and other commands later. -config = { - 'keypair_path': test_dir + '/maintainer.json', - 'cluster': get_network(), - 'multisig_program_id': multisig_program_id, - 'multisig_address': multisig_instance, - 'solido_program_id': solido_program_id, - 'solido_address': solido_address, - 'anker_program_id': anker_program_id, - 'anker_address': anker_address, - 'max_poll_interval_seconds': 10, -} -with open(test_dir + '/config.json', 'w', encoding='utf-8') as config_file: - json.dump(config, config_file, indent=2) - - -print('\nMaintenance command line:') -print(f'solido --config {test_dir}/config.json run-maintainer') diff --git a/tests/deploy_test_solido.py b/tests/deploy_test_solido.py index 020a88eca..49660d098 100755 --- a/tests/deploy_test_solido.py +++ b/tests/deploy_test_solido.py @@ -10,12 +10,11 @@ import json import os -from typing import Optional, Dict, Any +from typing import Optional, Any from util import ( create_test_account, solana_program_deploy, - create_spl_token_account, create_vote_account, get_network, solana, @@ -26,239 +25,244 @@ MAX_VALIDATION_COMMISSION_PERCENTAGE, ) -print('\nUploading Solido program ...') -solido_program_id = solana_program_deploy(get_solido_program_path() + '/lido.so') -print(f'> Solido program id is {solido_program_id}') -print('\nUploading Multisig program ...') -multisig_program_id = solana_program_deploy( - get_solido_program_path() + '/serum_multisig.so' -) -print(f'> Multisig program id is {multisig_program_id}') - -os.makedirs('tests/.keys', exist_ok=True) -maintainer = create_test_account('tests/.keys/maintainer.json') -st_sol_accounts_owner = create_test_account('tests/.keys/st-sol-accounts-owner.json') - -print('\nCreating new multisig ...') -multisig_data = multisig( - 'create-multisig', - '--multisig-program-id', - multisig_program_id, - '--threshold', - '1', - '--owners', - maintainer.pubkey, -) -multisig_instance = multisig_data['multisig_address'] -multisig_pda = multisig_data['multisig_program_derived_address'] -print(f'> Created instance at {multisig_instance}') +class Instance: + def __init__(self) -> None: + print('\nUploading Solido program ...') + self.solido_program_id = solana_program_deploy( + get_solido_program_path() + '/lido.so' + ) + print(f'> Solido program id is {self.solido_program_id}') -print('\nCreating Solido instance ...') -result = solido( - 'create-solido', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--max-validators', - '9', - '--max-maintainers', - '3', - '--max-commission-percentage', - str(MAX_VALIDATION_COMMISSION_PERCENTAGE), - '--treasury-fee-share', - '5', - '--developer-fee-share', - '2', - '--st-sol-appreciation-share', - '93', - '--treasury-account-owner', - st_sol_accounts_owner.pubkey, - '--developer-account-owner', - st_sol_accounts_owner.pubkey, - '--multisig-address', - multisig_instance, - keypair_path=maintainer.keypair_path, -) + print('\nUploading Multisig program ...') + self.multisig_program_id = solana_program_deploy( + get_solido_program_path() + '/serum_multisig.so' + ) + print(f'> Multisig program id is {self.multisig_program_id}') -solido_address = result['solido_address'] -treasury_account = result['treasury_account'] -developer_account = result['developer_account'] -st_sol_mint_account = result['st_sol_mint_address'] + os.makedirs('tests/.keys', exist_ok=True) + self.maintainer = create_test_account('tests/.keys/maintainer.json') + st_sol_accounts_owner = create_test_account( + 'tests/.keys/st-sol-accounts-owner.json' + ) -print(f'> Created instance at {solido_address}') + print('\nCreating new multisig ...') + multisig_data = multisig( + 'create-multisig', + '--multisig-program-id', + self.multisig_program_id, + '--threshold', + '1', + '--owners', + self.maintainer.pubkey, + ) + self.multisig_instance = multisig_data['multisig_address'] + print(f'> Created instance at {self.multisig_instance}') -approve_and_execute = get_approve_and_execute( - multisig_program_id=multisig_program_id, - multisig_instance=multisig_instance, - signer_keypair_paths=[maintainer.keypair_path], -) + print('\nCreating Solido instance ...') + result = solido( + 'create-solido', + '--multisig-program-id', + self.multisig_program_id, + '--solido-program-id', + self.solido_program_id, + '--max-validators', + '9', + '--max-maintainers', + '3', + '--max-commission-percentage', + str(MAX_VALIDATION_COMMISSION_PERCENTAGE), + '--treasury-fee-share', + '5', + '--developer-fee-share', + '2', + '--st-sol-appreciation-share', + '93', + '--treasury-account-owner', + st_sol_accounts_owner.pubkey, + '--developer-account-owner', + st_sol_accounts_owner.pubkey, + '--multisig-address', + self.multisig_instance, + keypair_path=self.maintainer.keypair_path, + ) + self.solido_address = result['solido_address'] + self.treasury_account = result['treasury_account'] + self.developer_account = result['developer_account'] + self.st_sol_mint_account = result['st_sol_mint_address'] + self.validator_list_address = result['validator_list_address'] + self.maintainer_list_address = result['maintainer_list_address'] -def add_validator(index: int, vote_account: Optional[str]) -> str: - """ - Add a validator to the instance, create the right accounts for it. The vote - account can be a pre-existing one, but if it is not provided, we will create - one. Returns the vote account address. - """ - print(f'\nCreating validator {index} ...') + print(f'> Created instance at {self.solido_address}') - if vote_account is None: - solido_instance = solido( - 'show-solido', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, + solido_instance = self.pull_solido() + solana( + 'program', + 'set-upgrade-authority', + '--new-upgrade-authority', + solido_instance['solido']['manager'], + self.solido_program_id, ) - validator = create_test_account(f'tests/.keys/validator-{index}-account.json') - validator_vote_account, _ = create_vote_account( - f'tests/.keys/validator-{index}-vote-account.json', - validator.keypair_path, - f'tests/.keys/validator-{index}-withdraw-account.json', - MAX_VALIDATION_COMMISSION_PERCENTAGE, - ) - vote_account = validator_vote_account.pubkey - - print(f'> Validator vote account: {vote_account}') - print('Adding validator ...') - transaction_result = solido( - 'add-validator', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--validator-vote-account', - vote_account, - '--multisig-address', - multisig_instance, - keypair_path=maintainer.keypair_path, - ) - approve_and_execute(transaction_result['transaction_address']) - return vote_account + self.approve_and_execute = get_approve_and_execute( + multisig_program_id=self.multisig_program_id, + multisig_instance=self.multisig_instance, + signer_keypair_paths=[self.maintainer.keypair_path], + ) + # For the first validator, add the test validator itself, so we include a + # validator that is actually voting, and earning rewards. + current_validators = json.loads(solana('validators', '--output', 'json')) -# For the first validator, add the test validator itself, so we include a -# validator that is actually voting, and earning rewards. -current_validators = json.loads(solana('validators', '--output', 'json')) + # If we're running on localhost, change the comission + if get_network() == 'http://127.0.0.1:8899': + solido_instance = self.pull_solido() + print( + '> Changing validator\'s comission to {}% ...'.format( + MAX_VALIDATION_COMMISSION_PERCENTAGE + ) + ) + validator = current_validators['validators'][0] + validator['commission'] = str(MAX_VALIDATION_COMMISSION_PERCENTAGE) + solana( + 'vote-update-commission', + validator['voteAccountPubkey'], + str(MAX_VALIDATION_COMMISSION_PERCENTAGE), + './test-ledger/vote-account-keypair.json', + ) + solana( + 'validator-info', + 'publish', + '--keypair', + './test-ledger/validator-keypair.json', + "solana-test-validator", + ) -# If we're running on localhost, change the comission to 100% and withdrawer -# address to the Solido's rewards withdraw authority. -if get_network() == 'http://127.0.0.1:8899': - solido_instance = solido( - 'show-solido', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - ) - print( - '> Changing validator\'s comission to {}% ...'.format( - MAX_VALIDATION_COMMISSION_PERCENTAGE - ) - ) - validator = current_validators['validators'][0] - validator['commission'] = str(MAX_VALIDATION_COMMISSION_PERCENTAGE) - solana( - 'vote-update-commission', - validator['voteAccountPubkey'], - str(MAX_VALIDATION_COMMISSION_PERCENTAGE), - './test-ledger/vote-account-keypair.json', - ) - solana( - 'validator-info', - 'publish', - '--keypair', - './test-ledger/validator-keypair.json', - "solana-test-validator", - ) + # Allow only validators that are voting, have 100% commission, and have their + # withdrawer set to Solido's rewards withdraw authority. On a local testnet, + # this will only contain the test validator, but on devnet or testnet, there can + # be more validators. + active_validators = [ + v + for v in current_validators['validators'] + if (not v['delinquent']) + and v['commission'] == str(MAX_VALIDATION_COMMISSION_PERCENTAGE) + ] + # Add up to 5 of the active validators. Locally there will only be one, but on + # the devnet or testnet there can be more, and we don't want to add *all* of them. + validators = [ + self.add_validator(i, vote_account=v['voteAccountPubkey']) + for (i, v) in enumerate(active_validators[:5]) + ] -# Allow only validators that are voting, have 100% commission, and have their -# withdrawer set to Solido's rewards withdraw authority. On a local testnet, -# this will only contain the test validator, but on devnet or testnet, there can -# be more validators. -active_validators = [ - v - for v in current_validators['validators'] - if (not v['delinquent']) - and v['commission'] == str(MAX_VALIDATION_COMMISSION_PERCENTAGE) -] + # Create two validators of our own, so we have a more interesting stake + # distribution. These validators are not running, so they will not earn + # rewards. + # validators.extend( + # self.add_validator(i, vote_account=None) + # for i in range(len(validators), len(validators) + 1) + # ) -# Add up to 5 of the active validators. Locally there will only be one, but on -# the devnet or testnet there can be more, and we don't want to add *all* of them. -validators = [ - add_validator(i, vote_account=v['voteAccountPubkey']) - for (i, v) in enumerate(active_validators[:5]) -] + print('Adding maintainer ...') + transaction_result = solido( + 'add-maintainer', + '--multisig-program-id', + self.multisig_program_id, + '--solido-program-id', + self.solido_program_id, + '--solido-address', + self.solido_address, + '--maintainer-address', + self.maintainer.pubkey, + '--multisig-address', + self.multisig_instance, + keypair_path=self.maintainer.keypair_path, + ) + self.approve_and_execute(transaction_result['transaction_address']) -# Create two validators of our own, so we have a more interesting stake -# distribution. These validators are not running, so they will not earn -# rewards. -validators.extend( - add_validator(i, vote_account=None) - for i in range(len(validators), len(validators) + 2) -) + output = { + "cluster": get_network(), + "multisig_program_id": self.multisig_program_id, + "multisig_address": self.multisig_instance, + "solido_program_id": self.solido_program_id, + "solido_address": self.solido_address, + "st_sol_mint": self.st_sol_mint_account, + } + print("Config file is ../solido_test.json") + with open('../solido_test.json', 'w') as outfile: + json.dump(output, outfile, indent=4) + for i, vote_account in enumerate(validators): + print(f' Validator {i} vote account: {vote_account}') -print('Adding maintainer ...') -transaction_result = solido( - 'add-maintainer', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--maintainer-address', - maintainer.pubkey, - '--multisig-address', - multisig_instance, - keypair_path=maintainer.keypair_path, -) -approve_and_execute(transaction_result['transaction_address']) + print('\nMaintenance command line:') + print( + ' ', + ' '.join( + [ + 'solido', + '--keypair-path', + self.maintainer.keypair_path, + '--config', + '../solido_test.json', + 'run-maintainer', + '--max-poll-interval-seconds', + '10', + ] + ), + ) + def pull_solido(self) -> Any: + return solido( + 'show-solido', + '--solido-program-id', + self.solido_program_id, + '--solido-address', + self.solido_address, + ) -solido_instance = solido( - 'show-solido', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, -) -print('\nDetails:') -print(f' Multisig program id: {multisig_program_id}') -print(f' Multisig address: {multisig_instance}') -print(f' Solido program id: {solido_program_id}') -print(f' Solido address: {solido_address}') -print(f' Reserve address: {solido_instance["reserve_account"]}') -print(f' Maintainer address: {maintainer.pubkey}') + def add_validator(self, index: int, vote_account: Optional[str]) -> str: + """ + Add a validator to the instance, create the right accounts for it. The vote + account can be a pre-existing one, but if it is not provided, we will create + one. Returns the vote account address. + """ + print(f'\nCreating validator {index} ...') -for i, vote_account in enumerate(validators): - print(f' Validator {i} vote account: {vote_account}') + if vote_account is None: + validator = create_test_account( + f'tests/.keys/validator-{index}-account.json' + ) + validator_vote_account, _ = create_vote_account( + f'tests/.keys/validator-{index}-vote-account.json', + validator.keypair_path, + f'tests/.keys/validator-{index}-withdraw-account.json', + MAX_VALIDATION_COMMISSION_PERCENTAGE, + ) + vote_account = validator_vote_account.pubkey + print(f'> Validator vote account: {vote_account}') -print('\nMaintenance command line:') -print( - ' ', - ' '.join( - [ - 'solido', - '--keypair-path', - maintainer.keypair_path, - '--cluster', - get_network(), - 'run-maintainer', + print('Adding validator ...') + transaction_result = solido( + 'add-validator', + '--multisig-program-id', + self.multisig_program_id, '--solido-program-id', - solido_program_id, + self.solido_program_id, '--solido-address', - solido_address, - '--max-poll-interval-seconds', - '10', - ] - ), -) + self.solido_address, + '--validator-vote-account', + vote_account, + '--multisig-address', + self.multisig_instance, + keypair_path=self.maintainer.keypair_path, + ) + self.approve_and_execute(transaction_result['transaction_address']) + return vote_account + + +if __name__ == "__main__": + Instance() diff --git a/tests/emulate.py b/tests/emulate.py new file mode 100644 index 000000000..0454ec061 --- /dev/null +++ b/tests/emulate.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +""" +Deployment emulation - starts solana-test-validator, creates an instance and starts a bot. +""" + +import unittest +import subprocess +import time +import os +import signal +from typing import Any, Tuple +import json + +from util import run, get_solido_path +from start_test_validator import get_rpc_block_height +from deploy_test_solido import Instance + + +class Emulator(unittest.TestCase): + """Launches a validator, Solido instance and a maintainer for test purposes. + + You can inherit from it to write your tests and interact with Solido instance. + """ + + def setUp(self) -> None: + "launches local solana-test-validator" + run('rm', '-rf', 'tests/.keys/', 'test-ledger/', 'tests/__pycache__/') + subprocess.run(['killall', '-qw', 'solana-test-validator']) + self.solana_test_validator = subprocess.Popen( + ["solana-test-validator --slots-per-epoch 150"], + stdout=subprocess.DEVNULL, + shell=True, + preexec_fn=os.setsid, + ) + + # wait for solana-test-validator to be up and ready + self.assertNotEqual(get_rpc_block_height(), None) + + # wait for instance to be deployed locally + self.instance = Instance() + + # start a maintainer and redirect logs to a file + self.logs_file = open("tests/.logs", "w") + self.maintainer_process = subprocess.Popen( + [ + get_solido_path(), + '--keypair-path', + 'tests/.keys/maintainer.json', + '--config', + '../solido_test.json', + 'run-maintainer', + '--max-poll-interval-seconds', + '1', + ], + stdout=self.logs_file, + universal_newlines=True, + preexec_fn=os.setsid, + ) + + @property + def epoch(self) -> Tuple[int, float]: + info = run('solana', 'epoch-info', '--output', 'json') + parsed = json.loads(info) + return int(parsed['epoch']), float(parsed['epochCompletedPercent']) + + def tearDown(self) -> None: + os.killpg(os.getpgid(self.solana_test_validator.pid), signal.SIGTERM) + os.killpg(os.getpgid(self.maintainer_process.pid), signal.SIGTERM) + self.logs_file.close() + + +class MyTest(Emulator): + def test_some(self) -> None: + print(self.epoch) + + +if __name__ == "__main__": + mt = MyTest() + unittest.main(verbosity=2, warnings='ignore') diff --git a/tests/start_test_validator.py b/tests/start_test_validator.py index 2920db9c7..96e214f7c 100755 --- a/tests/start_test_validator.py +++ b/tests/start_test_validator.py @@ -12,19 +12,8 @@ import subprocess import sys import time - from typing import Optional -# Start the validator, pipe its stdout to /dev/null. -test_validator = subprocess.Popen( - [ - 'solana-test-validator', - ], - stdout=subprocess.DEVNULL, - # Somehow, CI only works if `shell=True`, so this argument is left here on - # purpose. - shell=True, -) # Wait up to 60 seconds for the validator to be running and processing blocks. We # check this by running "solana block-height", and observing at least one @@ -32,42 +21,57 @@ # producing blocks. Previously we only checked "solana cluster-version", but # this can return a response before the validator is ready to accept # transactions. -last_observed_block_height: Optional[int] = None +def get_rpc_block_height() -> Optional[int]: + last_observed_block_height: Optional[int] = None -for _ in range(60): - result = subprocess.run( - ['solana', '--url', 'http://127.0.0.1:8899', 'block-height'], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - if result.returncode == 0: - current_block_height = int(result.stdout) - if ( - last_observed_block_height is not None - and current_block_height > last_observed_block_height - ): - break - last_observed_block_height = current_block_height + for _ in range(60): + result = subprocess.run( + ['solana', '--url', 'http://127.0.0.1:8899', 'block-height'], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + if result.returncode == 0: + current_block_height = int(result.stdout) + if ( + last_observed_block_height is not None + and current_block_height > last_observed_block_height + ): + return current_block_height + last_observed_block_height = current_block_height - sleep_seconds = 1 - time.sleep(sleep_seconds) + sleep_seconds = 1 + time.sleep(sleep_seconds) -is_rpc_online = last_observed_block_height is not None + return None -if is_rpc_online and test_validator.poll() is None: - # The RPC is online, and the process is still running. - print(test_validator.pid) -elif is_rpc_online: - print( - 'RPC is online, but the process is gone ... was a validator already running?', - file=sys.stderr, +if __name__ == "__main__": + # Start the validator, pipe its stdout to /dev/null. + test_validator = subprocess.Popen( + ['solana-test-validator'], + stdout=subprocess.DEVNULL, + # Somehow, CI only works if `shell=True`, so this argument is left here on + # purpose. + shell=True, ) - sys.exit(2) -else: - print( - 'Test validator is still not responding, something is wrong.', - file=sys.stderr, - ) - sys.exit(3) + last_observed_block_height = get_rpc_block_height() + is_rpc_online = last_observed_block_height is not None + + if is_rpc_online and test_validator.poll() is None: + # The RPC is online, and the process is still running. + print(test_validator.pid) + + elif is_rpc_online: + print( + 'RPC is online, but the process is gone ... was a validator already running?', + file=sys.stderr, + ) + sys.exit(2) + + else: + print( + 'Test validator is still not responding, something is wrong.', + file=sys.stderr, + ) + sys.exit(3) diff --git a/tests/test_anker.py b/tests/test_anker.py deleted file mode 100755 index 5d3207a3a..000000000 --- a/tests/test_anker.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/env python3 - -# SPDX-FileCopyrightText: 2021 Chorus One AG -# SPDX-License-Identifier: GPL-3.0 - -""" -This script calls 'solana' and 'solido' to confirm that functionality works. - -It exits with exit code 0 if everything works as expected, or with a nonzero -exit code if anything fails. It expects a test validator to be running at at the -default localhost port, and it expects a keypair at ~/.config/solana/id.json -that corresponds to a sufficiently funded account. -""" -import os -from typing import Any, Dict, Optional - -from util import ( - create_test_account, - get_approve_and_execute, - get_solido_program_path, - multisig, - solana_program_deploy, - solido, - spl_token, - spl_token_balance, - create_spl_token_account, - wait_for_slots, -) - -print('Creating test accounts ...') -os.makedirs('tests/.keys', exist_ok=True) -test_addrs = [ - create_test_account('tests/.keys/test-key-1.json'), - create_test_account('tests/.keys/test-key-2.json'), -] -print(f'> {test_addrs}') - -treasury_account_owner = create_test_account('tests/.keys/treasury-key.json') -print(f'> Treasury account owner: {treasury_account_owner}') - -developer_account_owner = create_test_account('tests/.keys/developer-fee-key.json') -print(f'> Developer fee account owner: {developer_account_owner}') - - -print('\nSetting up UST mint ...') -ust_mint_address = create_test_account('tests/.keys/ust_mint_address.json', fund=False) -spl_token('create-token', 'tests/.keys/ust_mint_address.json', '--decimals', '6') -print(f'> UST mint is {ust_mint_address.pubkey}.') - -print('\nUploading Multisig program ...') -multisig_program_id = solana_program_deploy( - get_solido_program_path() + '/serum_multisig.so' -) -print(f'> Multisig program id is {multisig_program_id}.') - -print('\nUploading Solido program ...') -solido_program_id = solana_program_deploy(get_solido_program_path() + '/lido.so') -print(f'> Solido program id is {solido_program_id}.') - -print('\nUploading Anker program ...') -anker_program_id = solana_program_deploy(get_solido_program_path() + '/anker.so') -print(f'> Anker program id is {anker_program_id}.') - -print('\nDeploying Orca Token Swap program ...') -orca_token_swap_program_id = solana_program_deploy( - get_solido_program_path() + '/orca_token_swap_v2.so' -) -print(f'> Orca program id is {orca_token_swap_program_id}.') - -print('\nCreating new multisig ...') -multisig_data = multisig( - 'create-multisig', - '--multisig-program-id', - multisig_program_id, - '--threshold', - '1', - '--owners', - ','.join(t.pubkey for t in test_addrs), -) -multisig_instance = multisig_data['multisig_address'] -multisig_pda = multisig_data['multisig_program_derived_address'] -print(f'> Created instance at {multisig_instance}.') - - -approve_and_execute = get_approve_and_execute( - multisig_program_id=multisig_program_id, - multisig_instance=multisig_instance, - signer_keypair_paths=[t.keypair_path for t in test_addrs], -) - - -print('\nCreating Solido instance ...') -result = solido( - 'create-solido', - '--multisig-program-id', - multisig_program_id, - '--solido-program-id', - solido_program_id, - '--max-validators', - '9', - '--max-maintainers', - '1', - '--max-commission-percentage', - '5', - '--treasury-fee-share', - '4', - '--developer-fee-share', - '1', - '--st-sol-appreciation-share', - '95', - '--treasury-account-owner', - treasury_account_owner.pubkey, - '--developer-account-owner', - developer_account_owner.pubkey, - '--multisig-address', - multisig_instance, - keypair_path=test_addrs[0].keypair_path, -) - -solido_address = result['solido_address'] -treasury_account = result['treasury_account'] -developer_account = result['developer_account'] -st_sol_mint_address = result['st_sol_mint_address'] - -print(f'> Created instance at {solido_address}.') - -print('\nCreating Token Pool accounts ...') -print('> Creating UST token pool account ...') -ust_pool_account = create_spl_token_account( - test_addrs[0].keypair_path, ust_mint_address.pubkey -) - -print('> Creating stSOL token pool account ...') -st_sol_pool_account = create_spl_token_account( - test_addrs[0].keypair_path, st_sol_mint_address -) - -print('> Adding liquidity ...') -print(' > Depositing 1 Sol to Solido') -result = solido( - 'deposit', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--amount-sol', - '1', -) -print(' > Transfering to pool\'s stSOL account.') -spl_token('transfer', st_sol_mint_address, '1', st_sol_pool_account) -print(' > Minting to pool\'s UST account.') -spl_token('mint', ust_mint_address.pubkey, '1', ust_pool_account) - -print('\nCreating token pool instance ...') -result = solido( - 'anker', - 'create-token-pool', - '--token-swap-program-id', - orca_token_swap_program_id, - '--st-sol-account', - st_sol_pool_account, - '--ust-account', - ust_pool_account, - '--ust-mint-address', - ust_mint_address.pubkey, - keypair_path=test_addrs[0].keypair_path, -) -token_pool_address = result['pool_address'] -print(f'> Created instance at {token_pool_address}.') - -# Custom Terra rewards address. -terra_rewards_address = 'terra18aqm668ygwppxnmkmjn4wrtgdweq5ay7rs42ch' - -# Get Anker authorities for testing creating Anker with a known minter. -authorities = solido( - 'anker', - 'show-authorities', - '--solido-address', - solido_address, - '--anker-program-id', - anker_program_id, -) -anker_st_sol_reserve_account = authorities['st_sol_reserve_account'] - -# Create bSOL mint. -b_sol_mint_address = create_test_account( - 'tests/.keys/b_sol_mint_address.json', fund=False -) -spl_token('create-token', b_sol_mint_address.keypair_path) -# Test changing the mint authority. -spl_token( - 'authorize', b_sol_mint_address.pubkey, 'mint', authorities['b_sol_mint_authority'] -) - -# Test creating Anker with a known bSOL minter, we do not test creating Anker -# without passing the `--b-sol-mint-address` flag because both implementations -# are similar. -print('\nCreating Anker instance with a known bSOL minter address...') -result = solido( - 'anker', - 'create', - '--b-sol-mint-address', - b_sol_mint_address.pubkey, - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--anker-program-id', - anker_program_id, - '--ust-mint-address', - ust_mint_address.pubkey, - '--token-swap-pool', - token_pool_address, - '--wormhole-core-bridge-program-id', - # Wormhole's testnet address. TODO: Replace with a new localhost program instance. - '3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5', - '--wormhole-token-bridge-program-id', - # Wormhole's testnet address. TODO: Replace with a new localhost program instance. - 'DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe', - '--terra-rewards-address', - terra_rewards_address, - '--sell-rewards-min-out-bps', - '0000', -) - -anker_address = result['anker_address'] -print(f'> Created instance at {anker_address}.') - - -print('\nVerifying Anker instance with `solido anker show` ...') -anker_show = solido('anker', 'show', '--anker-address', anker_address) - -# Check if `anker show-authorities` got it right. -expected_result = { - 'anker_address': authorities['anker_address'], - 'anker_program_id': anker_program_id, - 'solido_address': solido_address, - 'solido_program_id': solido_program_id, - 'b_sol_mint': b_sol_mint_address.pubkey, - 'st_sol_reserve': authorities['st_sol_reserve_account'], - 'ust_reserve': authorities['ust_reserve_account'], - 'b_sol_mint_authority': authorities['b_sol_mint_authority'], - 'reserve_authority': authorities['reserve_authority'], - 'terra_rewards_destination': terra_rewards_address, - 'token_swap_pool': token_pool_address, - 'token_swap_pool_ust_account': ust_pool_account, - 'token_swap_pool_st_sol_account': st_sol_pool_account, - 'sell_rewards_min_out_bps': 0, - 'ust_reserve_balance_micro_ust': 0, - 'st_sol_reserve_balance_st_lamports': 0, - 'st_sol_reserve_value_lamports': None, - 'b_sol_supply_b_lamports': 0, - 'historical_st_sol_price': [ - {'slot': 0, 'st_sol_price_in_micro_ust': 1_000_000}, - {'slot': 0, 'st_sol_price_in_micro_ust': 1_000_000}, - {'slot': 0, 'st_sol_price_in_micro_ust': 1_000_000}, - {'slot': 0, 'st_sol_price_in_micro_ust': 1_000_000}, - {'slot': 0, 'st_sol_price_in_micro_ust': 1_000_000}, - ], -} -assert anker_show == expected_result, f'Expected {anker_show} to be {expected_result}' -print('> Instance parameters are as expected.') - - -def perform_maintenance() -> Optional[Dict[str, Any]]: - result: Optional[Dict[str, Any]] = solido( - 'perform-maintenance', - '--solido-program-id', - solido_program_id, - '--solido-address', - solido_address, - '--anker-program-id', - anker_program_id, - '--stake-time', - 'anytime', - ) - return result - - -# There shouldn't be any maintenance to perform at this point. -result = perform_maintenance() -assert result is None, f'Did not expect maintenance here, but got {result}' - - -def deposit_solido_sol(amount_sol: float) -> str: - """ - Deposit SOL to Solido to get stSOL, return the recipient address. - """ - deposit_result = solido( - 'deposit', - '--solido-address', - solido_address, - '--solido-program-id', - solido_program_id, - '--amount-sol', - str(amount_sol), - ) - recipient: str = deposit_result['recipient'] - return recipient - - -# However, if we donate some stSOL to the reserve, then we should be able to -# sell that. -print('\nDonating 1 stSOL to the Anker reserve ...') -st_sol_account = deposit_solido_sol(1.0) -spl_token( - 'transfer', - st_sol_mint_address, - '1', - anker_st_sol_reserve_account, - '--from', - st_sol_account, -) - -anker_show = solido('anker', 'show', '--anker-address', anker_address) -assert anker_show['st_sol_reserve_balance_st_lamports'] == 1_000_000_000 -print('> Anker stSOL reserve now contains 1 SOL.') - -print('\nPerforming maintenance 5 times to populate the historical prices ...') -expected_price_update_result = {'FetchPoolPrice': {'st_sol_price_in_micro_ust': 500000}} -for i in range(5): - result = perform_maintenance() - assert ( - result == expected_price_update_result - ), f'Expected {result} to be {expected_price_update_result}' - - print(f'> ({i + 1}/5) Waiting for 100 slots for the next price update ...') - wait_for_slots(100) - -print('\nPerforming maintenance to swap that stSOL for UST ...') -result = perform_maintenance() -assert result == { - 'SellRewards': { - 'st_sol_amount_st_lamports': 1_000_000_000, - } -}, f'Expected SellRewards, but got {result}' - -anker_show = solido('anker', 'show', '--anker-address', anker_address) -assert anker_show['st_sol_reserve_balance_st_lamports'] == 0 -# The pool contained 1 stSOL and 1 UST, we doubled the amount of stSOL, so to -# keep the product constant, there is now 0.5 UST in the pool, and the other -# 0.5 UST went to Anker. -assert anker_show['ust_reserve_balance_micro_ust'] == 500_000 -print('> Anker stSOL reserve now contains 0.5 UST.') - - -print('\nDepositing 1 stSOL to Anker ...') -st_sol_account = deposit_solido_sol(1.0) -result = solido( - 'anker', - 'deposit', - '--anker-address', - anker_address, - '--from-st-sol-address', - st_sol_account, - '--amount-st-sol', - '1.0', -) -b_sol_account: str = result['b_sol_account'] -assert result['created_associated_b_sol_account'] == True - -b_sol_balance = spl_token_balance(b_sol_account) -assert b_sol_balance.balance_raw == 1_000_000_000 -print(f'> We now have 1 bSOL in account {b_sol_account}.') - -result = solido('anker', 'show', '--anker-address', anker_address) -assert result['st_sol_reserve_balance_st_lamports'] == 1_000_000_000 -assert result['b_sol_supply_b_lamports'] == 1_000_000_000 -print(f'> Anker reserve has 1 stSOL, the bSOL mint has a supply of 1 bSOL.') - -# We donate some stSOL once more, to check that when we withdraw, the user does -# not get more stSOL than they put in. -print('\nDonating 1 stSOL to the Anker reserve ...') -st_sol_account = deposit_solido_sol(1.0) -spl_token( - 'transfer', - st_sol_mint_address, - '1', - anker_st_sol_reserve_account, - '--from', - st_sol_account, -) - -print('Withdrawing 1 bSOL from Anker ...') -result = solido( - 'anker', - 'withdraw', - '--anker-address', - anker_address, - '--from-b-sol-address', - b_sol_account, - '--to-st-sol-address', - st_sol_account, - '--amount-b-sol', - '1.0', -) -assert result['from_b_sol_account'] == b_sol_account -assert result['to_st_sol_account'] == st_sol_account -assert result['created_associated_st_sol_account'] == False - -b_sol_balance = spl_token_balance(b_sol_account) -assert b_sol_balance.balance_raw == 0 -print(f'> bSOL balance of {b_sol_account} is now 0 again.') - -st_sol_balance = spl_token_balance(st_sol_account) -assert st_sol_balance.balance_raw == 1_000_000_000 -print(f'> stSOL balance of {st_sol_account} is now 1.0 stSOL again.') - -anker_show = solido('anker', 'show', '--anker-address', anker_address) -assert anker_show['st_sol_reserve_balance_st_lamports'] == 1_000_000_000 -assert anker_show['b_sol_supply_b_lamports'] == 0 -print(f'> Anker reserve has 1 stSOL, the bSOL mint has a supply of 0 bSOL.') - -print('\nTesting manager functions ...') -print('> Changing Terra rewards destination') -new_terra_rewards_destination = 'terra14dycr8jm7e5kw88g4studekkzzw5xc5ffnp4hk' -transaction_result = solido( - 'anker', - 'change-terra-rewards-destination', - '--anker-address', - anker_address, - '--multisig-address', - multisig_instance, - '--multisig-program-id', - multisig_program_id, - '--terra-rewards-destination', - new_terra_rewards_destination, - keypair_path=test_addrs[0].keypair_path, -) -transaction_address = transaction_result['transaction_address'] -approve_and_execute(transaction_address) - -print('> Changing Token Swap Pool') -print(' Creating new token pool instance ...') - -new_ust_pool_account = create_spl_token_account( - test_addrs[1].keypair_path, ust_mint_address.pubkey -) -new_st_sol_pool_account = create_spl_token_account( - test_addrs[1].keypair_path, st_sol_mint_address -) -spl_token('transfer', st_sol_mint_address, '1', new_st_sol_pool_account) -spl_token('mint', ust_mint_address.pubkey, '1', new_ust_pool_account) - -result = solido( - 'anker', - 'create-token-pool', - '--token-swap-program-id', - orca_token_swap_program_id, - '--st-sol-account', - new_st_sol_pool_account, - '--ust-account', - new_ust_pool_account, - '--ust-mint-address', - ust_mint_address.pubkey, - keypair_path=test_addrs[1].keypair_path, -) -new_token_pool_address = result['pool_address'] -print(f' Created instance at {new_token_pool_address}.') - -transaction_result = solido( - 'anker', - 'change-token-swap-pool', - '--anker-address', - anker_address, - '--multisig-address', - multisig_instance, - '--multisig-program-id', - multisig_program_id, - '--token-swap-pool', - new_token_pool_address, - keypair_path=test_addrs[0].keypair_path, -) -transaction_address = transaction_result['transaction_address'] -approve_and_execute(transaction_address) - -print('> Changing min out basis points') -new_min_out_bps = anker_show['sell_rewards_min_out_bps'] + 10 -transaction_result = solido( - 'anker', - 'change-sell-rewards-min-out-bps', - '--anker-address', - anker_address, - '--multisig-address', - multisig_instance, - '--multisig-program-id', - multisig_program_id, - '--sell-rewards-min-out-bps', - str(new_min_out_bps), - keypair_path=test_addrs[0].keypair_path, -) -transaction_address = transaction_result['transaction_address'] -approve_and_execute(transaction_address) - -print('\nVerifying Anker instance with `solido anker show` ...') -# See if `anker show` shows the correct output -anker_show = solido('anker', 'show', '--anker-address', anker_address) - -# Check if `anker show-authorities` got it right. -expected_result = { - 'anker_address': authorities['anker_address'], - 'anker_program_id': anker_program_id, - 'solido_address': solido_address, - 'solido_program_id': solido_program_id, - 'b_sol_mint': b_sol_mint_address.pubkey, - 'st_sol_reserve': authorities['st_sol_reserve_account'], - 'ust_reserve': authorities['ust_reserve_account'], - 'b_sol_mint_authority': authorities['b_sol_mint_authority'], - 'reserve_authority': authorities['reserve_authority'], - 'terra_rewards_destination': new_terra_rewards_destination, - 'token_swap_pool': new_token_pool_address, - 'token_swap_pool_ust_account': new_ust_pool_account, - 'token_swap_pool_st_sol_account': new_st_sol_pool_account, - 'sell_rewards_min_out_bps': new_min_out_bps, - 'ust_reserve_balance_micro_ust': 500_000, - 'st_sol_reserve_balance_st_lamports': 1_000_000_000, - 'st_sol_reserve_value_lamports': None, - 'b_sol_supply_b_lamports': 0, - 'historical_st_sol_price': [ - { - 'slot': anker_show['historical_st_sol_price'][i]['slot'], - 'st_sol_price_in_micro_ust': 500000, - } - for i in range(5) - ], -} -assert anker_show == expected_result, f'Expected {anker_show} to be {expected_result}' -print('> Instance parameters are as expected.') diff --git a/tests/test_multisig.py b/tests/test_multisig.py index aef872887..c08d65996 100755 --- a/tests/test_multisig.py +++ b/tests/test_multisig.py @@ -127,14 +127,7 @@ # derived address should be able to. print('\nAttempting direct upgrade, which should fail ...') try: - solana( - 'program', - 'deploy', - '--program-id', - program_id, - '--buffer', - buffer_address, - ) + solana('program', 'deploy', '--program-id', program_id, '--buffer', buffer_address) except subprocess.CalledProcessError as err: assert err.returncode == 1 new_info = solana_program_show(program_id) @@ -224,7 +217,7 @@ upgrade_transaction_address, keypair_path=addr2.keypair_path, ) -assert result['num_approvals'] == 2 +assert result['num_approvals'][0] == 2 assert result['threshold'] == 2 result = multisig( @@ -339,7 +332,7 @@ change_multisig_transaction_address, keypair_path=addr3.keypair_path, ) -assert result['num_approvals'] == 2 +assert result['num_approvals'][0] == 2 assert result['threshold'] == 2 result = multisig( @@ -360,15 +353,8 @@ 'MultisigChange': { 'old_threshold': 2, 'new_threshold': 2, - 'old_owners': [ - addr1.pubkey, - addr2.pubkey, - addr3.pubkey, - ], - 'new_owners': [ - addr1.pubkey, - addr2.pubkey, - ], + 'old_owners': [addr1.pubkey, addr2.pubkey, addr3.pubkey], + 'new_owners': [addr1.pubkey, addr2.pubkey], } } print('> Transaction has the required number of signatures.') @@ -423,10 +409,7 @@ upgrade_transaction_address, ) assert 'Outdated' in result['signers'] -assert result['signers']['Outdated'] == { - 'num_signed': 2, - 'num_owners': 3, -} +assert result['signers']['Outdated'] == {'num_signed': 2, 'num_owners': 3} print('> Owners ids are gone, but approval count is preserved as expected.') diff --git a/tests/test_solido.py b/tests/test_solido.py index 9f27f67ff..0bfa1da0c 100755 --- a/tests/test_solido.py +++ b/tests/test_solido.py @@ -57,6 +57,16 @@ developer_account_owner = create_test_account('tests/.keys/developer-fee-key.json') print(f'> Developer fee account owner: {developer_account_owner}') +validator_list_account_owner = create_test_account( + 'tests/.keys/validator-list-key.json', fund=False +) +print(f'> Validator list account owner: {validator_list_account_owner}') + +maintainer_list_account_owner = create_test_account( + 'tests/.keys/maintainer-list-key.json', fund=False +) +print(f'> Maintainer list account owner: {maintainer_list_account_owner}') + print('\nUploading Solido program ...') solido_program_id = solana_program_deploy(get_solido_program_path() + '/lido.so') print(f'> Solido program id is {solido_program_id}.') @@ -82,10 +92,7 @@ print(f'> Created instance at {multisig_instance}.') -def approve_and_execute( - transaction_to_approve: str, - signer: TestAccount, -) -> None: +def approve_and_execute(transaction_to_approve: str, signer: TestAccount) -> None: """ Helper to approve and execute a transaction with a single key. """ @@ -182,6 +189,10 @@ def approve_and_execute( treasury_account_owner.pubkey, '--developer-account-owner', developer_account_owner.pubkey, + '--validator-list-key-path', + validator_list_account_owner.keypair_path, + '--maintainer-list-key-path', + maintainer_list_account_owner.keypair_path, '--multisig-address', multisig_instance, keypair_path=test_addrs[0].keypair_path, @@ -191,9 +202,21 @@ def approve_and_execute( treasury_account = result['treasury_account'] developer_account = result['developer_account'] st_sol_mint_account = result['st_sol_mint_address'] +validator_list_address = result['validator_list_address'] +maintainer_list_address = result['maintainer_list_address'] print(f'> Created instance at {solido_address}.') +output = { + "multisig_program_id": multisig_program_id, + "multisig_address": multisig_instance, + "solido_program_id": solido_program_id, + "solido_address": solido_address, + "st_sol_mint": st_sol_mint_account, +} +with open('../solido_test.json', 'w') as outfile: + json.dump(output, outfile, indent=4) + solido_instance = solido( 'show-solido', '--solido-program-id', @@ -307,21 +330,14 @@ def add_validator( solido_address, ) -assert solido_instance['solido']['validators']['entries'][0] == { +assert solido_instance['validators']['entries'][0] == { 'pubkey': validator.vote_account.pubkey, - 'entry': { - 'stake_seeds': { - 'begin': 0, - 'end': 0, - }, - 'unstake_seeds': { - 'begin': 0, - 'end': 0, - }, - 'stake_accounts_balance': 0, - 'unstake_accounts_balance': 0, - 'active': True, - }, + 'stake_seeds': {'begin': 0, 'end': 0}, + 'unstake_seeds': {'begin': 0, 'end': 0}, + 'stake_accounts_balance': 0, + 'unstake_accounts_balance': 0, + 'effective_stake_balance': 0, + 'active': True, }, f'Unexpected validator entry, in {json.dumps(solido_instance, indent=True)}' maintainer = create_test_account('tests/.keys/maintainer-account-key.json') @@ -353,10 +369,8 @@ def add_validator( '--solido-address', solido_address, ) -assert solido_instance['solido']['maintainers']['entries'][0] == { - 'pubkey': maintainer.pubkey, - 'entry': None, -} + +assert solido_instance['maintainers']['entries'][0] == {'pubkey': maintainer.pubkey} print(f'> Removing maintainer {maintainer}') transaction_result = solido( @@ -383,7 +397,7 @@ def add_validator( solido_address, ) -assert len(solido_instance['solido']['maintainers']['entries']) == 0 +assert len(solido_instance['maintainers']['entries']) == 0 print(f'> Adding maintainer {maintainer} again') transaction_result = solido( @@ -419,6 +433,21 @@ def perform_maintenance() -> Any: ) +def consume_maintainence_instructions(verbose: bool = False) -> Any: + """ + Perform maintenance instructions till no more left + """ + last_result = None + while True: + maintainance_result = perform_maintenance() + if maintainance_result is not None: + last_result = maintainance_result + if verbose: + print(maintainance_result) + else: + return last_result + + print('\nRunning maintenance (should be no-op if epoch is unchanged) ...') result = perform_maintenance() if solido_instance['solido']['exchange_rate']['computed_in_epoch'] == current_epoch: @@ -488,10 +517,7 @@ def deposit(lamports: int, expect_created_token_account: bool = False) -> None: def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Validator: # Adding another validator - (validator, transaction_result) = add_validator( - 'validator-account-key-1', - 'validator-vote-account-key-1', - ) + (validator, transaction_result) = add_validator(keypath_account, keypath_vote) transaction_address = transaction_result['transaction_address'] approve_and_execute(transaction_address, test_addrs[0]) @@ -531,16 +557,16 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida } assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -# By donating to the stake account, we trigger maintenance to run WithdrawInactiveStake. +# By donating to the stake account, we trigger maintenance to run UpdateStakeAccountBalance. print( f'\nDonating to stake account {stake_account_address}, then running maintenance ...' ) solana('transfer', stake_account_address, '0.1') result = perform_maintenance() -assert 'WithdrawInactiveStake' in result +assert 'UpdateStakeAccountBalance' in result expected_result = { - 'WithdrawInactiveStake': { + 'UpdateStakeAccountBalance': { 'validator_vote_account': validator.vote_account.pubkey, 'expected_difference_stake_lamports': 100_000_000, # We donated 0.1 SOL. 'unstake_withdrawn_to_reserve_lamports': 1_499_750_000, # Amount that was unstaked for the newcomming validator. @@ -553,7 +579,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -print('> Performed WithdrawInactiveStake as expected.') +print('> Performed UpdateStakeAccountBalance as expected.') print('\nDonating 1.0 SOL to reserve, then running maintenance ...') @@ -624,7 +650,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida '--solido-address', solido_address, ) -assert not solido_instance['solido']['validators']['entries'][0]['entry'][ +assert not solido_instance['validators']['entries'][0][ 'active' ], 'Validator should be inactive after deactivation.' print('> Validator is inactive as expected.') @@ -652,7 +678,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida solido_address, ) # Should have bumped the validator's `stake_seeds` and `unstake_seeds`. -val = solido_instance['solido']['validators']['entries'][0]['entry'] +val = solido_instance['validators']['entries'][0] assert val['stake_seeds'] == {'begin': 1, 'end': 1} assert val['unstake_seeds'] == {'begin': 1, 'end': 2} @@ -660,7 +686,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida print('\nRunning maintenance (should withdraw from validator\'s unstake account) ...') result = perform_maintenance() expected_result = { - 'WithdrawInactiveStake': { + 'UpdateStakeAccountBalance': { 'validator_vote_account': validator.vote_account.pubkey, 'expected_difference_stake_lamports': 0, 'unstake_withdrawn_to_reserve_lamports': 1_500_250_000, @@ -681,9 +707,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida print('\nRunning maintenance (should remove the validator) ...') result = perform_maintenance() expected_result = { - 'RemoveValidator': { - 'validator_vote_account': validator.vote_account.pubkey, - } + 'RemoveValidator': {'validator_vote_account': validator.vote_account.pubkey} } assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' @@ -694,7 +718,7 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida '--solido-address', solido_address, ) -number_validators = len(solido_instance['solido']['validators']['entries']) +number_validators = len(solido_instance['validators']['entries']) assert ( number_validators == 1 ), f'\nExpected no validators\nGot: {number_validators} validators' @@ -720,19 +744,6 @@ def add_validator_and_approve(keypath_account: str, keypath_vote: str) -> Valida assert result == expected_result, f'\nExpected: {expected_result}\nActual: {result}' -def consume_maintainence_instructions(verbose: bool = False) -> None: - """ - Perform maintenance instructions till no more left - """ - while True: - maintainance_result = perform_maintenance() - if maintainance_result is not None: - if verbose: - print(maintainance_result) - else: - break - - print('\nConsuming all maintainence instructions') consume_maintainence_instructions(False) @@ -796,3 +807,75 @@ def set_max_validation_commission(fee: int) -> Any: assert ( maintainance_result == expected_result ), f'\nExpected: {expected_result}\nActual: {maintainance_result}' + +############################################################################# + +print( + '\nRestore max validation commission to %d%% ...' + % (MAX_VALIDATION_COMMISSION_PERCENTAGE) +) +transaction_status = set_max_validation_commission(MAX_VALIDATION_COMMISSION_PERCENTAGE) +assert transaction_status['did_execute'] == True + +validator_3 = add_validator_and_approve( + 'validator-account-key-3', 'validator-vote-account-key-3' +) +solido_instance = solido( + 'show-solido', + '--solido-program-id', + solido_program_id, + '--solido-address', + solido_address, +) +number_validators = len(solido_instance['validators']['entries']) +assert ( + number_validators == 2 +), f'\nExpected 2 validators\nGot: {number_validators} validators' + +print(f'\nClosing vote account {validator_3.vote_account.pubkey}') +solana( + "close-vote-account", + validator_3.vote_account.pubkey, + "tests/.keys/test-key-1.json", + "--authorized-withdrawer", + validator_3.withdrawer_account.keypair_path, +) + +print('\nConsuming all maintainence instructions (should remove all validators) ...') +consume_maintainence_instructions(False) +solido_instance = solido( + 'show-solido', + '--solido-program-id', + solido_program_id, + '--solido-address', + solido_address, +) +number_validators = len(solido_instance['validators']['entries']) +assert number_validators == 0 + + +# def test_rewards(): +# def balance(v1, v2, v3, reserve): +# return v1 + v2 + v3 + reserve + +# s1_before = balance(8.048978427, 9.054659727, 7.948410296, 2.4 + 0.00089088) +# s1 = balance(9.150682817, 9.054659727, 9.150682816, 0.09691397) +# assert s1_before == s1 +# s2 = balance(9.210892341, 9.126038084, 9.212256215, 0.10147973) +# rewards = s2 - s1 + +# def rewards_from_fees(t1, d1, a1, t2, d2, a2): +# dt = t2 - t1 +# print(f"dt {dt}, {0.04*rewards}, {dt-0.04*rewards}") +# dd = d2 - d1 +# print(f"dd {dd}, {0.01*rewards}, {dd-0.01*rewards}") +# da = a2 - a1 +# print(f"da {da}, {0.95*rewards}, {da-0.95*rewards}") +# return dt + dd + da + +# rewards_alt = rewards_from_fees( +# 0.008198959, 0.003279583, 0.147581266, 0.016108040, 0.005256852, 0.335421956 +# ) + +# diff = rewards - rewards_alt +# print(diff) diff --git a/tests/util.py b/tests/util.py index e76205274..b10049831 100644 --- a/tests/util.py +++ b/tests/util.py @@ -27,6 +27,10 @@ def __repr__(self) -> str: def run(*args: str) -> str: + return str(run_process(*args).stdout) + + +def run_process(*args: str) -> Any: """ Run a program, ensure it exits with code 0, return its stdout. """ @@ -45,7 +49,7 @@ def run(*args: str) -> str: print('Stderr:', err.stderr) raise - return result.stdout + return result def get_solido_program_path() -> str: @@ -86,7 +90,8 @@ def solido(*args: str, keypair_path: Optional[str] = None) -> Any: *args, ) if keypair_path is not None and keypair_path.startswith('usb://ledger'): - output = '\n'.join(output.split('\n')[2:]) + # get json at the end of output + output = output[output.find("{") :] if output == '': return {} else: @@ -199,11 +204,7 @@ def create_stake_account(keypair_fname: str) -> TestAccount: Generate a stake account funded with 2 Sol, returns its public key. """ test_account = create_test_account(keypair_fname, fund=False) - solana( - 'create-stake-account', - keypair_fname, - '2', - ) + solana('create-stake-account', keypair_fname, '2') return test_account @@ -229,13 +230,7 @@ def create_vote_account( # Publish validator info for this new validator, because `show-solido` # requires validator info to be present. name = f'Validator for {vote_key_fname}' - solana( - 'validator-info', - 'publish', - '--keypair', - validator_key_fname, - name, - ) + solana('validator-info', 'publish', '--keypair', validator_key_fname, name) return test_account, withdrawer_account @@ -283,7 +278,8 @@ def multisig(*args: str, keypair_path: Optional[str] = None) -> Any: # ✅ Approved # These lines should be ignored if keypair_path is not None and keypair_path.startswith('usb://ledger'): - output = '\n'.join(output.split('\n')[2:]) + # get json at the end of output + output = output[output.find("{") :] if output == '': return {} else: @@ -296,10 +292,7 @@ def multisig(*args: str, keypair_path: Optional[str] = None) -> Any: def get_approve_and_execute( - *, - multisig_program_id: str, - multisig_instance: str, - signer_keypair_paths: List[str], + *, multisig_program_id: str, multisig_instance: str, signer_keypair_paths: List[str] ) -> Callable[[str], None]: """ Return a function, `approve_and_execute`, which approves and executes the @@ -356,19 +349,12 @@ def solana_rpc(method: str, params: List[Any]) -> Any: suitable for serious use, but for tests or checking things on devnet it's useful. """ - body = { - 'jsonrpc': '2.0', - 'id': str(uuid4()), - 'method': method, - 'params': params, - } + body = {'jsonrpc': '2.0', 'id': str(uuid4()), 'method': method, 'params': params} req = request.Request( get_network(), method='POST', data=json.dumps(body).encode('utf-8'), - headers={ - 'Content-Type': 'application/json', - }, + headers={'Content-Type': 'application/json'}, ) response = request.urlopen(req) return json.load(response) @@ -379,8 +365,7 @@ def rpc_get_account_info(address: str) -> Optional[Dict[str, Any]]: Call getAccountInfo, see https://docs.solana.com/developing/clients/jsonrpc-api#getaccountinfo. """ result: Dict[str, Any] = solana_rpc( - method='getAccountInfo', - params=[address, {'encoding': 'jsonParsed'}], + method='getAccountInfo', params=[address, {'encoding': 'jsonParsed'}] ) # The value is either an object with decoded account info, or None, if the # account does not exist.