diff --git a/Cargo.lock b/Cargo.lock index 2b489a587..9f75faf9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3364,6 +3364,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mpl-core" +version = "0.8.1-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416812b4df9d6432f53e591609cfa914b72c4c09ebbdfc1e6a32310b77228d59" +dependencies = [ + "base64 0.22.1", + "borsh 0.10.4", + "modular-bitfield", + "num-derive 0.3.3", + "num-traits", + "rmp-serde", + "serde_json", + "solana-program", + "thiserror 1.0.69", +] + [[package]] name = "multimap" version = "0.8.3" @@ -3490,6 +3507,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -4504,6 +4532,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rocksdb" version = "0.22.0" @@ -5294,7 +5344,7 @@ dependencies = [ "bincode", "bytemuck", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-feature-set", "solana-log-collector", @@ -6359,7 +6409,7 @@ dependencies = [ "log", "memoffset", "num-bigint 0.4.6", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot 0.12.3", "rand 0.8.5", @@ -6471,7 +6521,7 @@ dependencies = [ "itertools 0.12.1", "libc", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "percentage", "rand 0.8.5", @@ -6623,7 +6673,7 @@ dependencies = [ "dialoguer", "hidapi", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "parking_lot 0.12.3", "qstring", @@ -6799,7 +6849,7 @@ dependencies = [ "memmap2 0.5.10", "mockall", "modular-bitfield", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_cpus", "num_enum", @@ -6904,7 +6954,7 @@ dependencies = [ "libsecp256k1", "log", "memmap2 0.5.10", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "pbkdf2 0.11.0", @@ -7562,7 +7612,7 @@ checksum = "16362e21cdee5b7a55a353f1d6adae59149dab696998ad85fda151ee621aad00" dependencies = [ "bincode", "log", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", @@ -7604,7 +7654,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d6cd47dbf91d4aafa3561de67420db02a85179ff59573b8bd6085c89057615c" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-log-collector", "solana-program-runtime", @@ -7628,7 +7678,7 @@ dependencies = [ "js-sys", "lazy_static", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -7651,7 +7701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f440b760dba3c32de8b1d8bc5a2118bca79fec1e6435952824a9ccc37f6276c" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-feature-set", "solana-log-collector", @@ -7676,7 +7726,7 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "merlin", - "num-derive", + "num-derive 0.4.2", "num-traits", "rand 0.8.5", "serde", @@ -7731,7 +7781,7 @@ version = "2.3.0" dependencies = [ "assert_matches", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-token 4.0.0", @@ -7747,7 +7797,7 @@ checksum = "68034596cf4804880d265f834af1ff2f821ad5293e41fa0f8f59086c181fc38e" dependencies = [ "assert_matches", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "spl-token 6.0.0", @@ -7755,6 +7805,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-associated-token-account-client" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f8349dbcbe575f354f9a533a21f272f3eb3808a49e2fdc1c34393b88ba76cb" +dependencies = [ + "solana-instruction", + "solana-pubkey", +] + [[package]] name = "spl-associated-token-account-test" version = "0.0.1" @@ -7772,7 +7832,7 @@ name = "spl-binary-oracle-pair" version = "0.1.0" dependencies = [ "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "solana-program-test", @@ -7816,6 +7876,18 @@ dependencies = [ "spl-discriminator-derive 0.2.0", ] +[[package]] +name = "spl-discriminator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7398da23554a31660f17718164e31d31900956054f54f52d5ec1be51cb4f4b3" +dependencies = [ + "bytemuck", + "solana-program-error", + "solana-sha256-hasher", + "spl-discriminator-derive 0.2.0", +] + [[package]] name = "spl-discriminator-derive" version = "0.1.1" @@ -7860,6 +7932,19 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-elgamal-registry" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a157622a63a4d12fbd8b347fd75ee442cb913137fa98647824c992fb049a15b" +dependencies = [ + "bytemuck", + "solana-program", + "solana-zk-sdk", + "spl-pod 0.5.0", + "spl-token-confidential-transfer-proof-extraction", +] + [[package]] name = "spl-example-cross-program-invocation" version = "1.0.0" @@ -7960,7 +8045,7 @@ dependencies = [ "base64 0.21.7", "bincode", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "serde", @@ -7972,7 +8057,9 @@ dependencies = [ "spl-governance-addin-mock", "spl-governance-test-sdk", "spl-governance-tools", - "spl-token 4.0.0", + "spl-token-2022 6.0.0", + "spl-transfer-hook-example", + "spl-transfer-hook-interface 0.9.0", "thiserror 1.0.69", ] @@ -7993,7 +8080,7 @@ dependencies = [ "assert_matches", "bincode", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "serde", @@ -8016,7 +8103,7 @@ dependencies = [ "assert_matches", "bincode", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "serde", @@ -8041,14 +8128,20 @@ dependencies = [ "bincode", "borsh 1.5.3", "lazy_static", - "num-derive", + "mpl-core", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", "solana-program", "solana-program-test", "solana-sdk", + "spl-tlv-account-resolution 0.9.0", "spl-token 4.0.0", + "spl-token-2022 6.0.0", + "spl-token-client 0.13.0", + "spl-transfer-hook-example", + "spl-transfer-hook-interface 0.9.0", "thiserror 1.0.69", ] @@ -8059,12 +8152,11 @@ dependencies = [ "arrayref", "bincode", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "serde", "serde_derive", "solana-program", - "spl-token 4.0.0", "thiserror 1.0.69", ] @@ -8098,7 +8190,7 @@ version = "0.2.0" dependencies = [ "borsh 1.5.3", "libm", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "solana-program", @@ -8126,6 +8218,20 @@ dependencies = [ "solana-program", ] +[[package]] +name = "spl-memo" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f09647c0974e33366efeb83b8e2daebb329f0420149e74d3a4bd2c08cf9f7cb" +dependencies = [ + "solana-account-info", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-pubkey", +] + [[package]] name = "spl-merkle-tree-reference" version = "0.1.0" @@ -8139,7 +8245,7 @@ name = "spl-name-service" version = "0.3.0" dependencies = [ "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "solana-program-test", @@ -8175,12 +8281,32 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-pod" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a7d5950993e1ff2680bd989df298eeb169367fb2f9deeef1f132de6e4e8016" +dependencies = [ + "borsh 1.5.3", + "bytemuck", + "bytemuck_derive", + "num-derive 0.4.2", + "num-traits", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "solana-program-option", + "solana-pubkey", + "solana-zk-sdk", + "thiserror 1.0.69", +] + [[package]] name = "spl-program-error" version = "0.3.0" dependencies = [ "lazy_static", - "num-derive", + "num-derive 0.4.2", "num-traits", "serial_test", "solana-program", @@ -8195,7 +8321,20 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b28bed65356558133751cc32b48a7a5ddfc59ac4e941314630bbed1ac10532" dependencies = [ - "num-derive", + "num-derive 0.4.2", + "num-traits", + "solana-program", + "spl-program-error-derive 0.4.1", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-program-error" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b5186f42b2b50168029d81e58e800b690877ef0b30580d107659250da1d1" +dependencies = [ + "num-derive 0.4.2", "num-traits", "solana-program", "spl-program-error-derive 0.4.1", @@ -8229,7 +8368,7 @@ name = "spl-record" version = "0.1.0" dependencies = [ "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "solana-program", "solana-program-test", @@ -8238,6 +8377,27 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-record" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1288810a85bbe7e62ee3c6f7b8119e8c1016e90351411d12e4132e98c7ca7344" +dependencies = [ + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-entrypoint", + "solana-program-error", + "solana-program-pack", + "solana-pubkey", + "solana-rent", + "thiserror 1.0.69", +] + [[package]] name = "spl-shared-memory" version = "2.0.6" @@ -8256,7 +8416,7 @@ dependencies = [ "arrayref", "bincode", "borsh 1.5.3", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "rand 0.8.5", @@ -8298,7 +8458,7 @@ dependencies = [ "spl-associated-token-account 2.3.0", "spl-single-pool", "spl-token 4.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "tempfile", "test-case", "tokio", @@ -8313,7 +8473,7 @@ dependencies = [ "bincode", "borsh 1.5.3", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "proptest", @@ -8389,6 +8549,28 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-tlv-account-resolution" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd99ff1e9ed2ab86e3fd582850d47a739fec1be9f4661cba1782d3a0f26805f3" +dependencies = [ + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator 0.4.1", + "spl-pod 0.5.0", + "spl-program-error 0.6.0", + "spl-type-length-value 0.7.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token" version = "4.0.0" @@ -8396,7 +8578,7 @@ dependencies = [ "arrayref", "bytemuck", "lazy_static", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "proptest", @@ -8415,7 +8597,22 @@ checksum = "70a0f06ac7f23dc0984931b1fe309468f14ea58e32660439c1cef19456f5d0e3" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", + "num-traits", + "num_enum", + "solana-program", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed320a6c934128d4f7e54fe00e16b8aeaecf215799d060ae14f93378da6dc834" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -8430,7 +8627,7 @@ dependencies = [ "base64 0.21.7", "bytemuck", "lazy_static", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "proptest", @@ -8462,7 +8659,7 @@ checksum = "d9c10f3483e48679619c76598d4e4aebb955bc49b0a5cc63323afbf44135c9bf" dependencies = [ "arrayref", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -8478,6 +8675,34 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-token-2022" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b27f7405010ef816587c944536b0eafbcc35206ab6ba0f2ca79f1d28e488f4f" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum", + "solana-program", + "solana-security-txt", + "solana-zk-sdk", + "spl-elgamal-registry", + "spl-memo 6.0.0", + "spl-pod 0.5.0", + "spl-token 7.0.0", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface 0.5.0", + "spl-token-metadata-interface 0.6.0", + "spl-transfer-hook-interface 0.9.0", + "spl-type-length-value 0.7.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-2022-test" version = "0.0.1" @@ -8493,7 +8718,7 @@ dependencies = [ "spl-memo 4.0.0", "spl-pod 0.1.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", "spl-transfer-hook-example", @@ -8530,7 +8755,7 @@ dependencies = [ "spl-memo 4.0.0", "spl-token 4.0.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-metadata-interface 0.2.0", "strum 0.25.0", "strum_macros 0.25.3", @@ -8562,6 +8787,37 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "spl-token-client" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9155237581388a928822ce91caf936cf54406d27bf21def44f18b25e304ba0e" +dependencies = [ + "async-trait", + "bincode", + "bytemuck", + "futures 0.3.31", + "futures-util", + "solana-banks-interface", + "solana-cli-output", + "solana-program-test", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "spl-associated-token-account-client", + "spl-elgamal-registry", + "spl-memo 6.0.0", + "spl-record 0.3.0", + "spl-token 7.0.0", + "spl-token-2022 6.0.0", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface 0.5.0", + "spl-token-metadata-interface 0.6.0", + "spl-transfer-hook-interface 0.9.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-collection" version = "0.1.0" @@ -8573,13 +8829,50 @@ dependencies = [ "spl-pod 0.1.0", "spl-program-error 0.3.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-group-example", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", "spl-type-length-value 0.3.0", ] +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1bf731fc65546330a7929a9735679add70f828dd076a4e69b59d3afb5423c" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383937e637ccbe546f736d5115344351ebd4d2a076907582335261da58236816" +dependencies = [ + "bytemuck", + "solana-curve25519", + "solana-program", + "solana-zk-sdk", + "spl-pod 0.5.0", + "thiserror 1.0.69", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-generation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8627184782eec1894de8ea26129c61303f1f0adeed65c20e0b10bc584f09356d" +dependencies = [ + "curve25519-dalek 4.1.3", + "solana-zk-sdk", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-group-example" version = "0.1.0" @@ -8590,7 +8883,7 @@ dependencies = [ "spl-discriminator 0.1.0", "spl-pod 0.1.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-group-interface 0.1.0", "spl-token-metadata-interface 0.2.0", "spl-type-length-value 0.3.0", @@ -8621,6 +8914,25 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-token-group-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d595667ed72dbfed8c251708f406d7c2814a3fa6879893b323d56a10bedfc799" +dependencies = [ + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator 0.4.1", + "spl-pod 0.5.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-lending" version = "0.2.0" @@ -8628,7 +8940,7 @@ dependencies = [ "arrayref", "assert_matches", "bytemuck", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "solana-program", @@ -8663,7 +8975,7 @@ dependencies = [ "solana-sdk", "spl-pod 0.1.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-metadata-interface 0.2.0", "spl-type-length-value 0.3.0", "test-case", @@ -8697,6 +9009,27 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-token-metadata-interface" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb9c89dbc877abd735f05547dcf9e6e12c00c11d6d74d8817506cab4c99fdbb" +dependencies = [ + "borsh 1.5.3", + "num-derive 0.4.2", + "num-traits", + "solana-borsh", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator 0.4.1", + "spl-pod 0.5.0", + "spl-type-length-value 0.7.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-token-swap" version = "3.0.0" @@ -8704,7 +9037,7 @@ dependencies = [ "arbitrary", "arrayref", "enum_dispatch", - "num-derive", + "num-derive 0.4.2", "num-traits", "proptest", "roots", @@ -8733,7 +9066,7 @@ dependencies = [ name = "spl-token-upgrade" version = "0.1.1" dependencies = [ - "num-derive", + "num-derive 0.4.2", "num-traits", "num_enum", "solana-program", @@ -8741,7 +9074,7 @@ dependencies = [ "solana-sdk", "spl-token 4.0.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "test-case", "thiserror 1.0.69", ] @@ -8762,7 +9095,7 @@ dependencies = [ "spl-associated-token-account 2.3.0", "spl-token 4.0.0", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-token-upgrade", "tokio", "walkdir", @@ -8799,7 +9132,7 @@ dependencies = [ "solana-test-validator", "spl-tlv-account-resolution 0.5.1", "spl-token-2022 1.0.0", - "spl-token-client", + "spl-token-client 0.8.0", "spl-transfer-hook-interface 0.4.1", "strum 0.25.0", "strum_macros 0.25.3", @@ -8814,10 +9147,10 @@ dependencies = [ "solana-program", "solana-program-test", "solana-sdk", - "spl-tlv-account-resolution 0.5.1", - "spl-token-2022 1.0.0", - "spl-transfer-hook-interface 0.4.1", - "spl-type-length-value 0.3.0", + "spl-tlv-account-resolution 0.9.0", + "spl-token-2022 6.0.0", + "spl-transfer-hook-interface 0.9.0", + "spl-type-length-value 0.7.0", ] [[package]] @@ -8850,6 +9183,31 @@ dependencies = [ "spl-type-length-value 0.5.0", ] +[[package]] +name = "spl-transfer-hook-interface" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa7503d52107c33c88e845e1351565050362c2314036ddf19a36cd25137c043" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-account-info", + "solana-cpi", + "solana-decode-error", + "solana-instruction", + "solana-msg", + "solana-program-error", + "solana-pubkey", + "spl-discriminator 0.4.1", + "spl-pod 0.5.0", + "spl-program-error 0.6.0", + "spl-tlv-account-resolution 0.9.0", + "spl-type-length-value 0.7.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-type-length-value" version = "0.3.0" @@ -8875,6 +9233,24 @@ dependencies = [ "spl-program-error 0.5.0", ] +[[package]] +name = "spl-type-length-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba70ef09b13af616a4c987797870122863cba03acc4284f226a4473b043923f9" +dependencies = [ + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-account-info", + "solana-decode-error", + "solana-msg", + "solana-program-error", + "spl-discriminator 0.4.1", + "spl-pod 0.5.0", + "thiserror 1.0.69", +] + [[package]] name = "spl-type-length-value-derive" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8bf0b21fb..cd5d3707c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,6 @@ resolver = "2" solana-program = "2.1.7" borsh = "1.5.1" spl-token-2022 = { version = "6.0.0", features = ["no-entrypoint"] } -spl-transfer-hook-interface = { version = "0.8.2" } solana-program-test = "2.1.7" solana-sdk = "2.1.7" solana-account-decoder = "2.1.7" @@ -95,3 +94,14 @@ solana-banks-interface = "2.1.7" solana-rpc-client = "2.1.7" solana-rpc-client-api = "2.1.7" solana-zk-token-sdk = "2.1.7" +spl-tlv-account-resolution = "0.9.0" +spl-transfer-hook-interface = "0.9.0" +spl-token-client = "0.13.0" + + +[workspace.lints.rust.unexpected_cfgs] +level = "warn" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/governance/CHANGELOG.md b/governance/CHANGELOG.md index a427dec04..16513fa08 100644 --- a/governance/CHANGELOG.md +++ b/governance/CHANGELOG.md @@ -1,5 +1,11 @@ # SPL Governance Changelog +## v3.1.2 - + +- Add deprecated warning for create_mint_governance() and create_token_governance() (it will be removed in v4.0.0) +- Add SPL Token 2022 support +- Add VersionedTransaction support for proposals + ## v3.1.1 - 25 Apr 2022 - Weighted multi choice voting diff --git a/governance/addin-mock/program/Cargo.toml b/governance/addin-mock/program/Cargo.toml index 782bed295..860f6c084 100644 --- a/governance/addin-mock/program/Cargo.toml +++ b/governance/addin-mock/program/Cargo.toml @@ -36,3 +36,6 @@ spl-governance-test-sdk = { version = "0.1.3", path ="../../test-sdk"} [lib] crate-type = ["cdylib", "lib"] + +[lints] +workspace = true \ No newline at end of file diff --git a/governance/program/Cargo.toml b/governance/program/Cargo.toml index 9b20629c8..c1b451370 100644 --- a/governance/program/Cargo.toml +++ b/governance/program/Cargo.toml @@ -8,8 +8,11 @@ license = "Apache-2.0" edition = "2021" [features] +default = ["custom-heap"] +custom-heap = [] no-entrypoint = [] test-sbf = [] +custom-panic = [] [dependencies] arrayref = "0.3.7" @@ -20,7 +23,8 @@ num-traits = "0.2" serde = "1.0.195" serde_derive = "1.0.103" solana-program = { workspace = true } -spl-token = { version = "4.0.0", path = "../../token/program", features = ["no-entrypoint"] } +spl-token-2022 = { workspace = true } +spl-transfer-hook-interface = { workspace = true } spl-governance-tools = { version = "0.1.3", path ="../tools"} spl-governance-addin-api = { version = "0.1.3", path ="../addin-api"} thiserror = "1.0" @@ -33,6 +37,12 @@ solana-program-test = { workspace = true } solana-sdk = { workspace = true } spl-governance-test-sdk = { version = "0.1.3", path ="../test-sdk"} spl-governance-addin-mock = { version = "0.1.3", path ="../addin-mock/program"} +spl-transfer-hook-example = { path = "../../token/transfer-hook/example", features = [ + "no-entrypoint", +] } [lib] crate-type = ["cdylib", "lib"] + +[lints] +workspace = true \ No newline at end of file diff --git a/governance/program/src/entrypoint.rs b/governance/program/src/entrypoint.rs index ce0d8210a..1e975706e 100644 --- a/governance/program/src/entrypoint.rs +++ b/governance/program/src/entrypoint.rs @@ -1,5 +1,5 @@ //! Program entrypoint definitions -#![cfg(all(target_os = "solana", not(feature = "no-entrypoint")))] +#![cfg(not(feature = "no-entrypoint"))] use { crate::{error::GovernanceError, processor}, @@ -9,10 +9,148 @@ use { }, }; +/* +Optimizing Bump Heap Allocation + +Objective: Increase available heap memory while maintaining flexibility in program invocation. + +1. Initial State: Default 32 KiB Heap + +Memory Layout: +0x300000000 0x300008000 + | | + v v + [--------------------] + ^ ^ + | | + VM Lower VM Upper + Boundary Boundary + +Default Allocator (Allocates Backwards / Top Down) (Default 32 KiB): +0x300000000 0x300008000 + | | + [--------------------] + ^ + | + Allocation starts here (SAFE) + +2. Naive Approach: Increase HEAP_LENGTH to 8 * 32 KiB + Default Allocator + +Memory Layout with Increased HEAP_LENGTH: +0x300000000 0x300008000 0x300040000 + | | | + v v v + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower VM Upper Allocation starts here + Boundary Boundary (ACCESS VIOLATION!) + +Issue: Access violation occurs without requestHeapFrame, requiring it for every transaction. + +3. Optimized Solution: Forward Allocation with Flexible Heap Usage + +Memory Layout (Same as Naive Approach): +0x300000000 0x300008000 0x300040000 + | | | + v v v + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower VM Upper Allocator & VM + Boundary Boundary Heap Limit + +Forward Allocator Behavior: + +a) Without requestHeapFrame: +0x300000000 0x300008000 + | | + [--------------------] + ^ ^ + | | + VM Lower VM Upper + Boundary Boundary + Allocation + starts here (SAFE) + +b) With requestHeapFrame: +0x300000000 0x300008000 0x300040000 + | | | + [--------------------|------------------------------------|] + ^ ^ ^ + | | | + VM Lower | VM Upper + Boundary Boundary + Allocation Allocation continues Maximum allocation + starts here with requestHeapFrame with requestHeapFrame +(SAFE) + +Key Advantages: +1. Compatibility: Functions without requestHeapFrame for allocations ≤32 KiB. +2. Extensibility: Supports larger allocations when requestHeapFrame is invoked. +3. Efficiency: Eliminates mandatory requestHeapFrame calls for all transactions. + +Conclusion: +The forward allocation strategy offers a robust solution, providing both backward +compatibility for smaller heap requirements and the flexibility to utilize extended +heap space when necessary. + +The following allocator is a copy of the bump allocator found in +solana_program::entrypoint and +https://github.com/solana-labs/solana-program-library/blob/master/examples/rust/custom-heap/src/entrypoint.rs + +but with changes to its HEAP_LENGTH and its +starting allocation address. +*/ + +#[cfg(target_os = "solana")] +use { + solana_program::entrypoint::HEAP_START_ADDRESS, + std::{alloc::Layout, mem::size_of, ptr::null_mut, usize}, +}; + +/// Length of the memory region used for program heap. +pub const HEAP_LENGTH: usize = 8 * 32 * 1024; + +#[cfg(target_os = "solana")] +struct BumpAllocator; + +#[cfg(target_os = "solana")] +unsafe impl std::alloc::GlobalAlloc for BumpAllocator { + #[inline] + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + const POS_PTR: *mut usize = HEAP_START_ADDRESS as *mut usize; + const TOP_ADDRESS: usize = HEAP_START_ADDRESS as usize + HEAP_LENGTH; + const BOTTOM_ADDRESS: usize = HEAP_START_ADDRESS as usize + size_of::<*mut u8>(); + let mut pos = *POS_PTR; + if pos == 0 { + // First time, set starting position to bottom address + pos = BOTTOM_ADDRESS; + } + // Align the position upwards + pos = (pos + layout.align() - 1) & !(layout.align() - 1); + let next_pos = pos.saturating_add(layout.size()); + if next_pos > TOP_ADDRESS { + return null_mut(); + } + *POS_PTR = next_pos; + pos as *mut u8 + } + + #[inline] + unsafe fn dealloc(&self, _: *mut u8, _: Layout) { + // I'm a bump allocator, I don't free + } +} + +#[cfg(target_os = "solana")] +#[global_allocator] +static A: BumpAllocator = BumpAllocator; + solana_program::entrypoint!(process_instruction); -fn process_instruction( +fn process_instruction<'a>( program_id: &Pubkey, - accounts: &[AccountInfo], + accounts: &'a [AccountInfo<'a>], instruction_data: &[u8], ) -> ProgramResult { if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) { diff --git a/governance/program/src/error.rs b/governance/program/src/error.rs index 02c04f542..3b9ee059c 100644 --- a/governance/program/src/error.rs +++ b/governance/program/src/error.rs @@ -35,8 +35,8 @@ pub enum GovernanceError { #[error("Governing Token Owner must sign transaction")] GoverningTokenOwnerMustSign, - /// Governing Token Owner or Delegate must sign transaction - #[error("Governing Token Owner or Delegate must sign transaction")] + /// Governing Token Owner or Delegate must sign transaction + #[error("Governing Token Owner or Delegate must sign transaction")] GoverningTokenOwnerOrDelegateMustSign, // 505 /// All votes must be relinquished to withdraw governing tokens @@ -507,19 +507,111 @@ pub enum GovernanceError { /// Invalid Governance for RequiredSignatory #[error("Invalid Governance for RequiredSignatory")] - InvalidGovernanceForRequiredSignatory, + InvalidGovernanceForRequiredSignatory, // 621 /// SignatoryRecord already exists #[error("Signatory Record has already been created")] - SignatoryRecordAlreadyExists, + SignatoryRecordAlreadyExists, // 622 /// Instruction has been removed #[error("Instruction has been removed")] - InstructionDeprecated, + InstructionDeprecated, // 623 /// Proposal is missing signatories required by its governance #[error("Proposal is missing required signatories")] - MissingRequiredSignatories, + MissingRequiredSignatories, // 624 + + /// Math Overflow + #[error("Mathematical Overflow")] + MathematicalOverflow, // 625 + + /// Invalid lookup table account owner + #[error("Invalid lookup table account owner")] + InvalidLookupTableAccountOwner, // 626 + + /// Invalid lookup table accounts key + #[error("Invalid lookup table account key")] + InvalidLookupTableAccountKey, // 627 + + /// Invalid number of accounts in message + #[error("Invalid number of accounts in message")] + InvalidNumberOfAccountsInMessage, // 628 + + /// Invalid account found in message + #[error("Invalid account found in message")] + InvalidAccountFoundInMessage, // 629 + + /// Invalid account signer found in message + #[error("Invalid account signer found in message")] + InvalidAccountSigner, // 630 + + /// Invalid writable account found in message + #[error("Invalid writable account found in message")] + InvalidAccountWritable, // 631 + + /// Invalid account found + #[error("Invalid account found")] + InvalidAccountFound, // 632 + + /// Account in lookuptable is missing + #[error("Account in lookuptable is missing")] + MissingAddressInLookuptable, // 633 + + /// Account is protected, it cannot be passed into a CPI as writable + #[error("Account is protected, it cannot be passed into a CPI as writable")] + ProtectedAccount, // 634 + + /// TransactionMessage is malformed + #[error("TransactionMessage is malformed")] + InvalidTransactionMessage, // 635 + + /// Transaction buffer already exists + #[error("Transaction buffer already exists")] + TransactionBufferAlreadyExists, // 636 + + /// Versioned Transaction already exists + #[error("Versioned Transaction already exists")] + VersionedTransactionAlreadyExists, // 637 + + /// Transaction buffer unauthorized extension + #[error("Transaction buffer unauthorized extension")] + TransactionBufferUnauthorizedExtension, // 638 + + /// Versioned Transaction already removed + #[error("Versioned Transaction already removed")] + VersionedTransactionAlreadyRemoved, // 639 + + /// Final buffer exceeded 10128 bytes + #[error("Final buffer exceeded 10128 bytes")] + FinalBufferSizeExceeded, // 640 + + /// Final message buffer hash doesnt match the expected hash + #[error("Final message buffer hash doesnt match the expected hash")] + FinalBufferHashMismatch, // 641 + + /// Final buffer size mismatch + #[error("Final buffer size mismatch")] + FinalBufferSizeMismatch, // 642 + + /// Invalid number of accounts in the address look up table account + #[error("Invalid number of accounts in the address look up table account")] + InvalidNumberOfAccounts, // 643 + + /// Transaction buffer does not exist + #[error("Transaction buffer does not exist")] + TransactionBufferDoesNotExist, // 644 + + /// Invalid account type + #[error("Invalid account type")] + InvalidAccountType, // 645 + + /// Transaction creator must sign + #[error("Transaction creator must sign")] + TransactionCreatorMustSign, // 646 + + /// Lookup Table Account has been extended after vote has started + #[error("Lookup Table Account has been extended after vote has started")] + LookupTableAccountHasBeenAltered, // 647 } impl PrintProgramError for GovernanceError { diff --git a/governance/program/src/instruction.rs b/governance/program/src/instruction.rs index 240627b8f..5b51dd642 100644 --- a/governance/program/src/instruction.rs +++ b/governance/program/src/instruction.rs @@ -1,18 +1,20 @@ //! Program instructions +// Needed to avoid deprecation warning when building/testing the program +#![allow(deprecated)] + use { crate::{ state::{ enums::MintMaxVoterWeightSource, - governance::{ - get_governance_address, get_mint_governance_address, - get_program_governance_address, get_token_governance_address, GovernanceConfig, - }, + governance::{get_governance_address, GovernanceConfig}, native_treasury::get_native_treasury_address, program_metadata::get_program_metadata_address, proposal::{get_proposal_address, VoteType}, proposal_deposit::get_proposal_deposit_address, proposal_transaction::{get_proposal_transaction_address, InstructionData}, + proposal_transaction_buffer::get_proposal_transaction_buffer_address, + proposal_versioned_transaction::get_proposal_versioned_transaction_address, realm::{ get_governing_token_holding_address, get_realm_address, GoverningTokenConfigAccountArgs, GoverningTokenConfigArgs, RealmConfigArgs, @@ -24,11 +26,10 @@ use { token_owner_record::get_token_owner_record_address, vote_record::{get_vote_record_address, Vote}, }, - tools::bpf_loader_upgradeable::get_program_data_address, + tools::spl_token::inline_spl_token, }, borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, solana_program::{ - bpf_loader_upgradeable, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, system_program, sysvar, @@ -51,7 +52,7 @@ pub enum GovernanceInstruction { /// The account will be created with the Realm PDA as its owner /// 4. `[signer]` Payer /// 5. `[]` System - /// 6. `[]` SPL Token + /// 6. `[]` SPL Token or SPL Token 2022 program /// 7. `[]` Sysvar Rent /// 8. `[]` Council Token Mint - optional /// 9. `[writable]` Council Token Holding account - optional unless council @@ -95,7 +96,7 @@ pub enum GovernanceInstruction { /// governing_token_owner] /// 6. `[signer]` Payer /// 7. `[]` System - /// 8. `[]` SPL Token program + /// 8. `[]` SPL Token or SPL Token 2022 program /// 9. `[]` RealmConfig account. /// * PDA seeds: ['realm-config', realm] DepositGoverningTokens { @@ -120,7 +121,7 @@ pub enum GovernanceInstruction { /// 4. `[writable]` TokenOwnerRecord account. /// * PDA seeds: ['governance',realm, governing_token_mint, /// governing_token_owner] - /// 5. `[]` SPL Token program + /// 5. `[]` SPL Token or SPL Token 2022 program /// 6. `[]` RealmConfig account. /// * PDA seeds: ['realm-config', realm] WithdrawGoverningTokens {}, @@ -164,37 +165,9 @@ pub enum GovernanceInstruction { config: GovernanceConfig, }, - /// Creates Program Governance account which governs an upgradable program - /// - /// 0. `[]` Realm account the created Governance belongs to - /// 1. `[writable]` Program Governance account. - /// * PDA seeds: ['program-governance', realm, governed_program] - /// 2. `[]` Program governed by this Governance account - /// 3. `[writable]` Program Data account of the Program governed by this - /// Governance account - /// 4. `[signer]` Current Upgrade Authority account of the Program - /// governed by this Governance account - /// 5. `[]` Governing TokenOwnerRecord account (Used only if not signed by - /// RealmAuthority) - /// 6. `[signer]` Payer - /// 7. `[]` bpf_upgradeable_loader program - /// 8. `[]` System program - /// 9. `[signer]` Governance authority - /// 10. `[]` RealmConfig account. - /// * PDA seeds: ['realm-config', realm] - /// 11. `[]` Optional Voter Weight Record - CreateProgramGovernance { - /// Governance config - #[allow(dead_code)] - config: GovernanceConfig, - - #[allow(dead_code)] - /// Indicates whether Program's upgrade_authority should be transferred - /// to the Governance PDA If it's set to false then it can be - /// done at a later time However the instruction would validate - /// the current upgrade_authority signed the transaction nonetheless - transfer_upgrade_authority: bool, - }, + /// Formerly CreateProgramGovernance. Exists for backwards-compatibility. + #[deprecated(since = "3.1.1", note = "please use `CreateGovernance` instead")] + CreateProgramGovernanceDeprecated, /// Creates Proposal account for Transactions which will be executed at some /// point in the future @@ -430,67 +403,13 @@ pub enum GovernanceInstruction { /// 3+ Any extra accounts that are part of the transaction, in order ExecuteTransaction, - /// Creates Mint Governance account which governs a mint - /// - /// 0. `[]` Realm account the created Governance belongs to - /// 1. `[writable]` Mint Governance account. - /// * PDA seeds: ['mint-governance', realm, governed_mint] - /// 2. `[writable]` Mint governed by this Governance account - /// 3. `[signer]` Current Mint authority (MintTokens and optionally - /// FreezeAccount) - /// 4. `[]` Governing TokenOwnerRecord account (Used only if not signed by - /// RealmAuthority) - /// 5. `[signer]` Payer - /// 6. `[]` SPL Token program - /// 7. `[]` System program - /// 8. `[signer]` Governance authority - /// 9. `[]` RealmConfig account. - /// * PDA seeds: ['realm-config', realm] - /// 10. `[]` Optional Voter Weight Record - CreateMintGovernance { - #[allow(dead_code)] - /// Governance config - config: GovernanceConfig, - - #[allow(dead_code)] - /// Indicates whether Mint's authorities (MintTokens, FreezeAccount) - /// should be transferred to the Governance PDA. If it's set to - /// false then it can be done at a later time. However the - /// instruction would validate the current mint authority signed the - /// transaction nonetheless - transfer_mint_authorities: bool, - }, - - /// Creates Token Governance account which governs a token account - /// - /// 0. `[]` Realm account the created Governance belongs to - /// 1. `[writable]` Token Governance account. - /// * PDA seeds: ['token-governance', realm, governed_token] - /// 2. `[writable]` Token account governed by this Governance account - /// 3. `[signer]` Current token account authority (AccountOwner and - /// optionally CloseAccount) - /// 4. `[]` Governing TokenOwnerRecord account (Used only if not signed by - /// RealmAuthority) - /// 5. `[signer]` Payer - /// 6. `[]` SPL Token program - /// 7. `[]` System program - /// 8. `[signer]` Governance authority - /// 9. `[]` RealmConfig account. - /// * PDA seeds: ['realm-config', realm] - /// 10. `[]` Optional Voter Weight Record - CreateTokenGovernance { - #[allow(dead_code)] - /// Governance config - config: GovernanceConfig, + /// Formerly CreateMintGovernance. Exists for backwards-compatibility. + #[deprecated(since = "3.1.1", note = "please use `CreateGovernance` instead")] + CreateMintGovernanceDeprecated, - #[allow(dead_code)] - /// Indicates whether the token account authorities (AccountOwner and - /// optionally CloseAccount) should be transferred to the Governance PDA - /// If it's set to false then it can be done at a later time - /// However the instruction would validate the current token owner - /// signed the transaction nonetheless - transfer_account_authorities: bool, - }, + /// Formerly CreateTokenGovernance. Exists for backwards-compatibility. + #[deprecated(since = "3.1.1", note = "please use `CreateGovernance` instead")] + CreateTokenGovernanceDeprecated, /// Sets GovernanceConfig for a Governance /// @@ -611,7 +530,7 @@ pub enum GovernanceInstruction { /// membership /// 5. `[]` RealmConfig account. /// * PDA seeds: ['realm-config', realm] - /// 6. `[]` SPL Token program + /// 6. `[]` SPL Token or SPL Token 2022 program RevokeGoverningTokens { /// The amount to revoke #[allow(dead_code)] @@ -663,6 +582,135 @@ pub enum GovernanceInstruction { /// 2. `[writable]` Beneficiary Account which would receive lamports from /// the disposed RequiredSignatory Account RemoveRequiredSignatory, + + /// Creates a Transaction Buffer with a set of instructions for the Proposal + /// at the given index position New Transaction must be inserted at the + /// end of the range indicated by Proposal transactions_next_index + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[]` TokenOwnerRecord account of the Proposal owner + /// 3. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 4. `[writable]` ProposalTransactionBuffer, account. + /// * PDA seeds: ['transaction_buffer', proposal, creator, buffer_index] + /// 5. `[signer]` Payer + /// 6. `[]` System program + CreateTransactionBuffer { + /// Index of the buffer account to seed the account derivation + buffer_index: u8, + /// Hash of the final assembled transaction message. + final_buffer_hash: [u8; 32], + /// Final size of the buffer. + final_buffer_size: u16, + /// Initial slice of the buffer. + buffer: Vec, + }, + + /// Extend a Transaction Buffer with a set of instructions for the Proposal + /// at the given index position New Transaction must be inserted at the + /// end of the range indicated by Proposal transactions_next_index + /// 0. `[]` Governance account + /// 1. `[]` Proposal account + /// 2. `[writable]` ProposalTransactionBuffer, account. + /// * PDA seeds: ['transaction_buffer', proposal, creator, buffer_index] + /// 3. `[signer]` Creator + ExtendTransactionBuffer { + /// Index of the buffer account to seed the account derivation + buffer_index: u8, + /// Initial slice of the buffer. + buffer: Vec, + }, + + /// Closes a Transaction Buffer + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[]` TokenOwnerRecord account of the Proposal owner + /// 3. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 4. `[writable]` ProposalTransactionBuffer, account. + /// * PDA seeds: ['transaction_buffer', proposal, creator, buffer_index] + /// 5. `[signer]` Benificiary + CloseTransactionBuffer { + /// Index of the buffer account to seed the account derivation + buffer_index: u8, + }, + + /// Creates a Versioned Transaction from Buffer Transaction for the Proposal + /// at the given index position New Transaction must be inserted at the + /// end of the range indicated by Proposal transactions_next_index + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[]` TokenOwnerRecord account of the Proposal owner + /// 3. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 4. `[writable]` ProposalVersionedTransaction, account. + /// * PDA seeds: ['version_transaction', proposal, option_index, transaction_index] + /// 5. `[writable]` ProposalTransactionBuffer, account. + /// * PDA seeds: ['transaction_buffer', proposal, creator, buffer_index] + /// 6. `[signer]` Payer + /// 7. `[]` System program + InsertVersionedTransactionFromBuffer { + /// The index of the option the transaction is for + option_index: u8, + /// Number of ephemeral signing PDAs required by the transaction. + ephemeral_signers: u8, + /// The index of the transaction in the proposal + transaction_index: u16, + }, + + /// Creates a Versioned Transaction for the Proposal at the + /// given index position New Transaction must be inserted at the end of + /// the range indicated by Proposal transactions_next_index + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[]` TokenOwnerRecord account of the Proposal owner + /// 3. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 4. `[writable]` ProposalVersionedTransaction, account. + /// * PDA seeds: ['version_transaction', proposal, option_index, transaction_index] + /// 5. `[signer]` Payer + /// 6. `[]` System program + InsertVersionedTransaction { + /// The index of the option the transaction is for + option_index: u8, + /// Number of ephemeral signing PDAs required by the transaction. + ephemeral_signers: u8, + /// The index of the transaction in the proposal + transaction_index: u16, + /// The transaction message in bytes + transaction_message: Vec, + }, + + /// Executes a Versioned Transaction in the Proposal + /// Anybody can execute transaction once Proposal has been voted Yes and + /// transaction_hold_up time has passed The actual transaction being + /// executed will be signed by Governance PDA the Proposal belongs to + /// For example to execute Program upgrade the ProgramGovernance PDA would + /// be used as the signer + /// + /// 0. `[]` Governance account + /// 1. `[writable]` Proposal account + /// 2. `[writable]` ProposalVersionedTransaction account you wish to + /// execute + /// `remaining_accounts` must include the following accounts in the exact + /// order: + /// 1. AddressLookupTable accounts in the order they appear in + /// `message.address_table_lookups`. + /// 2. Accounts in the order they appear in `message.account_keys`. + /// 3. Accounts in the order they appear in + /// `message.address_table_lookups`. + ExecuteVersionedTransaction, + + /// Removes Versioned Transaction from the Proposal + /// + /// 0. `[writable]` Proposal account + /// 1. `[]` TokenOwnerRecord account of the Proposal owner + /// 2. `[signer]` Governance Authority (Token Owner or Governance + /// Delegate) + /// 3. `[writable]` ProposalVersionedTransaction account + /// 4. `[writable]` Beneficiary Account which would receive lamports from + /// the disposed ProposalVersionedTransaction account + RemoveVersionedTransaction, } /// Creates CreateRealm instruction @@ -681,6 +729,8 @@ pub fn create_realm( name: String, min_community_weight_to_create_governance: u64, community_mint_max_voter_weight_source: MintMaxVoterWeightSource, + is_token_2022_for_community: bool, + is_token_2022_for_council: bool, ) -> Instruction { let realm_address = get_realm_address(program_id, &name); let community_token_holding_address = @@ -693,7 +743,14 @@ pub fn create_realm( AccountMeta::new(community_token_holding_address, false), AccountMeta::new(*payer, true), AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly( + if is_token_2022_for_community { + spl_token_2022::id() + } else { + inline_spl_token::id() + }, + false, + ), AccountMeta::new_readonly(sysvar::rent::id(), false), ]; @@ -703,6 +760,14 @@ pub fn create_realm( accounts.push(AccountMeta::new_readonly(council_token_mint, false)); accounts.push(AccountMeta::new(council_token_holding_address, false)); + accounts.push(AccountMeta::new_readonly( + if is_token_2022_for_council { + spl_token_2022::id() + } else { + inline_spl_token::id() + }, + false, + )); true } else { false @@ -748,6 +813,7 @@ pub fn deposit_governing_tokens( // Args amount: u64, governing_token_mint: &Pubkey, + is_token_2022: bool, ) -> Instruction { let token_owner_record_address = get_token_owner_record_address( program_id, @@ -761,7 +827,7 @@ pub fn deposit_governing_tokens( let realm_config_address = get_realm_config_address(program_id, realm); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(governing_token_holding_address, false), AccountMeta::new(*governing_token_source, false), @@ -770,10 +836,22 @@ pub fn deposit_governing_tokens( AccountMeta::new(token_owner_record_address, false), AccountMeta::new(*payer, true), AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly( + if is_token_2022 { + spl_token_2022::id() + } else { + inline_spl_token::id() + }, + false, + ), AccountMeta::new_readonly(realm_config_address, false), ]; + // needed for transfer_checked instruction + if is_token_2022 { + accounts.push(AccountMeta::new(*governing_token_mint, false)); + }; + let instruction = GovernanceInstruction::DepositGoverningTokens { amount }; Instruction { @@ -792,6 +870,7 @@ pub fn withdraw_governing_tokens( governing_token_owner: &Pubkey, // Args governing_token_mint: &Pubkey, + is_token_2022: bool, ) -> Instruction { let token_owner_record_address = get_token_owner_record_address( program_id, @@ -805,16 +884,28 @@ pub fn withdraw_governing_tokens( let realm_config_address = get_realm_config_address(program_id, realm); - let accounts = vec![ + let mut accounts = vec![ AccountMeta::new_readonly(*realm, false), AccountMeta::new(governing_token_holding_address, false), AccountMeta::new(*governing_token_destination, false), AccountMeta::new_readonly(*governing_token_owner, true), AccountMeta::new(token_owner_record_address, false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly( + if is_token_2022 { + spl_token_2022::id() + } else { + inline_spl_token::id() + }, + false, + ), AccountMeta::new_readonly(realm_config_address, false), ]; + // needed for transfer_checked instruction + if is_token_2022 { + accounts.push(AccountMeta::new(*governing_token_mint, false)); + }; + let instruction = GovernanceInstruction::WithdrawGoverningTokens {}; Instruction { @@ -903,141 +994,6 @@ pub fn create_governance( } } -/// Creates CreateProgramGovernance instruction -#[allow(clippy::too_many_arguments)] -pub fn create_program_governance( - program_id: &Pubkey, - // Accounts - realm: &Pubkey, - governed_program: &Pubkey, - governed_program_upgrade_authority: &Pubkey, - token_owner_record: &Pubkey, - payer: &Pubkey, - create_authority: &Pubkey, - voter_weight_record: Option, - // Args - config: GovernanceConfig, - transfer_upgrade_authority: bool, -) -> Instruction { - let program_governance_address = - get_program_governance_address(program_id, realm, governed_program); - let governed_program_data_address = get_program_data_address(governed_program); - - let mut accounts = vec![ - AccountMeta::new_readonly(*realm, false), - AccountMeta::new(program_governance_address, false), - AccountMeta::new_readonly(*governed_program, false), - AccountMeta::new(governed_program_data_address, false), - AccountMeta::new_readonly(*governed_program_upgrade_authority, true), - AccountMeta::new_readonly(*token_owner_record, false), - AccountMeta::new(*payer, true), - AccountMeta::new_readonly(bpf_loader_upgradeable::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(*create_authority, true), - ]; - - with_realm_config_accounts(program_id, &mut accounts, realm, voter_weight_record, None); - - let instruction = GovernanceInstruction::CreateProgramGovernance { - config, - transfer_upgrade_authority, - }; - - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&instruction).unwrap(), - } -} - -/// Creates CreateMintGovernance -#[allow(clippy::too_many_arguments)] -pub fn create_mint_governance( - program_id: &Pubkey, - // Accounts - realm: &Pubkey, - governed_mint: &Pubkey, - governed_mint_authority: &Pubkey, - token_owner_record: &Pubkey, - payer: &Pubkey, - create_authority: &Pubkey, - voter_weight_record: Option, - // Args - config: GovernanceConfig, - transfer_mint_authorities: bool, -) -> Instruction { - let mint_governance_address = get_mint_governance_address(program_id, realm, governed_mint); - - let mut accounts = vec![ - AccountMeta::new_readonly(*realm, false), - AccountMeta::new(mint_governance_address, false), - AccountMeta::new(*governed_mint, false), - AccountMeta::new_readonly(*governed_mint_authority, true), - AccountMeta::new_readonly(*token_owner_record, false), - AccountMeta::new(*payer, true), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(*create_authority, true), - ]; - - with_realm_config_accounts(program_id, &mut accounts, realm, voter_weight_record, None); - - let instruction = GovernanceInstruction::CreateMintGovernance { - config, - transfer_mint_authorities, - }; - - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&instruction).unwrap(), - } -} - -/// Creates CreateTokenGovernance instruction -#[allow(clippy::too_many_arguments)] -pub fn create_token_governance( - program_id: &Pubkey, - // Accounts - realm: &Pubkey, - governed_token: &Pubkey, - governed_token_owner: &Pubkey, - token_owner_record: &Pubkey, - payer: &Pubkey, - create_authority: &Pubkey, - voter_weight_record: Option, - // Args - config: GovernanceConfig, - transfer_account_authorities: bool, -) -> Instruction { - let token_governance_address = get_token_governance_address(program_id, realm, governed_token); - - let mut accounts = vec![ - AccountMeta::new_readonly(*realm, false), - AccountMeta::new(token_governance_address, false), - AccountMeta::new(*governed_token, false), - AccountMeta::new_readonly(*governed_token_owner, true), - AccountMeta::new_readonly(*token_owner_record, false), - AccountMeta::new(*payer, true), - AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(system_program::id(), false), - AccountMeta::new_readonly(*create_authority, true), - ]; - - with_realm_config_accounts(program_id, &mut accounts, realm, voter_weight_record, None); - - let instruction = GovernanceInstruction::CreateTokenGovernance { - config, - transfer_account_authorities, - }; - - Instruction { - program_id: *program_id, - accounts, - data: borsh::to_vec(&instruction).unwrap(), - } -} - /// Creates CreateProposal instruction #[allow(clippy::too_many_arguments)] pub fn create_proposal( @@ -1714,6 +1670,7 @@ pub fn revoke_governing_tokens( revoke_authority: &Pubkey, // Args amount: u64, + is_token_2022: bool, ) -> Instruction { let token_owner_record_address = get_token_owner_record_address( program_id, @@ -1734,7 +1691,14 @@ pub fn revoke_governing_tokens( AccountMeta::new(*governing_token_mint, false), AccountMeta::new_readonly(*revoke_authority, true), AccountMeta::new_readonly(realm_config_address, false), - AccountMeta::new_readonly(spl_token::id(), false), + AccountMeta::new_readonly( + if is_token_2022 { + spl_token_2022::id() + } else { + inline_spl_token::id() + }, + false, + ), ]; let instruction = GovernanceInstruction::RevokeGoverningTokens { amount }; @@ -1883,3 +1847,378 @@ pub fn complete_proposal( data: borsh::to_vec(&instruction).unwrap(), } } + +/// Creates DepositGoverningTokens with extra account metas instruction for +/// token hook extension +#[allow(clippy::too_many_arguments)] +pub fn deposit_governing_tokens_with_extra_account_metas( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_source: &Pubkey, + governing_token_owner: &Pubkey, + governing_token_source_authority: &Pubkey, + payer: &Pubkey, + // Args + amount: u64, + governing_token_mint: &Pubkey, + extra_account_metas: Vec, +) -> Instruction { + let token_owner_record_address = get_token_owner_record_address( + program_id, + realm, + governing_token_mint, + governing_token_owner, + ); + + let governing_token_holding_address = + get_governing_token_holding_address(program_id, realm, governing_token_mint); + + let realm_config_address = get_realm_config_address(program_id, realm); + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governing_token_holding_address, false), + AccountMeta::new(*governing_token_source, false), + AccountMeta::new_readonly(*governing_token_owner, true), + AccountMeta::new_readonly(*governing_token_source_authority, true), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(realm_config_address, false), + AccountMeta::new(*governing_token_mint, false), + ]; + + accounts.extend(extra_account_metas); + + let instruction = GovernanceInstruction::DepositGoverningTokens { amount }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates WithdrawGoverningTokens with extra account metas instruction for +/// token hook extension +pub fn withdraw_governing_tokens_with_extra_account_metas( + program_id: &Pubkey, + // Accounts + realm: &Pubkey, + governing_token_destination: &Pubkey, + governing_token_owner: &Pubkey, + // Args + governing_token_mint: &Pubkey, + extra_account_metas: Vec, +) -> Instruction { + let token_owner_record_address = get_token_owner_record_address( + program_id, + realm, + governing_token_mint, + governing_token_owner, + ); + + let governing_token_holding_address = + get_governing_token_holding_address(program_id, realm, governing_token_mint); + + let realm_config_address = get_realm_config_address(program_id, realm); + + let mut accounts = vec![ + AccountMeta::new_readonly(*realm, false), + AccountMeta::new(governing_token_holding_address, false), + AccountMeta::new(*governing_token_destination, false), + AccountMeta::new_readonly(*governing_token_owner, true), + AccountMeta::new(token_owner_record_address, false), + AccountMeta::new_readonly(spl_token_2022::id(), false), + AccountMeta::new_readonly(realm_config_address, false), + AccountMeta::new(*governing_token_mint, false), + ]; + + accounts.extend(extra_account_metas); + + let instruction = GovernanceInstruction::WithdrawGoverningTokens {}; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates CreateTransactionBuffer instruction +#[allow(clippy::too_many_arguments)] +pub fn create_transaction_buffer( + program_id: &Pubkey, + // Accounts + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + buffer_index: u8, + final_buffer_hash: [u8; 32], + final_buffer_size: u16, + buffer: Vec, +) -> Instruction { + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal, + payer, + &buffer_index.to_le_bytes(), + ); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(proposal_transaction_buffer_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = GovernanceInstruction::CreateTransactionBuffer { + buffer_index, + final_buffer_hash, + final_buffer_size, + buffer, + }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates ExtendTransactionBuffer instruction +#[allow(clippy::too_many_arguments)] +pub fn extend_transaction_buffer( + program_id: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + payer: &Pubkey, + // Args + buffer_index: u8, + buffer: Vec, +) -> Instruction { + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal, + payer, + &buffer_index.to_le_bytes(), + ); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new_readonly(*proposal, false), + AccountMeta::new(proposal_transaction_buffer_address, false), + AccountMeta::new_readonly(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + ]; + + let instruction = GovernanceInstruction::ExtendTransactionBuffer { + buffer_index, + buffer, + }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates CloseTransactionBuffer instruction +#[allow(clippy::too_many_arguments)] +pub fn close_transaction_buffer( + program_id: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + governance_authority: &Pubkey, + token_owner_record: &Pubkey, + beneficiary: &Pubkey, + // Args + buffer_index: u8, +) -> Instruction { + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal, + beneficiary, + &buffer_index.to_le_bytes(), + ); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new_readonly(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(proposal_transaction_buffer_address, false), + AccountMeta::new(*beneficiary, true), + ]; + + let instruction = GovernanceInstruction::CloseTransactionBuffer { buffer_index }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates InsertVersionedTransactionFromBuffer instruction +#[allow(clippy::too_many_arguments)] +pub fn insert_versioned_transaction_from_buffer( + program_id: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + option_index: u8, + ephemeral_signers: u8, + transaction_index: u16, + buffer_index: u8, +) -> Instruction { + // Get PDA addresses + let proposal_versioned_tx_address = get_proposal_versioned_transaction_address( + program_id, + proposal, + &option_index.to_le_bytes(), + &transaction_index.to_le_bytes(), + ); + + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal, + payer, + &buffer_index.to_le_bytes(), + ); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(proposal_versioned_tx_address, false), + AccountMeta::new(proposal_transaction_buffer_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = GovernanceInstruction::InsertVersionedTransactionFromBuffer { + option_index, + ephemeral_signers, + transaction_index, + }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates InsertVersionedTransaction instruction +#[allow(clippy::too_many_arguments)] +pub fn insert_versioned_transaction( + program_id: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + payer: &Pubkey, + // Args + option_index: u8, + ephemeral_signers: u8, + transaction_index: u16, + transaction_message: Vec, +) -> Instruction { + // Get PDA address for the versioned transaction + let proposal_versioned_tx_address = get_proposal_versioned_transaction_address( + program_id, + proposal, + &option_index.to_le_bytes(), + &transaction_index.to_le_bytes(), + ); + + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(proposal_versioned_tx_address, false), + AccountMeta::new(*payer, true), + AccountMeta::new_readonly(system_program::id(), false), + ]; + + let instruction = GovernanceInstruction::InsertVersionedTransaction { + option_index, + ephemeral_signers, + transaction_index, + transaction_message, + }; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates ExecuteVersionedTransaction instruction +#[allow(clippy::too_many_arguments)] +pub fn execute_versioned_transaction( + program_id: &Pubkey, + governance: &Pubkey, + proposal: &Pubkey, + proposal_versioned_transaction: &Pubkey, + // Note: remaining_accounts has to be added during instruction creation +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*governance, false), + AccountMeta::new(*proposal, false), + AccountMeta::new(*proposal_versioned_transaction, false), + ]; + + let instruction = GovernanceInstruction::ExecuteVersionedTransaction; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} + +/// Creates RemoveVersionedTransaction instruction +#[allow(clippy::too_many_arguments)] +pub fn remove_versioned_transaction( + program_id: &Pubkey, + // Accounts + proposal: &Pubkey, + token_owner_record: &Pubkey, + governance_authority: &Pubkey, + proposal_versioned_transaction: &Pubkey, + beneficiary: &Pubkey, +) -> Instruction { + let accounts = vec![ + AccountMeta::new(*proposal, false), + AccountMeta::new_readonly(*token_owner_record, false), + AccountMeta::new_readonly(*governance_authority, true), + AccountMeta::new(*proposal_versioned_transaction, false), + AccountMeta::new(*beneficiary, false), + ]; + + let instruction = GovernanceInstruction::RemoveVersionedTransaction; + + Instruction { + program_id: *program_id, + accounts, + data: borsh::to_vec(&instruction).unwrap(), + } +} diff --git a/governance/program/src/processor/mod.rs b/governance/program/src/processor/mod.rs index c7a6c2403..223b65fd2 100644 --- a/governance/program/src/processor/mod.rs +++ b/governance/program/src/processor/mod.rs @@ -6,12 +6,9 @@ mod process_cancel_proposal; mod process_cast_vote; mod process_complete_proposal; mod process_create_governance; -mod process_create_mint_governance; mod process_create_native_treasury; -mod process_create_program_governance; mod process_create_proposal; mod process_create_realm; -mod process_create_token_governance; mod process_create_token_owner_record; mod process_deposit_governing_tokens; mod process_execute_transaction; @@ -30,6 +27,7 @@ mod process_set_realm_config; mod process_sign_off_proposal; mod process_update_program_metadata; mod process_withdraw_governing_tokens; +mod proposal_versioned_transactions; use { crate::{error::GovernanceError, instruction::GovernanceInstruction}, @@ -39,12 +37,9 @@ use { process_cast_vote::*, process_complete_proposal::*, process_create_governance::*, - process_create_mint_governance::*, process_create_native_treasury::*, - process_create_program_governance::*, process_create_proposal::*, process_create_realm::*, - process_create_token_governance::*, process_create_token_owner_record::*, process_deposit_governing_tokens::*, process_execute_transaction::*, @@ -63,6 +58,7 @@ use { process_sign_off_proposal::*, process_update_program_metadata::*, process_withdraw_governing_tokens::*, + proposal_versioned_transactions::*, solana_program::{ account_info::AccountInfo, borsh1::try_from_slice_unchecked, entrypoint::ProgramResult, msg, program_error::ProgramError, pubkey::Pubkey, @@ -70,9 +66,9 @@ use { }; /// Processes an instruction -pub fn process_instruction( +pub fn process_instruction<'a>( program_id: &Pubkey, - accounts: &[AccountInfo], + accounts: &'a [AccountInfo<'a>], input: &[u8], ) -> ProgramResult { msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); @@ -81,22 +77,45 @@ pub fn process_instruction( let instruction: GovernanceInstruction = try_from_slice_unchecked(input).map_err(|_| ProgramError::InvalidInstructionData)?; - if let GovernanceInstruction::InsertTransaction { - option_index, - index, - hold_up_time, - instructions: _, - } = instruction - { - // Do not dump instruction data into logs - msg!( - "GOVERNANCE-INSTRUCTION: InsertInstruction {{option_index: {:?}, index: {:?}, hold_up_time: {:?} }}", + // Do not dump instruction data into logs + match instruction { + GovernanceInstruction::InsertTransaction { option_index, index, - hold_up_time - ); - } else { - msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction); + .. + } => { + msg!( + "GOVERNANCE-INSTRUCTION: InsertInstruction {{ option_index: {:?}, index: {:?} }}", + option_index, + index, + ); + } + GovernanceInstruction::CreateTransactionBuffer { buffer_index, .. } => { + msg!( + "GOVERNANCE-INSTRUCTION: CreateTransactionBuffer {{ buffer_index: {:?} }}", + buffer_index, + ); + } + GovernanceInstruction::ExtendTransactionBuffer { buffer_index, .. } => { + msg!( + "GOVERNANCE-INSTRUCTION: ExtendTransactionBuffer {{ buffer_index: {:?} }}", + buffer_index, + ); + } + GovernanceInstruction::InsertVersionedTransaction { + option_index, + transaction_index, + .. + } => { + msg!( + "GOVERNANCE-INSTRUCTION: InsertVersionedTransaction {{ option_index: {:?}, transaction_index: {:?} }}", + option_index, + transaction_index + ); + } + _ => { + msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction); + } } match instruction { @@ -116,33 +135,6 @@ pub fn process_instruction( new_governance_delegate, } => process_set_governance_delegate(program_id, accounts, &new_governance_delegate), - GovernanceInstruction::CreateProgramGovernance { - config, - transfer_upgrade_authority, - } => process_create_program_governance( - program_id, - accounts, - config, - transfer_upgrade_authority, - ), - - GovernanceInstruction::CreateMintGovernance { - config, - transfer_mint_authorities, - } => { - process_create_mint_governance(program_id, accounts, config, transfer_mint_authorities) - } - - GovernanceInstruction::CreateTokenGovernance { - config, - transfer_account_authorities, - } => process_create_token_governance( - program_id, - accounts, - config, - transfer_account_authorities, - ), - GovernanceInstruction::CreateGovernance { config } => { process_create_governance(program_id, accounts, config) } @@ -167,9 +159,6 @@ pub fn process_instruction( GovernanceInstruction::AddSignatory { signatory } => { process_add_signatory(program_id, accounts, signatory) } - GovernanceInstruction::Legacy1 => { - Err(GovernanceError::InstructionDeprecated.into()) // No-op - } GovernanceInstruction::SignOffProposal {} => { process_sign_off_proposal(program_id, accounts) } @@ -243,5 +232,68 @@ pub fn process_instruction( GovernanceInstruction::RemoveRequiredSignatory => { process_remove_required_signatory(program_id, accounts) } + GovernanceInstruction::CreateTransactionBuffer { + buffer_index, + final_buffer_hash, + final_buffer_size, + buffer, + } => process_create_transaction_buffer( + program_id, + accounts, + buffer_index, + final_buffer_hash, + final_buffer_size, + buffer, + ), + + GovernanceInstruction::ExtendTransactionBuffer { + buffer_index, + buffer, + } => process_extend_transaction_buffer(program_id, accounts, buffer_index, buffer), + + GovernanceInstruction::CloseTransactionBuffer { buffer_index } => { + process_close_transaction_buffer(program_id, accounts, buffer_index) + } + + GovernanceInstruction::InsertVersionedTransactionFromBuffer { + option_index, + ephemeral_signers, + transaction_index, + } => process_insert_versioned_transaction_from_buffer( + program_id, + accounts, + option_index, + ephemeral_signers, + transaction_index, + ), + + GovernanceInstruction::InsertVersionedTransaction { + option_index, + ephemeral_signers, + transaction_index, + transaction_message, + } => process_insert_versioned_transaction( + program_id, + accounts, + option_index, + ephemeral_signers, + transaction_index, + transaction_message, + ), + + GovernanceInstruction::ExecuteVersionedTransaction {} => { + process_execute_versioned_transaction(program_id, accounts) + } + + GovernanceInstruction::RemoveVersionedTransaction {} => { + process_remove_versioned_transaction(program_id, accounts) + } + #[allow(deprecated)] + GovernanceInstruction::Legacy1 + | GovernanceInstruction::CreateProgramGovernanceDeprecated + | GovernanceInstruction::CreateMintGovernanceDeprecated + | GovernanceInstruction::CreateTokenGovernanceDeprecated => { + Err(GovernanceError::InstructionDeprecated.into()) // No-op + } } } diff --git a/governance/program/src/processor/process_create_mint_governance.rs b/governance/program/src/processor/process_create_mint_governance.rs deleted file mode 100644 index 0c4d37693..000000000 --- a/governance/program/src/processor/process_create_mint_governance.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Program state processor - -use { - crate::{ - state::{ - enums::GovernanceAccountType, - governance::{ - assert_valid_create_governance_args, get_mint_governance_address_seeds, - GovernanceConfig, GovernanceV2, - }, - realm::get_realm_data, - }, - tools::{ - spl_token::{ - assert_spl_token_mint_authority_is_signer, set_spl_token_account_authority, - }, - structs::Reserved119, - }, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - program_pack::Pack, - pubkey::Pubkey, - rent::Rent, - sysvar::Sysvar, - }, - spl_governance_tools::account::create_and_serialize_account_signed, - spl_token::{instruction::AuthorityType, state::Mint}, -}; - -/// Processes CreateMintGovernance instruction -pub fn process_create_mint_governance( - program_id: &Pubkey, - accounts: &[AccountInfo], - config: GovernanceConfig, - transfer_mint_authorities: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let realm_info = next_account_info(account_info_iter)?; // 0 - let mint_governance_info = next_account_info(account_info_iter)?; // 1 - - let governed_mint_info = next_account_info(account_info_iter)?; // 2 - let governed_mint_authority_info = next_account_info(account_info_iter)?; // 3 - - let token_owner_record_info = next_account_info(account_info_iter)?; // 4 - - let payer_info = next_account_info(account_info_iter)?; // 5 - let spl_token_info = next_account_info(account_info_iter)?; // 6 - - let system_info = next_account_info(account_info_iter)?; // 7 - - let rent = Rent::get()?; - - let create_authority_info = next_account_info(account_info_iter)?; // 8 - - assert_valid_create_governance_args(program_id, &config, realm_info)?; - - let realm_data = get_realm_data(program_id, realm_info)?; - - realm_data.assert_create_authority_can_create_governance( - program_id, - realm_info.key, - token_owner_record_info, - create_authority_info, - account_info_iter, // realm_config_info 9, voter_weight_record_info 10 - )?; - - let mint_governance_data = GovernanceV2 { - account_type: GovernanceAccountType::MintGovernanceV2, - realm: *realm_info.key, - governed_account: *governed_mint_info.key, - config, - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; - - create_and_serialize_account_signed::( - payer_info, - mint_governance_info, - &mint_governance_data, - &get_mint_governance_address_seeds(realm_info.key, governed_mint_info.key), - program_id, - system_info, - &rent, - 0, - )?; - - if transfer_mint_authorities { - set_spl_token_account_authority( - governed_mint_info, - governed_mint_authority_info, - mint_governance_info.key, - AuthorityType::MintTokens, - spl_token_info, - )?; - - // If the mint has freeze_authority then transfer it as well - let mint_data = Mint::unpack(&governed_mint_info.data.borrow())?; - // Note: The code assumes mint_authority==freeze_authority - // If this is not the case then the caller should set freeze_authority - // accordingly before making the transfer - if mint_data.freeze_authority.is_some() { - set_spl_token_account_authority( - governed_mint_info, - governed_mint_authority_info, - mint_governance_info.key, - AuthorityType::FreezeAccount, - spl_token_info, - )?; - } - } else { - assert_spl_token_mint_authority_is_signer( - governed_mint_info, - governed_mint_authority_info, - )?; - } - - Ok(()) -} diff --git a/governance/program/src/processor/process_create_program_governance.rs b/governance/program/src/processor/process_create_program_governance.rs deleted file mode 100644 index b3d077e2a..000000000 --- a/governance/program/src/processor/process_create_program_governance.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Program state processor - -use { - crate::{ - state::{ - enums::GovernanceAccountType, - governance::{ - assert_valid_create_governance_args, get_program_governance_address_seeds, - GovernanceConfig, GovernanceV2, - }, - realm::get_realm_data, - }, - tools::{ - bpf_loader_upgradeable::{ - assert_program_upgrade_authority_is_signer, set_program_upgrade_authority, - }, - structs::Reserved119, - }, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - pubkey::Pubkey, - rent::Rent, - sysvar::Sysvar, - }, - spl_governance_tools::account::create_and_serialize_account_signed, -}; - -/// Processes CreateProgramGovernance instruction -pub fn process_create_program_governance( - program_id: &Pubkey, - accounts: &[AccountInfo], - config: GovernanceConfig, - transfer_upgrade_authority: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let realm_info = next_account_info(account_info_iter)?; // 0 - let program_governance_info = next_account_info(account_info_iter)?; // 1 - - let governed_program_info = next_account_info(account_info_iter)?; // 2 - let governed_program_data_info = next_account_info(account_info_iter)?; // 3 - let governed_program_upgrade_authority_info = next_account_info(account_info_iter)?; // 4 - - let token_owner_record_info = next_account_info(account_info_iter)?; // 5 - - let payer_info = next_account_info(account_info_iter)?; // 6 - let bpf_upgrade_loader_info = next_account_info(account_info_iter)?; // 7 - - let system_info = next_account_info(account_info_iter)?; // 8 - - let rent = Rent::get()?; - - let create_authority_info = next_account_info(account_info_iter)?; // 9 - - assert_valid_create_governance_args(program_id, &config, realm_info)?; - - let realm_data = get_realm_data(program_id, realm_info)?; - - realm_data.assert_create_authority_can_create_governance( - program_id, - realm_info.key, - token_owner_record_info, - create_authority_info, - account_info_iter, // realm_config_info 10, voter_weight_record_info 11 - )?; - - let program_governance_data = GovernanceV2 { - account_type: GovernanceAccountType::ProgramGovernanceV2, - realm: *realm_info.key, - governed_account: *governed_program_info.key, - config, - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; - - create_and_serialize_account_signed::( - payer_info, - program_governance_info, - &program_governance_data, - &get_program_governance_address_seeds(realm_info.key, governed_program_info.key), - program_id, - system_info, - &rent, - 0, - )?; - - if transfer_upgrade_authority { - set_program_upgrade_authority( - governed_program_info.key, - governed_program_data_info, - governed_program_upgrade_authority_info, - program_governance_info, - bpf_upgrade_loader_info, - )?; - } else { - assert_program_upgrade_authority_is_signer( - governed_program_info.key, - governed_program_data_info, - governed_program_upgrade_authority_info, - )?; - } - - Ok(()) -} diff --git a/governance/program/src/processor/process_create_realm.rs b/governance/program/src/processor/process_create_realm.rs index f299b6746..2c51195a0 100644 --- a/governance/program/src/processor/process_create_realm.rs +++ b/governance/program/src/processor/process_create_realm.rs @@ -13,7 +13,7 @@ use { get_realm_config_address_seeds, resolve_governing_token_config, RealmConfigAccount, }, }, - tools::{spl_token::create_spl_token_account_signed, structs::Reserved110}, + tools::{spl_token::{create_spl_token_account_signed, inline_spl_token}, structs::Reserved110}, }, solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -26,13 +26,13 @@ use { }; /// Processes CreateRealm instruction -pub fn process_create_realm( +pub fn process_create_realm<'a>( program_id: &Pubkey, - accounts: &[AccountInfo], + accounts: &'a [AccountInfo<'a>], name: String, realm_config_args: RealmConfigArgs, ) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); + let account_info_iter = &mut accounts.iter().peekable(); let realm_info = next_account_info(account_info_iter)?; // 0 let realm_authority_info = next_account_info(account_info_iter)?; // 1 @@ -69,6 +69,18 @@ pub fn process_create_realm( let council_token_mint_address = if realm_config_args.use_council_mint { let council_token_mint_info = next_account_info(account_info_iter)?; // 8 let council_token_holding_info = next_account_info(account_info_iter)?; // 9 + // maintain backwards compatibility using Peekable iterator + let council_spl_token_info = if let Some(next_info) = account_info_iter.peek() { + // Check if next_info.key is either spl_token_2022::id() or inline_spl_token::ID + let is_spl_token = next_info.key == &spl_token_2022::id() || next_info.key == &inline_spl_token::ID; + if is_spl_token { + next_account_info(account_info_iter)? + } else { + spl_token_info + } + } else { + spl_token_info + }; // 10 if using spl-token-2022 create_spl_token_account_signed( payer_info, @@ -78,7 +90,7 @@ pub fn process_create_realm( realm_info, program_id, system_info, - spl_token_info, + council_spl_token_info, rent_sysvar_info, rent, )?; @@ -89,15 +101,14 @@ pub fn process_create_realm( }; // Create and serialize RealmConfig - let realm_config_info = next_account_info(account_info_iter)?; // 10 - - // 11, 12 + let realm_config_info = next_account_info(account_info_iter)?; // 11 + // 12, 13 let community_token_config = resolve_governing_token_config( account_info_iter, &realm_config_args.community_token_config_args, )?; - // 13, 14 + // 14, 15 let council_token_config = resolve_governing_token_config( account_info_iter, &realm_config_args.council_token_config_args, diff --git a/governance/program/src/processor/process_create_token_governance.rs b/governance/program/src/processor/process_create_token_governance.rs deleted file mode 100644 index 039a4e3d6..000000000 --- a/governance/program/src/processor/process_create_token_governance.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Program state processor - -use { - crate::{ - state::{ - enums::GovernanceAccountType, - governance::{ - assert_valid_create_governance_args, get_token_governance_address_seeds, - GovernanceConfig, GovernanceV2, - }, - realm::get_realm_data, - }, - tools::{ - spl_token::{assert_spl_token_owner_is_signer, set_spl_token_account_authority}, - structs::Reserved119, - }, - }, - solana_program::{ - account_info::{next_account_info, AccountInfo}, - entrypoint::ProgramResult, - program_pack::Pack, - pubkey::Pubkey, - rent::Rent, - sysvar::Sysvar, - }, - spl_governance_tools::account::create_and_serialize_account_signed, - spl_token::{instruction::AuthorityType, state::Account}, -}; - -/// Processes CreateTokenGovernance instruction -pub fn process_create_token_governance( - program_id: &Pubkey, - accounts: &[AccountInfo], - config: GovernanceConfig, - transfer_account_authorities: bool, -) -> ProgramResult { - let account_info_iter = &mut accounts.iter(); - - let realm_info = next_account_info(account_info_iter)?; // 0 - let token_governance_info = next_account_info(account_info_iter)?; // 1 - - let governed_token_info = next_account_info(account_info_iter)?; // 2 - let governed_token_owner_info = next_account_info(account_info_iter)?; // 3 - - let token_owner_record_info = next_account_info(account_info_iter)?; // 4 - - let payer_info = next_account_info(account_info_iter)?; // 5 - let spl_token_info = next_account_info(account_info_iter)?; // 6 - - let system_info = next_account_info(account_info_iter)?; // 7 - - let rent = Rent::get()?; - - let create_authority_info = next_account_info(account_info_iter)?; // 8 - - assert_valid_create_governance_args(program_id, &config, realm_info)?; - - let realm_data = get_realm_data(program_id, realm_info)?; - - realm_data.assert_create_authority_can_create_governance( - program_id, - realm_info.key, - token_owner_record_info, - create_authority_info, - account_info_iter, // realm_config_info 9, voter_weight_record_info 10 - )?; - - let token_governance_data = GovernanceV2 { - account_type: GovernanceAccountType::TokenGovernanceV2, - realm: *realm_info.key, - governed_account: *governed_token_info.key, - config, - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; - - create_and_serialize_account_signed::( - payer_info, - token_governance_info, - &token_governance_data, - &get_token_governance_address_seeds(realm_info.key, governed_token_info.key), - program_id, - system_info, - &rent, - 0, - )?; - - if transfer_account_authorities { - set_spl_token_account_authority( - governed_token_info, - governed_token_owner_info, - token_governance_info.key, - AuthorityType::AccountOwner, - spl_token_info, - )?; - - // If the token account has close_authority then transfer it as well - let token_account_data = Account::unpack(&governed_token_info.data.borrow())?; - // Note: The code assumes owner==close_authority - // If this is not the case then the caller should set close_authority - // accordingly before making the transfer - if token_account_data.close_authority.is_some() { - set_spl_token_account_authority( - governed_token_info, - governed_token_owner_info, - token_governance_info.key, - AuthorityType::CloseAccount, - spl_token_info, - )?; - } - } else { - assert_spl_token_owner_is_signer(governed_token_info, governed_token_owner_info)?; - } - - Ok(()) -} diff --git a/governance/program/src/processor/process_deposit_governing_tokens.rs b/governance/program/src/processor/process_deposit_governing_tokens.rs index c138e5469..bb377c218 100644 --- a/governance/program/src/processor/process_deposit_governing_tokens.rs +++ b/governance/program/src/processor/process_deposit_governing_tokens.rs @@ -13,8 +13,8 @@ use { }, }, tools::spl_token::{ - get_spl_token_mint, is_spl_token_account, is_spl_token_mint, mint_spl_tokens_to, - transfer_spl_tokens, + get_current_mint_fee, get_spl_token_mint, is_spl_token_account, is_spl_token_mint, + mint_spl_tokens_to, transfer_checked_spl_tokens, transfer_spl_tokens, }, }, solana_program::{ @@ -46,6 +46,12 @@ pub fn process_deposit_governing_tokens( let spl_token_info = next_account_info(account_info_iter)?; // 8 let realm_config_info = next_account_info(account_info_iter)?; // 9 + let expected_mint_info = if let Ok(expected_mint_info) = next_account_info(account_info_iter) { + Some(expected_mint_info) + } else { + None + }; // 10 if using spl-token-2022 + let rent = Rent::get()?; let realm_data = get_realm_data(program_id, realm_info)?; @@ -65,13 +71,32 @@ pub fn process_deposit_governing_tokens( if is_spl_token_account(governing_token_source_info) { // If the source is spl-token token account then transfer tokens from it - transfer_spl_tokens( - governing_token_source_info, - governing_token_holding_info, - governing_token_source_authority_info, - amount, - spl_token_info, - )?; + match expected_mint_info { + Some(mint_info) => { + let additional_accounts = account_info_iter.as_slice(); + transfer_checked_spl_tokens( + governing_token_source_info, + governing_token_holding_info, + governing_token_source_authority_info, + amount, + spl_token_info, + mint_info, + additional_accounts, + )?; + } + _ => { + // Maintain backwards compatibility + // Token-2022 requires transfer_checked method with mint + // Downstream the instruction will fail if the data supplied is invalid + transfer_spl_tokens( + governing_token_source_info, + governing_token_holding_info, + governing_token_source_authority_info, + amount, + spl_token_info, + )?; + } + } } else if is_spl_token_mint(governing_token_source_info) { // If it's a mint then mint the tokens mint_spl_tokens_to( @@ -91,6 +116,22 @@ pub fn process_deposit_governing_tokens( governing_token_owner_info.key, ); + // Adjust deposit amount to account for transfer fees + // Fee calculation requires to be on-chain due to epoch clock requirement + // Ensures accurate token amount after fee deduction to be stored in the TokenOwnerRecord + let fee = if is_spl_token_account(governing_token_source_info) { + match expected_mint_info { + Some(mint_info) => get_current_mint_fee(mint_info, amount)?, + None => 0, + } + } else { + 0 + }; + + let deposit_amount = amount + .checked_sub(fee) + .ok_or(GovernanceError::MathematicalOverflow)?; + if token_owner_record_info.data_is_empty() { // Deposited tokens can only be withdrawn by the owner so let's make sure the // owner signed the transaction @@ -102,7 +143,7 @@ pub fn process_deposit_governing_tokens( account_type: GovernanceAccountType::TokenOwnerRecordV2, realm: *realm_info.key, governing_token_owner: *governing_token_owner_info.key, - governing_token_deposit_amount: amount, + governing_token_deposit_amount: deposit_amount, governing_token_mint, governance_delegate: None, unrelinquished_votes_count: 0, @@ -131,7 +172,7 @@ pub fn process_deposit_governing_tokens( token_owner_record_data.governing_token_deposit_amount = token_owner_record_data .governing_token_deposit_amount - .checked_add(amount) + .checked_add(deposit_amount) .unwrap(); token_owner_record_data.serialize(&mut token_owner_record_info.data.borrow_mut()[..])?; diff --git a/governance/program/src/processor/process_set_realm_config.rs b/governance/program/src/processor/process_set_realm_config.rs index 347f26556..87f6c2617 100644 --- a/governance/program/src/processor/process_set_realm_config.rs +++ b/governance/program/src/processor/process_set_realm_config.rs @@ -24,9 +24,9 @@ use { }; /// Processes SetRealmConfig instruction -pub fn process_set_realm_config( +pub fn process_set_realm_config<'a>( program_id: &Pubkey, - accounts: &[AccountInfo], + accounts: &'a [AccountInfo<'a>], realm_config_args: RealmConfigArgs, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); diff --git a/governance/program/src/processor/process_withdraw_governing_tokens.rs b/governance/program/src/processor/process_withdraw_governing_tokens.rs index ac2c1fe9c..456940bc3 100644 --- a/governance/program/src/processor/process_withdraw_governing_tokens.rs +++ b/governance/program/src/processor/process_withdraw_governing_tokens.rs @@ -10,7 +10,9 @@ use { get_token_owner_record_address_seeds, get_token_owner_record_data_for_seeds, }, }, - tools::spl_token::{get_spl_token_mint, transfer_spl_tokens_signed}, + tools::spl_token::{ + get_spl_token_mint, transfer_spl_tokens_signed, transfer_spl_tokens_signed_checked, + }, }, solana_program::{ account_info::{next_account_info, AccountInfo}, @@ -33,6 +35,11 @@ pub fn process_withdraw_governing_tokens( let token_owner_record_info = next_account_info(account_info_iter)?; // 4 let spl_token_info = next_account_info(account_info_iter)?; // 5 let realm_config_info = next_account_info(account_info_iter)?; // 6 + let expected_mint_info = if let Ok(expected_mint_info) = next_account_info(account_info_iter) { + Some(expected_mint_info) + } else { + None + }; // 7 if using token_2022 if !governing_token_owner_info.is_signer { return Err(GovernanceError::GoverningTokenOwnerMustSign.into()); @@ -67,15 +74,36 @@ pub fn process_withdraw_governing_tokens( token_owner_record_data.assert_can_withdraw_governing_tokens()?; - transfer_spl_tokens_signed( - governing_token_holding_info, - governing_token_destination_info, - realm_info, - &get_realm_address_seeds(&realm_data.name), - program_id, - token_owner_record_data.governing_token_deposit_amount, - spl_token_info, - )?; + match expected_mint_info { + Some(mint_info) => { + let additional_accounts = account_info_iter.as_slice(); + transfer_spl_tokens_signed_checked( + governing_token_holding_info, + governing_token_destination_info, + realm_info, + &get_realm_address_seeds(&realm_data.name), + program_id, + token_owner_record_data.governing_token_deposit_amount, + spl_token_info, + mint_info, + additional_accounts, + )?; + } + _ => { + // Maintain backwards compatibility + // Token-2022 requires transfer_checked method with mint + // Downstream the instruction will fail if the data supplied is invalid + transfer_spl_tokens_signed( + governing_token_holding_info, + governing_token_destination_info, + realm_info, + &get_realm_address_seeds(&realm_data.name), + program_id, + token_owner_record_data.governing_token_deposit_amount, + spl_token_info, + )?; + } + } token_owner_record_data.governing_token_deposit_amount = 0; token_owner_record_data.serialize(&mut token_owner_record_info.data.borrow_mut()[..])?; diff --git a/governance/program/src/processor/proposal_versioned_transactions/mod.rs b/governance/program/src/processor/proposal_versioned_transactions/mod.rs new file mode 100644 index 000000000..74e6f3818 --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/mod.rs @@ -0,0 +1,15 @@ +mod process_close_transaction_buffer; +mod process_create_transaction_buffer; +mod process_execute_proposal_versioned_transaction; +mod process_extend_transaction_buffer; +mod process_insert_proposal_versioned_transaction; +mod process_insert_proposal_versioned_transaction_from_buffer; +mod process_remove_versioned_transaction; + +pub use { + process_close_transaction_buffer::*, process_create_transaction_buffer::*, + process_execute_proposal_versioned_transaction::*, process_extend_transaction_buffer::*, + process_insert_proposal_versioned_transaction::*, + process_insert_proposal_versioned_transaction_from_buffer::*, + process_remove_versioned_transaction::*, +}; diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_close_transaction_buffer.rs b/governance/program/src/processor/proposal_versioned_transactions/process_close_transaction_buffer.rs new file mode 100644 index 000000000..b6a5eaea9 --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_close_transaction_buffer.rs @@ -0,0 +1,94 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + governance::get_governance_data, + proposal::get_proposal_data_for_governance, + proposal_transaction_buffer::{ + get_proposal_transaction_buffer_address, + get_proposal_transaction_buffer_data_for_proposal, + }, + token_owner_record::get_token_owner_record_data_for_proposal_owner, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + msg, + pubkey::Pubkey, + sysvar::Sysvar, + }, + spl_governance_tools::account::dispose_account, +}; + +/// Processes CloseTransactionBuffer instruction +pub fn process_close_transaction_buffer( + program_id: &Pubkey, + accounts: &[AccountInfo], + // Index of the buffer account to seed the account derivation + buffer_index: u8, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governance_authority_info = next_account_info(account_info_iter)?; // 3 + + let proposal_transaction_buffer_info = next_account_info(account_info_iter)?; // 4 + + let beneficiary_info = next_account_info(account_info_iter)?; // 5 + + if !beneficiary_info.is_signer { + return Err(GovernanceError::TransactionCreatorMustSign.into()); + } + + if proposal_transaction_buffer_info.data_is_empty() { + return Err(GovernanceError::TransactionBufferAlreadyExists.into()); + } + + let clock = Clock::get()?; + + // Governance account is no longer used and it's deserialized only to validate + // the provided account + let governance_data = get_governance_data(program_id, governance_info)?; + + let proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + + // Check if the proposal is in the draft stage. + // even if the transaction buffer has not been inserted into the final transaction proposal + proposal_data.assert_can_cancel(&governance_data.config, clock.unix_timestamp)?; + + let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + let _proposal_transaction_buffer_data = get_proposal_transaction_buffer_data_for_proposal( + program_id, + proposal_transaction_buffer_info, + proposal_info.key, + )?; + + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal_info.key, + &beneficiary_info.key, + &buffer_index.to_le_bytes(), + ); + + if proposal_transaction_buffer_address != *proposal_transaction_buffer_info.key { + msg!("Proposal transaction buffer address does not match"); + return Err(GovernanceError::InvalidAccountFound.into()); + } + + dispose_account(proposal_transaction_buffer_info, beneficiary_info)?; + + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_create_transaction_buffer.rs b/governance/program/src/processor/proposal_versioned_transactions/process_create_transaction_buffer.rs new file mode 100644 index 000000000..dc706391f --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_create_transaction_buffer.rs @@ -0,0 +1,101 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + enums::GovernanceAccountType, + governance::get_governance_data, + proposal::get_proposal_data_for_governance, + proposal_transaction_buffer::{ + get_proposal_transaction_buffer_address_seeds, ProposalTransactionBuffer, + }, + token_owner_record::get_token_owner_record_data_for_proposal_owner, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, + }, + spl_governance_tools::account::create_and_serialize_account_signed, +}; + +/// Processes CreateTransactionBuffer instruction +pub fn process_create_transaction_buffer( + program_id: &Pubkey, + accounts: &[AccountInfo], + // Index of the buffer account to seed the account derivation + buffer_index: u8, + // Hash (sha256) of the final assembled transaction message. + final_buffer_hash: [u8; 32], + // Final size of the buffer. + final_buffer_size: u16, + // Initial slice of the buffer. + buffer: Vec, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governance_authority_info = next_account_info(account_info_iter)?; // 3 + + let proposal_transaction_buffer_info = next_account_info(account_info_iter)?; // 4 + + let payer_info = next_account_info(account_info_iter)?; // 5 + let system_info = next_account_info(account_info_iter)?; // 6 + let rent = &Rent::get()?; + + if !payer_info.is_signer { + return Err(GovernanceError::TransactionCreatorMustSign.into()); + } + if !proposal_transaction_buffer_info.data_is_empty() { + return Err(GovernanceError::TransactionBufferAlreadyExists.into()); + } + + let _governance_data = get_governance_data(program_id, governance_info)?; + + let proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + proposal_data.assert_can_edit_instructions()?; + + let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let proposal_transaction_buffer_data = ProposalTransactionBuffer { + account_type: GovernanceAccountType::ProposalTransactionBuffer, + proposal: *proposal_info.key, + creator: *payer_info.key, + buffer_index, + final_buffer_hash, + final_buffer_size, + buffer, + }; + // Validate transaction buffer data sizes + proposal_transaction_buffer_data.invariant()?; + + create_and_serialize_account_signed::( + payer_info, + proposal_transaction_buffer_info, + &proposal_transaction_buffer_data, + &get_proposal_transaction_buffer_address_seeds( + proposal_info.key, + payer_info.key, + &buffer_index.to_le_bytes(), + ), + program_id, + system_info, + rent, + 0, + )?; + + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_execute_proposal_versioned_transaction.rs b/governance/program/src/processor/proposal_versioned_transactions/process_execute_proposal_versioned_transaction.rs new file mode 100644 index 000000000..f36629eb8 --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_execute_proposal_versioned_transaction.rs @@ -0,0 +1,156 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + enums::{ProposalState, TransactionExecutionStatus}, + governance::get_governance_data, + native_treasury::get_native_treasury_address_seeds, + proposal::{get_proposal_data_for_governance, OptionVoteResult}, + proposal_versioned_transaction::get_proposal_versioned_transaction_data_for_proposal, + }, + tools::{ + ephermal_signers::derive_ephemeral_signers, + executable_transaction_message::ExecutableTransactionMessage, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + clock::Clock, + entrypoint::ProgramResult, + pubkey::Pubkey, + sysvar::Sysvar, + }, +}; + +/// Processes ExecuteVersionedTransaction instruction +pub fn process_execute_versioned_transaction( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + let proposal_versioned_transaction_info = next_account_info(account_info_iter)?; // 2 + + let clock = Clock::get()?; + + let governance_data = get_governance_data(program_id, governance_info)?; + + let mut proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + + let mut proposal_versioned_transaction_data = + get_proposal_versioned_transaction_data_for_proposal( + program_id, + proposal_versioned_transaction_info, + proposal_info.key, + )?; + + proposal_data.assert_can_execute_versioned_transaction( + &proposal_versioned_transaction_data, + &governance_data.config, + clock.unix_timestamp, + )?; + + let transaction = proposal_versioned_transaction_data.take(); + + // `remaining_accounts` must include the following accounts in the exact order: + // 1. AddressLookupTable accounts in the order they appear in + // `message.address_table_lookups`. + // 2. Accounts in the order they appear in `message.account_keys`. + // 3. Accounts in the order they appear in `message.address_table_lookups`. + let transaction_account_infos = account_info_iter.as_slice(); + + let transaction_message = transaction.message; + let num_lookups = transaction_message.address_table_lookups.len(); + + let message_account_infos = transaction_account_infos + .get(num_lookups..) + .ok_or(GovernanceError::InvalidNumberOfAccounts)?; + let address_lookup_table_account_infos = transaction_account_infos + .get(..num_lookups) + .ok_or(GovernanceError::InvalidNumberOfAccounts)?; + + // Sign the transaction using the governance PDA + let mut governance_seeds = governance_data.get_governance_address_seeds()?.to_vec(); + let (governance_pubkey, bump_seed) = + Pubkey::find_program_address(&governance_seeds, program_id); + let bump = &[bump_seed]; + // It will not be included if it is not used in execute_message() + governance_seeds.push(bump); + + // Sign the transaction using the governance treasury PDA + let mut treasury_seeds = get_native_treasury_address_seeds(governance_info.key).to_vec(); + let (treasury_address, treasury_bump_seed) = + Pubkey::find_program_address(&treasury_seeds, program_id); + let treasury_bump = &[treasury_bump_seed]; + // It will not be included if it is not used in execute_message() + treasury_seeds.push(treasury_bump); + + let (ephemeral_signer_keys, ephemeral_signer_seeds) = derive_ephemeral_signers( + program_id, + &proposal_versioned_transaction_info.key, + &transaction.ephemeral_signer_bumps, + proposal_versioned_transaction_data.transaction_index, + ); + + let executable_message = ExecutableTransactionMessage::new_validated( + transaction_message, + message_account_infos, + address_lookup_table_account_infos, + &treasury_address, + &governance_pubkey, + &ephemeral_signer_keys, + proposal_data.voting_at_slot.unwrap(), + )?; + + // Protected accounts that cannot be used in execute_message() + let protected_accounts = &[*proposal_info.key]; + + // Execute the transaction message instructions one-by-one + executable_message.execute_message( + &governance_seeds[..], + &treasury_seeds[..], + &governance_pubkey, + &treasury_address, + &ephemeral_signer_seeds, + protected_accounts, + )?; + + // Update proposal and instruction accounts + if proposal_data.state == ProposalState::Succeeded { + proposal_data.executing_at = Some(clock.unix_timestamp); + proposal_data.state = ProposalState::Executing; + } + let option = + &mut proposal_data.options[proposal_versioned_transaction_data.option_index as usize]; + option.transactions_executed_count = option.transactions_executed_count.checked_add(1).unwrap(); + + // Checking for Executing and ExecutingWithErrors states because instruction can + // still be executed after being flagged with error The check for + // instructions_executed_count ensures Proposal can't be transitioned to + // Completed state from ExecutingWithErrors + if (proposal_data.state == ProposalState::Executing + || proposal_data.state == ProposalState::ExecutingWithErrors) + && proposal_data + .options + .iter() + .filter(|o| o.vote_result == OptionVoteResult::Succeeded) + .all(|o| o.transactions_executed_count == o.transactions_count) + { + proposal_data.closed_at = Some(clock.unix_timestamp); + proposal_data.state = ProposalState::Completed; + } + + proposal_data.serialize(&mut proposal_info.data.borrow_mut()[..])?; + + proposal_versioned_transaction_data.executed_at = Some(clock.unix_timestamp); + proposal_versioned_transaction_data.execution_status = TransactionExecutionStatus::Success; + proposal_versioned_transaction_data + .serialize(&mut proposal_versioned_transaction_info.data.borrow_mut()[..])?; + + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_extend_transaction_buffer.rs b/governance/program/src/processor/proposal_versioned_transactions/process_extend_transaction_buffer.rs new file mode 100644 index 000000000..c016cffb6 --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_extend_transaction_buffer.rs @@ -0,0 +1,116 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + governance::get_governance_data, + proposal::get_proposal_data_for_governance, + proposal_transaction_buffer::{ + get_proposal_transaction_buffer_address, + get_proposal_transaction_buffer_data_for_proposal, + }, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + pubkey::Pubkey, + }, +}; + +/// Processes ExtendTransactionBuffer instruction +pub fn process_extend_transaction_buffer( + program_id: &Pubkey, + accounts: &[AccountInfo], + // Index of the buffer account to seed the account derivation + buffer_index: u8, + buffer: Vec, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + + let proposal_transaction_buffer_info = next_account_info(account_info_iter)?; // 2 + + let creator_info = next_account_info(account_info_iter)?; // 3 + + if !creator_info.is_signer { + return Err(GovernanceError::TransactionCreatorMustSign.into()); + } + + // proposal transaction buffer has to be created first + if proposal_transaction_buffer_info.data_is_empty() { + return Err(GovernanceError::TransactionBufferDoesNotExist.into()); + } + + // Governance account is no longer used and it's deserialized only to validate + // the provided account + let _governance_data = get_governance_data(program_id, governance_info)?; + + let proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + proposal_data.assert_can_edit_instructions()?; + + let mut proposal_transaction_buffer_data = get_proposal_transaction_buffer_data_for_proposal( + program_id, + proposal_transaction_buffer_info, + proposal_info.key, + )?; + + // Skipping token owner record validation for the transaction buffer creator + // since it's already verified in create_transaction_buffer(). + // We only verify the payer matches the creator below to minimize overall transaction size + + // Check transaction buffer validations + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal_info.key, + creator_info.key, + &buffer_index.to_le_bytes(), + ); + + if proposal_transaction_buffer_address != *proposal_transaction_buffer_info.key { + msg!("Proposal transaction buffer address does not match"); + return Err(GovernanceError::InvalidAccountFound.into()); + } + + let current_buffer_size = proposal_transaction_buffer_data.buffer.len() as u16; + let remaining_space = proposal_transaction_buffer_data + .final_buffer_size + .checked_sub(current_buffer_size) + .unwrap(); + + // Check if the new data exceeds the remaining space + let new_data_size = buffer.len() as u16; + + // Log the buffer sizes + msg!("Buffer size: {} -> {}", new_data_size, current_buffer_size); + + // Check if we have enough remaining space, otherwise the initial final_buffer_size calculation was incorrect + if new_data_size > remaining_space { + return Err(GovernanceError::FinalBufferSizeExceeded.into()); + } + + // Check if creator is valid + if proposal_transaction_buffer_data.creator != *creator_info.key { + return Err(GovernanceError::TransactionBufferUnauthorizedExtension.into()); + } + + let buffer_slice_extension = buffer; + + // Extend the buffer, log if it panics + proposal_transaction_buffer_data + .buffer + .extend_from_slice(&buffer_slice_extension); + + proposal_transaction_buffer_data.invariant()?; + + // Serialize the modified transaction buffer back to account data + proposal_transaction_buffer_data + .serialize(&mut proposal_transaction_buffer_info.data.borrow_mut()[..])?; + + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction.rs b/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction.rs new file mode 100644 index 000000000..4efef464e --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction.rs @@ -0,0 +1,163 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + enums::{GovernanceAccountType, TransactionExecutionStatus}, + governance::get_governance_data, + proposal::get_proposal_data_for_governance, + proposal_versioned_transaction::{ + get_proposal_versioned_transaction_address_seeds, ProposalVersionedTransaction, + VERSIONED_TRANSACTION_BUFFER_SEED, + }, + token_owner_record::get_token_owner_record_data_for_proposal_owner, + }, + tools::{ephermal_signers::EPHERMAL_SIGNER_SEED, transaction_message::TransactionMessage}, + }, + borsh::BorshDeserialize, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, + }, + spl_governance_tools::account::create_and_serialize_account_signed, + std::cmp::Ordering, +}; + +/// Processes InsertVersionedTransaction instruction +pub fn process_insert_versioned_transaction( + program_id: &Pubkey, + accounts: &[AccountInfo], + option_index: u8, + // Number of ephemeral signing PDAs required by the transaction. + ephemeral_signers: u8, + transaction_index: u16, + transaction_message: Vec, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governance_authority_info = next_account_info(account_info_iter)?; // 3 + + let proposal_versioned_transaction_info = next_account_info(account_info_iter)?; // 4 + + let payer_info = next_account_info(account_info_iter)?; // 5 + let system_info = next_account_info(account_info_iter)?; // 6 + let rent = &Rent::get()?; + + if !payer_info.is_signer { + return Err(GovernanceError::TransactionCreatorMustSign.into()); + } + if !proposal_versioned_transaction_info.data_is_empty() { + return Err(GovernanceError::VersionedTransactionAlreadyExists.into()); + } + // Governance account is no longer used and it's deserialized only to validate + // the provided account + let _governance_data = get_governance_data(program_id, governance_info)?; + + let mut proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + proposal_data.assert_can_edit_instructions()?; + + let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let option = &mut proposal_data.options[option_index as usize]; + + match transaction_index.cmp(&option.transactions_next_index) { + Ordering::Greater => return Err(GovernanceError::InvalidTransactionIndex.into()), + // If the index is the same as transactions_next_index then we are adding a new transaction + // If the index is below transactions_next_index then we are inserting into an existing + // empty space + Ordering::Equal => { + option.transactions_next_index = option.transactions_next_index.checked_add(1).unwrap(); + } + Ordering::Less => {} + } + + option.transactions_count = option.transactions_count.checked_add(1).unwrap(); + proposal_data.serialize(&mut proposal_info.data.borrow_mut()[..])?; + + process_create_versioned_transaction_account( + program_id, + option_index, + ephemeral_signers, + transaction_index, + transaction_message, + proposal_info, + proposal_versioned_transaction_info, + payer_info, + system_info, + rent, + )?; + + Ok(()) +} + +// Generic function to create versioned_transaction account +pub fn process_create_versioned_transaction_account<'a>( + program_id: &Pubkey, + option_index: u8, + ephemeral_signers: u8, + transaction_index: u16, + transaction_message: Vec, + proposal_info: &AccountInfo<'a>, + proposal_versioned_transaction_info: &AccountInfo<'a>, + payer_info: &AccountInfo<'a>, + system_info: &AccountInfo<'a>, + rent: &Rent, +) -> ProgramResult { + let transaction_message = TransactionMessage::deserialize(&mut transaction_message.as_slice())?; + + let ephemeral_signer_bumps: Vec = (0..ephemeral_signers) + .map(|ephemeral_signer_index| { + let ephemeral_signer_seeds = &[ + VERSIONED_TRANSACTION_BUFFER_SEED, + proposal_versioned_transaction_info.key.as_ref(), + EPHERMAL_SIGNER_SEED, + &transaction_index.to_le_bytes(), + &ephemeral_signer_index.to_le_bytes(), + ]; + + let (_, bump) = Pubkey::find_program_address(ephemeral_signer_seeds, program_id); + bump + }) + .collect(); + + let proposal_versioned_transaction_data = ProposalVersionedTransaction { + account_type: GovernanceAccountType::ProposalVersionedTransaction, + proposal: *proposal_info.key, + option_index, + transaction_index, + executed_at: None, + execution_index: 0, + ephemeral_signer_bumps, + message: transaction_message.try_into()?, + execution_status: TransactionExecutionStatus::None, + }; + create_and_serialize_account_signed::( + payer_info, + proposal_versioned_transaction_info, + &proposal_versioned_transaction_data, + &get_proposal_versioned_transaction_address_seeds( + proposal_info.key, + &option_index.to_le_bytes(), + &transaction_index.to_le_bytes(), + ), + program_id, + system_info, + rent, + 0, + )?; + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction_from_buffer.rs b/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction_from_buffer.rs new file mode 100644 index 000000000..17c648eca --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_insert_proposal_versioned_transaction_from_buffer.rs @@ -0,0 +1,126 @@ +//! Program state processor + +use { + super::process_create_versioned_transaction_account, + crate::{ + error::GovernanceError, + state::{ + governance::get_governance_data, + proposal::get_proposal_data_for_governance, + proposal_transaction_buffer::{ + get_proposal_transaction_buffer_address, + get_proposal_transaction_buffer_data_for_proposal, + }, + token_owner_record::get_token_owner_record_data_for_proposal_owner, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + pubkey::Pubkey, + rent::Rent, + sysvar::Sysvar, + }, + spl_governance_tools::account::dispose_account, + std::cmp::Ordering, +}; + +/// Processes InsertVersionedTransactionFromBuffer instruction +pub fn process_insert_versioned_transaction_from_buffer( + program_id: &Pubkey, + accounts: &[AccountInfo], + option_index: u8, + // Number of ephemeral signing PDAs required by the transaction. + ephemeral_signers: u8, + transaction_index: u16, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let governance_info = next_account_info(account_info_iter)?; // 0 + let proposal_info = next_account_info(account_info_iter)?; // 1 + let token_owner_record_info = next_account_info(account_info_iter)?; // 2 + let governance_authority_info = next_account_info(account_info_iter)?; // 3 + + let proposal_versioned_transaction_info = next_account_info(account_info_iter)?; // 4 + let proposal_transaction_buffer_info = next_account_info(account_info_iter)?; // 4 + + let payer_info = next_account_info(account_info_iter)?; // 5 + let system_info = next_account_info(account_info_iter)?; // 6 + let rent = &Rent::get()?; + + if !payer_info.is_signer { + return Err(GovernanceError::TransactionCreatorMustSign.into()); + } + if !proposal_versioned_transaction_info.data_is_empty() { + return Err(GovernanceError::VersionedTransactionAlreadyExists.into()); + } + + // Governance account is no longer used and it's deserialized only to validate + // the provided account + let _governance_data = get_governance_data(program_id, governance_info)?; + + let mut proposal_data = + get_proposal_data_for_governance(program_id, proposal_info, governance_info.key)?; + proposal_data.assert_can_edit_instructions()?; + + let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let proposal_transaction_buffer_data = get_proposal_transaction_buffer_data_for_proposal( + program_id, + proposal_transaction_buffer_info, + proposal_info.key, + )?; + + let proposal_transaction_buffer_address = get_proposal_transaction_buffer_address( + program_id, + proposal_info.key, + payer_info.key, + &proposal_transaction_buffer_data.buffer_index.to_le_bytes(), + ); + if proposal_transaction_buffer_address != *proposal_transaction_buffer_info.key { + msg!("Proposal transaction buffer address does not match"); + return Err(GovernanceError::InvalidAccountFound.into()); + } + proposal_transaction_buffer_data.validate_hash()?; + proposal_transaction_buffer_data.validate_size()?; + + let option = &mut proposal_data.options[option_index as usize]; + + match transaction_index.cmp(&option.transactions_next_index) { + Ordering::Greater => return Err(GovernanceError::InvalidTransactionIndex.into()), + // If the index is the same as transactions_next_index then we are adding a new transaction + // If the index is below transactions_next_index then we are inserting into an existing + // empty space + Ordering::Equal => { + option.transactions_next_index = option.transactions_next_index.checked_add(1).unwrap(); + } + Ordering::Less => {} + } + + option.transactions_count = option.transactions_count.checked_add(1).unwrap(); + proposal_data.serialize(&mut proposal_info.data.borrow_mut()[..])?; + + process_create_versioned_transaction_account( + program_id, + option_index, + ephemeral_signers, + transaction_index, + proposal_transaction_buffer_data.buffer, + proposal_info, + proposal_versioned_transaction_info, + payer_info, + system_info, + rent, + )?; + + dispose_account(proposal_transaction_buffer_info, payer_info)?; + + Ok(()) +} diff --git a/governance/program/src/processor/proposal_versioned_transactions/process_remove_versioned_transaction.rs b/governance/program/src/processor/proposal_versioned_transactions/process_remove_versioned_transaction.rs new file mode 100644 index 000000000..dd93619a3 --- /dev/null +++ b/governance/program/src/processor/proposal_versioned_transactions/process_remove_versioned_transaction.rs @@ -0,0 +1,63 @@ +//! Program state processor + +use { + crate::{ + error::GovernanceError, + state::{ + proposal::get_proposal_data, + proposal_versioned_transaction::get_proposal_versioned_transaction_data_for_proposal, + token_owner_record::get_token_owner_record_data_for_proposal_owner, + }, + }, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + pubkey::Pubkey, + }, + spl_governance_tools::account::dispose_account, +}; + +/// Processes RemoveVersionedTransaction instruction +pub fn process_remove_versioned_transaction( + program_id: &Pubkey, + accounts: &[AccountInfo], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let proposal_info = next_account_info(account_info_iter)?; // 0 + let token_owner_record_info = next_account_info(account_info_iter)?; // 1 + let governance_authority_info = next_account_info(account_info_iter)?; // 2 + + let proposal_versioned_transaction_info = next_account_info(account_info_iter)?; // 3 + let beneficiary_info = next_account_info(account_info_iter)?; // 4 + + if proposal_versioned_transaction_info.data_is_empty() { + return Err(GovernanceError::VersionedTransactionAlreadyRemoved.into()); + } + let mut proposal_data = get_proposal_data(program_id, proposal_info)?; + proposal_data.assert_can_edit_instructions()?; + + let token_owner_record_data = get_token_owner_record_data_for_proposal_owner( + program_id, + token_owner_record_info, + &proposal_data.token_owner_record, + )?; + + token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?; + + let proposal_versioned_transaction_data = get_proposal_versioned_transaction_data_for_proposal( + program_id, + proposal_versioned_transaction_info, + proposal_info.key, + )?; + + dispose_account(proposal_versioned_transaction_info, beneficiary_info)?; + + let option = + &mut proposal_data.options[proposal_versioned_transaction_data.option_index as usize]; + option.transactions_count = option.transactions_count.checked_sub(1).unwrap(); + + proposal_data.serialize(&mut proposal_info.data.borrow_mut()[..])?; + + Ok(()) +} diff --git a/governance/program/src/state/enums.rs b/governance/program/src/state/enums.rs index fb5a8f002..a6484c54e 100644 --- a/governance/program/src/state/enums.rs +++ b/governance/program/src/state/enums.rs @@ -102,6 +102,15 @@ pub enum GovernanceAccountType { /// Required signatory account RequiredSignatory, + + /// ProposalVersionedTransaction account which holds instructions to execute for + /// Proposal within a single Versioned Transaction + /// and adds index for proposal option and multiple instructions + ProposalVersionedTransaction, + + /// ProposalTransactionBuffer account which holds instruction buffer which would then create + /// a ProposalVersionedTransaction. + ProposalTransactionBuffer, } /// What state a Proposal is in @@ -205,9 +214,10 @@ pub enum VoteTipping { } /// The status of instruction execution -#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)] +#[derive(Clone, Debug, Default, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)] pub enum TransactionExecutionStatus { /// Transaction was not executed yet + #[default] None, /// Transaction was executed successfully diff --git a/governance/program/src/state/governance.rs b/governance/program/src/state/governance.rs index e66db9004..151fb9dc6 100644 --- a/governance/program/src/state/governance.rs +++ b/governance/program/src/state/governance.rs @@ -160,6 +160,8 @@ pub fn is_governance_v2_account_type(account_type: &GovernanceAccountType) -> bo | GovernanceAccountType::SignatoryRecordV2 | GovernanceAccountType::ProposalInstructionV1 | GovernanceAccountType::ProposalTransactionV2 + | GovernanceAccountType::ProposalVersionedTransaction + | GovernanceAccountType::ProposalTransactionBuffer | GovernanceAccountType::VoteRecordV1 | GovernanceAccountType::VoteRecordV2 | GovernanceAccountType::ProgramMetadata @@ -196,6 +198,8 @@ pub fn try_get_governance_v2_type_for_v1( | GovernanceAccountType::SignatoryRecordV2 | GovernanceAccountType::ProposalInstructionV1 | GovernanceAccountType::ProposalTransactionV2 + | GovernanceAccountType::ProposalVersionedTransaction + | GovernanceAccountType::ProposalTransactionBuffer | GovernanceAccountType::VoteRecordV1 | GovernanceAccountType::VoteRecordV2 | GovernanceAccountType::ProgramMetadata @@ -243,6 +247,8 @@ impl GovernanceV2 { | GovernanceAccountType::RealmConfig | GovernanceAccountType::VoteRecordV2 | GovernanceAccountType::ProposalTransactionV2 + | GovernanceAccountType::ProposalVersionedTransaction + | GovernanceAccountType::ProposalTransactionBuffer | GovernanceAccountType::ProposalV2 | GovernanceAccountType::ProgramMetadata | GovernanceAccountType::ProposalDeposit @@ -505,7 +511,7 @@ pub fn assert_governance_for_realm( Ok(()) } -/// Returns ProgramGovernance PDA seeds +/// Returns Legacy ProgramGovernance PDA seeds pub fn get_program_governance_address_seeds<'a>( realm: &'a Pubkey, governed_program: &'a Pubkey, @@ -520,7 +526,7 @@ pub fn get_program_governance_address_seeds<'a>( ] } -/// Returns ProgramGovernance PDA address +/// Returns Legacy ProgramGovernance PDA address pub fn get_program_governance_address<'a>( program_id: &Pubkey, realm: &'a Pubkey, @@ -533,7 +539,7 @@ pub fn get_program_governance_address<'a>( .0 } -/// Returns MintGovernance PDA seeds +/// Returns Legacy MintGovernance PDA seeds pub fn get_mint_governance_address_seeds<'a>( realm: &'a Pubkey, governed_mint: &'a Pubkey, @@ -544,7 +550,7 @@ pub fn get_mint_governance_address_seeds<'a>( [b"mint-governance", realm.as_ref(), governed_mint.as_ref()] } -/// Returns MintGovernance PDA address +/// Returns Legacy MintGovernance PDA address pub fn get_mint_governance_address<'a>( program_id: &Pubkey, realm: &'a Pubkey, @@ -557,7 +563,7 @@ pub fn get_mint_governance_address<'a>( .0 } -/// Returns TokenGovernance PDA seeds +/// Returns Legacy TokenGovernance PDA seeds pub fn get_token_governance_address_seeds<'a>( realm: &'a Pubkey, governed_token: &'a Pubkey, @@ -568,7 +574,7 @@ pub fn get_token_governance_address_seeds<'a>( [b"token-governance", realm.as_ref(), governed_token.as_ref()] } -/// Returns TokenGovernance PDA address +/// Returns Legacy TokenGovernance PDA address pub fn get_token_governance_address<'a>( program_id: &Pubkey, realm: &'a Pubkey, @@ -724,7 +730,6 @@ mod test { // Act let size = borsh::to_vec(&governance_data).unwrap().len(); - // Assert assert_eq!(governance_data.get_max_size(), Some(size)); } diff --git a/governance/program/src/state/legacy.rs b/governance/program/src/state/legacy.rs index d2c659795..878db7e6c 100644 --- a/governance/program/src/state/legacy.rs +++ b/governance/program/src/state/legacy.rs @@ -159,6 +159,8 @@ pub fn is_governance_v1_account_type(account_type: &GovernanceAccountType) -> bo | GovernanceAccountType::SignatoryRecordV2 | GovernanceAccountType::ProposalInstructionV1 | GovernanceAccountType::ProposalTransactionV2 + | GovernanceAccountType::ProposalVersionedTransaction + | GovernanceAccountType::ProposalTransactionBuffer | GovernanceAccountType::VoteRecordV1 | GovernanceAccountType::VoteRecordV2 | GovernanceAccountType::ProgramMetadata diff --git a/governance/program/src/state/mod.rs b/governance/program/src/state/mod.rs index aa9ebf56b..253dbfe0e 100644 --- a/governance/program/src/state/mod.rs +++ b/governance/program/src/state/mod.rs @@ -8,6 +8,8 @@ pub mod program_metadata; pub mod proposal; pub mod proposal_deposit; pub mod proposal_transaction; +pub mod proposal_transaction_buffer; +pub mod proposal_versioned_transaction; pub mod realm; pub mod realm_config; pub mod required_signatory; diff --git a/governance/program/src/state/proposal.rs b/governance/program/src/state/proposal.rs index 8b28f88b7..fd9134943 100644 --- a/governance/program/src/state/proposal.rs +++ b/governance/program/src/state/proposal.rs @@ -15,6 +15,7 @@ use { governance::GovernanceConfig, legacy::ProposalV1, proposal_transaction::ProposalTransactionV2, + proposal_versioned_transaction::ProposalVersionedTransaction, realm::RealmV2, realm_config::RealmConfigAccount, vote_record::{Vote, VoteKind}, @@ -880,6 +881,52 @@ impl ProposalV2 { Ok(()) } + /// Checks if Instructions can be executed for the Proposal in the given + /// state + pub fn assert_can_execute_versioned_transaction( + &self, + proposal_versioned_transaction_data: &ProposalVersionedTransaction, + governance_config: &GovernanceConfig, + current_unix_timestamp: UnixTimestamp, + ) -> Result<(), ProgramError> { + match self.state { + ProposalState::Succeeded + | ProposalState::Executing + | ProposalState::ExecutingWithErrors => {} + ProposalState::Draft + | ProposalState::SigningOff + | ProposalState::Completed + | ProposalState::Voting + | ProposalState::Cancelled + | ProposalState::Defeated + | ProposalState::Vetoed => { + return Err(GovernanceError::InvalidStateCannotExecuteTransaction.into()) + } + } + + if self.options[proposal_versioned_transaction_data.option_index as usize].vote_result + != OptionVoteResult::Succeeded + { + return Err(GovernanceError::CannotExecuteDefeatedOption.into()); + } + + if self + .voting_completed_at + .unwrap() + .checked_add(governance_config.min_transaction_hold_up_time as i64) + .unwrap() + >= current_unix_timestamp + { + return Err(GovernanceError::CannotExecuteTransactionWithinHoldUpTime.into()); + } + + if proposal_versioned_transaction_data.executed_at.is_some() { + return Err(GovernanceError::TransactionAlreadyExecuted.into()); + } + + Ok(()) + } + /// Checks if the instruction can be flagged with error for the Proposal in /// the given state pub fn assert_can_flag_transaction_error( @@ -1152,9 +1199,11 @@ pub fn get_proposal_data( reserved: [0; 64], reserved1: 0, }); + } else if account_type == GovernanceAccountType::ProposalV2 { + get_account_data::(program_id, proposal_info) + } else { + return Err(GovernanceError::InvalidAccountType.into()); } - - get_account_data::(program_id, proposal_info) } /// Deserializes Proposal and validates it belongs to the given Governance and diff --git a/governance/program/src/state/proposal_transaction.rs b/governance/program/src/state/proposal_transaction.rs index 2ab96ce72..2f62f0734 100644 --- a/governance/program/src/state/proposal_transaction.rs +++ b/governance/program/src/state/proposal_transaction.rs @@ -229,9 +229,11 @@ pub fn get_proposal_transaction_data( execution_status: proposal_transaction_data_v1.execution_status, reserved_v2: [0; 8], }); + } else if account_type == GovernanceAccountType::ProposalTransactionV2 { + get_account_data::(program_id, proposal_transaction_info) + } else { + return Err(GovernanceError::InvalidAccountType.into()); } - - get_account_data::(program_id, proposal_transaction_info) } /// Deserializes and returns ProposalTransaction account and checks it belongs diff --git a/governance/program/src/state/proposal_transaction_buffer.rs b/governance/program/src/state/proposal_transaction_buffer.rs new file mode 100644 index 000000000..90441f73e --- /dev/null +++ b/governance/program/src/state/proposal_transaction_buffer.rs @@ -0,0 +1,188 @@ +//! ProposalTransactionBuffer Account + +use { + super::enums::GovernanceAccountType, + crate::error::GovernanceError, + borsh::{io::Write, BorshDeserialize, BorshSchema, BorshSerialize}, + solana_program::{ + account_info::AccountInfo, hash::hashv, msg, program_error::ProgramError, + program_pack::IsInitialized, pubkey::Pubkey, + }, + spl_governance_tools::account::{get_account_data, AccountMaxSize}, +}; + +/// Maximum PDA allocation size in an inner ix is 10240 bytes. +/// 10240 - account contents = 10032 bytes +pub const MAX_BUFFER_SIZE: usize = 10032; + +/// One of onchain buffer that consumes buffers and transforms them into +/// Versioned Transactions This account will be closed once it gets transformed +/// into ProposalVersionedTransaction +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct ProposalTransactionBuffer { + /// Governance Account type + pub account_type: GovernanceAccountType, + /// The Proposal the transaction buffer belongs to + pub proposal: Pubkey, + /// Member of the Goverenance who created the TransactionBuffer. + pub creator: Pubkey, + /// Index to seed address derivation + pub buffer_index: u8, + /// Hash of the final assembled transaction message. + pub final_buffer_hash: [u8; 32], + /// The size of the final assembled transaction message. + pub final_buffer_size: u16, + /// The buffer of the transaction message. + pub buffer: Vec, +} + +impl AccountMaxSize for ProposalTransactionBuffer { + fn get_max_size(&self) -> Option { + Some( + 1 + // account discriminator + 1 + // account type + 32 + // proposal + 32 + // creator + 1 + // buffer_index + 32 + // transaction_message_hash + 2 + // final_buffer_size + 4 + // vec length bytes + self.final_buffer_size as usize, // buffer + ) + } +} + +impl IsInitialized for ProposalTransactionBuffer { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::ProposalTransactionBuffer + } +} + +impl ProposalTransactionBuffer { + /// Size of onchain transaction buffer + pub fn size(final_message_buffer_size: u16) -> Result { + // Make sure final size is not greater than MAX_BUFFER_SIZE bytes. + if (final_message_buffer_size as usize) > MAX_BUFFER_SIZE { + return Err(GovernanceError::FinalBufferSizeExceeded.into()); + } + Ok( + 1 + // account discriminator + 32 + // proposal + 32 + // creator + 1 + // buffer_index + 32 + // transaction_message_hash + 2 + // final_buffer_size + 4 + // vec length bytes + final_message_buffer_size as usize, // buffer + ) + } + + /// validate the final buffer has of the transaction buffer + pub fn validate_hash(&self) -> Result<(), ProgramError> { + let message_buffer_hash = hashv(&[self.buffer.as_slice()]); + if message_buffer_hash.to_bytes() != self.final_buffer_hash { + return Err(GovernanceError::FinalBufferHashMismatch.into()); + } + Ok(()) + } + + /// validate the size of buffer of the transaction buffer + pub fn validate_size(&self) -> Result<(), ProgramError> { + if self.buffer.len() != self.final_buffer_size as usize { + return Err(GovernanceError::FinalBufferSizeMismatch.into()); + } + Ok(()) + } + + /// Check to make validate the size of buffer of the transaction buffer + pub fn invariant(&self) -> Result<(), ProgramError> { + if self.final_buffer_size as usize > MAX_BUFFER_SIZE { + msg!("Current final buffer size: {}", self.final_buffer_size); + return Err(GovernanceError::FinalBufferSizeExceeded.into()); + } + if self.buffer.len() > self.final_buffer_size as usize { + msg!( + "Current buffer size: {}, is larger than final buffer size: {}", + self.buffer.len(), + self.final_buffer_size + ); + return Err(GovernanceError::FinalBufferSizeExceeded.into()); + } + + Ok(()) + } + + /// Serializes account into the target buffer + pub fn serialize(self, writer: W) -> Result<(), ProgramError> { + borsh::to_writer(writer, &self)?; + Ok(()) + } +} + +/// Seed prefix for ProposalTransactionBuffer PDAs +pub const TRANSACTION_BUFFER_SEED: &[u8] = b"transaction_buffer"; + +/// Returns ProposalTransactionBuffer PDA seeds +pub fn get_proposal_transaction_buffer_address_seeds<'a>( + proposal: &'a Pubkey, + creator: &'a Pubkey, + buffer_index: &'a [u8; 1], // u8 le bytes +) -> [&'a [u8]; 4] { + [ + TRANSACTION_BUFFER_SEED, + proposal.as_ref(), + creator.as_ref(), + buffer_index, + ] +} + +/// Returns ProposalTransactionBuffer PDA address +pub fn get_proposal_transaction_buffer_address<'a>( + program_id: &Pubkey, + proposal: &'a Pubkey, + creator: &'a Pubkey, + buffer_index: &'a [u8; 1], // u8 le bytes +) -> Pubkey { + Pubkey::find_program_address( + &get_proposal_transaction_buffer_address_seeds(proposal, creator, buffer_index), + program_id, + ) + .0 +} + +/// Deserializes ProposalTransactionBuffer account and checks owner program +pub fn get_proposal_transaction_buffer_data( + program_id: &Pubkey, + proposal_transaction_buffer_info: &AccountInfo, +) -> Result { + let proposal_transaction_buffer_data = get_account_data::( + program_id, + proposal_transaction_buffer_info, + )?; + + if proposal_transaction_buffer_data.account_type + != GovernanceAccountType::ProposalTransactionBuffer + { + msg!("Invalid proposal transaction buffer account type"); + return Err(GovernanceError::InvalidGovernanceForProposal.into()); + } + Ok(proposal_transaction_buffer_data) +} + +/// Deserializes ProposalTransactionBuffer and validates it belongs to the given +/// Governance +pub fn get_proposal_transaction_buffer_data_for_proposal( + program_id: &Pubkey, + proposal_transaction_buffer_info: &AccountInfo, + proposal: &Pubkey, +) -> Result { + let proposal_transaction_buffer_data = + get_proposal_transaction_buffer_data(program_id, proposal_transaction_buffer_info)?; + + if proposal_transaction_buffer_data.proposal != *proposal { + msg!("Mismatch of proposal for proposal transaction buffer"); + return Err(GovernanceError::InvalidGovernanceForProposal.into()); + } + + Ok(proposal_transaction_buffer_data) +} diff --git a/governance/program/src/state/proposal_versioned_transaction.rs b/governance/program/src/state/proposal_versioned_transaction.rs new file mode 100644 index 000000000..7bb546657 --- /dev/null +++ b/governance/program/src/state/proposal_versioned_transaction.rs @@ -0,0 +1,348 @@ +//! ProposalVersionedTransaction Account + +use { + crate::{ + error::GovernanceError, + state::enums::{GovernanceAccountType, TransactionExecutionStatus}, + tools::transaction_message::{ + CompiledInstruction, MessageAddressTableLookup, TransactionMessage, + }, + }, + borsh::{io::Write, BorshDeserialize, BorshSchema, BorshSerialize}, + solana_program::{ + account_info::AccountInfo, borsh1::get_instance_packed_len, clock::UnixTimestamp, msg, + program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, + }, + spl_governance_tools::account::{get_account_data, AccountMaxSize}, +}; + +impl IsInitialized for ProposalVersionedTransaction { + fn is_initialized(&self) -> bool { + self.account_type == GovernanceAccountType::ProposalVersionedTransaction + } +} + +impl ProposalVersionedTransaction { + /// Serializes account into the target buffer + pub fn serialize(self, writer: W) -> Result<(), ProgramError> { + borsh::to_writer(writer, &self)?; + + Ok(()) + } +} + +/// Account for an instruction to be executed for Proposal +#[derive(Clone, Default, Debug, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct ProposalVersionedTransaction { + /// Governance Account type + pub account_type: GovernanceAccountType, + + /// The Proposal the instruction belongs to + pub proposal: Pubkey, + + /// The option index the instruction belongs to + pub option_index: u8, + + /// Unique transaction index within it's parent Proposal + pub transaction_index: u16, + + /// Executed index must be sequential + pub execution_index: u8, + + /// Executed at flag + pub executed_at: Option, + + /// Instruction execution status + pub execution_status: TransactionExecutionStatus, + + /// Derivation bumps for additional signers. + /// Some transactions require multiple signers. Often these additional + /// signers are "ephemeral" keypairs that are generated on the client + /// with a sole purpose of signing the transaction and be discarded + /// immediately after. When wrapping such transactions into proposals, + /// we replace these "ephemeral" signing keypairs with PDAs derived from + /// the ProposalVersionedTransaction's `transaction_index` and controlled by + /// the Goverenance Program; during execution the program includes the + /// seeds of these PDAs into the `invoke_signed` calls, thus "signing" + /// on behalf of these PDAs. + pub ephemeral_signer_bumps: Vec, + + /// data required for executing the transaction. + pub message: ProposalTransactionMessage, +} + +impl AccountMaxSize for ProposalVersionedTransaction { + /// proposal versioned_transaction can only be created from + /// proposal_transaction_message + fn get_max_size(&self) -> Option { + let message_size = get_instance_packed_len(&self.message).unwrap_or_default(); + + Some( + 1 + // account discriminator + 1 + // account_type + 32 + // proposal + 1 + // option_index + 2 + // transaction_index + 1 + // execution_index + 9 + // executed_at (Option) + 1 + // execution_status + 4 + self.ephemeral_signer_bumps.len() + + message_size + + 40, // additional overhead + ) + } +} + +impl ProposalVersionedTransaction { + /// Reduces the ProposalVersionedTransaction to its default empty value and + /// moves ownership of the data to the caller/return value. + pub fn take(&mut self) -> ProposalVersionedTransaction { + core::mem::take(self) + } +} + +/// ProposalTransactionMessage Account +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize, Default, BorshSchema)] +pub struct ProposalTransactionMessage { + /// The number of signer pubkeys in the account_keys vec. + pub num_signers: u8, + /// The number of writable signer pubkeys in the account_keys vec. + pub num_writable_signers: u8, + /// The number of writable non-signer pubkeys in the account_keys vec. + pub num_writable_non_signers: u8, + /// Unique account pubkeys (including program IDs) required for execution of + /// the tx. The signer pubkeys appear at the beginning of the vec, with + /// writable pubkeys first, and read-only pubkeys following. + /// The non-signer pubkeys follow with writable pubkeys first and read-only + /// ones following. Program IDs are also stored at the end of the vec + /// along with other non-signer non-writable pubkeys: + /// + /// ```plaintext + /// [pubkey1, pubkey2, pubkey3, pubkey4, pubkey5, pubkey6, pubkey7, pubkey8] + /// |---writable---| |---readonly---| |---writable---| |---readonly---| + /// |------------signers-------------| |----------non-signers-----------| + /// ``` + pub account_keys: Vec, + /// List of instructions making up the tx. + pub instructions: Vec, + /// List of address table lookups used to load additional accounts + /// for this transaction. + pub address_table_lookups: Vec, +} + +impl ProposalTransactionMessage { + /// Returns the number of all the account keys (static + dynamic) in the + /// message. + pub fn num_all_account_keys(&self) -> usize { + let num_account_keys_from_lookups = self + .address_table_lookups + .iter() + .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len()) + .sum::(); + + self.account_keys.len() + num_account_keys_from_lookups + } + + /// Returns true if the account at the specified index is a part of static + /// `account_keys` and was requested to be writable. + pub fn is_static_writable_index(&self, key_index: usize) -> bool { + let num_account_keys = self.account_keys.len(); + let num_signers = usize::from(self.num_signers); + let num_writable_signers = usize::from(self.num_writable_signers); + let num_writable_non_signers = usize::from(self.num_writable_non_signers); + + if key_index >= num_account_keys { + // `index` is not a part of static `account_keys`. + return false; + } + + if key_index < num_writable_signers { + // `index` is within the range of writable signer keys. + return true; + } + + if key_index >= num_signers { + // `index` is within the range of non-signer keys. + let index_into_non_signers = key_index.saturating_sub(num_signers); + // Whether `index` is within the range of writable non-signer keys. + return index_into_non_signers < num_writable_non_signers; + } + + false + } + + /// Returns true if the account at the specified index was requested to be a + /// signer. + pub fn is_signer_index(&self, key_index: usize) -> bool { + key_index < usize::from(self.num_signers) + } +} + +impl TryFrom for ProposalTransactionMessage { + type Error = ProgramError; + + fn try_from(message: TransactionMessage) -> Result { + let account_keys: Vec = message.account_keys.into(); + let instructions: Vec = message.instructions.into(); + let instructions: Vec = instructions + .into_iter() + .map(ProposalCompiledInstruction::from) + .collect(); + let address_table_lookups: Vec = + message.address_table_lookups.into(); + + let num_all_account_keys = account_keys.len() + + address_table_lookups + .iter() + .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len()) + .sum::(); + + if account_keys.len() <= usize::from(message.num_signers) { + msg!("account_keys length is smaller than number of signers"); + return Err(GovernanceError::InvalidTransactionMessage.into()); + } + + if message.num_writable_signers > message.num_signers { + msg!("message writable signers length is larger than number of signers, they must be equal or less"); + return Err(GovernanceError::InvalidTransactionMessage.into()); + } + if usize::from(message.num_writable_non_signers) + > account_keys + .len() + .saturating_sub(usize::from(message.num_signers)) + { + return Err(GovernanceError::InvalidTransactionMessage.into()); + } + + // Validate that all program ID indices and account indices are within the + // bounds of the account keys. + for instruction in &instructions { + if usize::from(instruction.program_id_index) >= num_all_account_keys { + return Err(GovernanceError::InvalidTransactionMessage.into()); + } + for account_index in &instruction.account_indexes { + if usize::from(*account_index) >= num_all_account_keys { + return Err(GovernanceError::InvalidTransactionMessage.into()); + } + } + } + + Ok(Self { + num_signers: message.num_signers, + num_writable_signers: message.num_writable_signers, + num_writable_non_signers: message.num_writable_non_signers, + account_keys, + instructions, + address_table_lookups: address_table_lookups + .into_iter() + .map(VersionedTransactionMessageAddressTableLookup::from) + .collect(), + }) + } +} + +/// Concise serialization schema for instructions that make up a transaction. +/// Closely mimics the Solana transaction wire format. +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct ProposalCompiledInstruction { + /// Indices of the program_id in tx's account_keys + pub program_id_index: u8, + /// Indices into the tx's `account_keys` list indicating which accounts to + /// pass to the instruction. + pub account_indexes: Vec, + /// Instruction data. + pub data: Vec, +} + +impl From for ProposalCompiledInstruction { + fn from(compiled_instruction: CompiledInstruction) -> Self { + Self { + program_id_index: compiled_instruction.program_id_index, + account_indexes: compiled_instruction.account_indexes.into(), + data: compiled_instruction.data.into(), + } + } +} + +/// Address table lookups describe an on-chain address lookup table to use +/// for loading more readonly and writable accounts into a transaction. +#[derive(Clone, Debug, PartialEq, Eq, BorshDeserialize, BorshSerialize, BorshSchema)] +pub struct VersionedTransactionMessageAddressTableLookup { + /// Address lookup table account key. + pub account_key: Pubkey, + /// List of indexes used to load writable accounts. + pub writable_indexes: Vec, + /// List of indexes used to load readonly accounts. + pub readonly_indexes: Vec, +} + +impl From for VersionedTransactionMessageAddressTableLookup { + fn from(m: MessageAddressTableLookup) -> Self { + Self { + account_key: m.account_key, + writable_indexes: m.writable_indexes.into(), + readonly_indexes: m.readonly_indexes.into(), + } + } +} + +/// Seed prefix for ProposalTransactionBuffer PDAs +pub const VERSIONED_TRANSACTION_BUFFER_SEED: &[u8] = b"version_transaction"; + +/// Returns ProposalVersionedTransaction PDA seeds +pub fn get_proposal_versioned_transaction_address_seeds<'a>( + proposal: &'a Pubkey, + option_index: &'a [u8; 1], // u8 le bytes + transaction_index_le_bytes: &'a [u8; 2], // u16 le bytes +) -> [&'a [u8]; 4] { + [ + VERSIONED_TRANSACTION_BUFFER_SEED, + proposal.as_ref(), + option_index, + transaction_index_le_bytes, + ] +} + +/// Returns ProposalVersionedTransaction PDA address +pub fn get_proposal_versioned_transaction_address<'a>( + program_id: &Pubkey, + proposal: &'a Pubkey, + option_index_le_bytes: &'a [u8; 1], // u8 le bytes + transaction_index_le_bytes: &'a [u8; 2], // u16 le bytes +) -> Pubkey { + Pubkey::find_program_address( + &get_proposal_versioned_transaction_address_seeds( + proposal, + option_index_le_bytes, + transaction_index_le_bytes, + ), + program_id, + ) + .0 +} + +/// Deserializes ProposalVersionedTransaction account and checks owner program +pub fn get_proposal_versioned_transaction_data( + program_id: &Pubkey, + proposal_transaction_info: &AccountInfo, +) -> Result { + get_account_data::(program_id, proposal_transaction_info) +} + +/// Deserializes and returns ProposalVersionedTransaction account and checks it +/// belongs to the given Proposal +pub fn get_proposal_versioned_transaction_data_for_proposal( + program_id: &Pubkey, + proposal_transaction_info: &AccountInfo, + proposal: &Pubkey, +) -> Result { + let proposal_transaction_data = + get_proposal_versioned_transaction_data(program_id, proposal_transaction_info)?; + + if proposal_transaction_data.proposal != *proposal { + msg!("Mismatch of proposal for proposal versioned transaction"); + return Err(GovernanceError::InvalidProposalForProposalTransaction.into()); + } + + Ok(proposal_transaction_data) +} diff --git a/governance/program/src/state/realm.rs b/governance/program/src/state/realm.rs index e43609d05..702c6838e 100644 --- a/governance/program/src/state/realm.rs +++ b/governance/program/src/state/realm.rs @@ -196,6 +196,8 @@ pub fn is_realm_account_type(account_type: &GovernanceAccountType) -> bool { | GovernanceAccountType::SignatoryRecordV2 | GovernanceAccountType::ProposalInstructionV1 | GovernanceAccountType::ProposalTransactionV2 + | GovernanceAccountType::ProposalVersionedTransaction + | GovernanceAccountType::ProposalTransactionBuffer | GovernanceAccountType::VoteRecordV1 | GovernanceAccountType::VoteRecordV2 | GovernanceAccountType::ProgramMetadata @@ -382,9 +384,11 @@ pub fn get_realm_data( // Add the extra reserved_v2 padding reserved_v2: [0; 128], }); + } else if account_type == GovernanceAccountType::RealmV2 { + get_account_data::(program_id, realm_info) + } else { + return Err(GovernanceError::InvalidAccountType.into()); } - - get_account_data::(program_id, realm_info) } /// Deserializes account and checks the given authority is Realm's authority diff --git a/governance/program/src/state/realm_config.rs b/governance/program/src/state/realm_config.rs index d252aa05e..bd3b80da3 100644 --- a/governance/program/src/state/realm_config.rs +++ b/governance/program/src/state/realm_config.rs @@ -16,7 +16,6 @@ use { pubkey::Pubkey, }, spl_governance_tools::account::{get_account_data, AccountMaxSize}, - std::slice::Iter, }; /// The type of the governing token defines: @@ -273,10 +272,13 @@ pub fn get_realm_config_address(program_id: &Pubkey, realm: &Pubkey) -> Pubkey { } /// Resolves GoverningTokenConfig from GoverningTokenConfigArgs and instruction /// accounts -pub fn resolve_governing_token_config( - account_info_iter: &mut Iter, +pub fn resolve_governing_token_config<'a, I>( + account_info_iter: &mut I, governing_token_config_args: &GoverningTokenConfigArgs, -) -> Result { +) -> Result +where + I: Iterator>, +{ let voter_weight_addin = if governing_token_config_args.use_voter_weight_addin { let voter_weight_addin_info = next_account_info(account_info_iter)?; Some(*voter_weight_addin_info.key) diff --git a/governance/program/src/state/signatory_record.rs b/governance/program/src/state/signatory_record.rs index c20a8abd4..3e3f7ade8 100644 --- a/governance/program/src/state/signatory_record.rs +++ b/governance/program/src/state/signatory_record.rs @@ -140,9 +140,11 @@ pub fn get_signatory_record_data( // Add the extra reserved_v2 padding reserved_v2: [0; 8], }); + } else if account_type == GovernanceAccountType::SignatoryRecordV2 { + get_account_data::(program_id, signatory_record_info) + } else { + return Err(GovernanceError::InvalidAccountType.into()); } - - get_account_data::(program_id, signatory_record_info) } /// Deserializes SignatoryRecord and validates its PDA diff --git a/governance/program/src/state/token_owner_record.rs b/governance/program/src/state/token_owner_record.rs index 9c1cd9506..bf8fdb09c 100644 --- a/governance/program/src/state/token_owner_record.rs +++ b/governance/program/src/state/token_owner_record.rs @@ -363,8 +363,10 @@ pub fn get_token_owner_record_data( // Add the extra reserved_v2 padding reserved_v2: [0; 128], } - } else { + } else if account_type == GovernanceAccountType::TokenOwnerRecordV2 { get_account_data::(program_id, token_owner_record_info)? + } else { + return Err(GovernanceError::InvalidAccountType.into()); }; // If the deserialized account uses the old account layout indicated by the @@ -450,10 +452,7 @@ pub fn get_token_owner_record_data_for_proposal_owner( #[cfg(test)] mod test { - use { - super::*, - solana_program::stake_history::Epoch, - }; + use {super::*, solana_program::stake_history::Epoch}; fn create_test_token_owner_record() -> TokenOwnerRecordV2 { TokenOwnerRecordV2 { @@ -494,7 +493,6 @@ mod test { // Act let size = borsh::to_vec(&token_owner_record).unwrap().len(); - // Assert assert_eq!(token_owner_record.get_max_size(), Some(size)); } diff --git a/governance/program/src/state/vote_record.rs b/governance/program/src/state/vote_record.rs index 8606ba020..32a3a775c 100644 --- a/governance/program/src/state/vote_record.rs +++ b/governance/program/src/state/vote_record.rs @@ -204,9 +204,11 @@ pub fn get_vote_record_data( vote, reserved_v2: [0; 8], }); + } else if account_type == GovernanceAccountType::VoteRecordV2 { + get_account_data::(program_id, vote_record_info) + } else { + return Err(GovernanceError::InvalidAccountType.into()); } - - get_account_data::(program_id, vote_record_info) } /// Deserializes VoteRecord and checks it belongs to the provided Proposal and diff --git a/governance/program/src/tools/ephermal_signers.rs b/governance/program/src/tools/ephermal_signers.rs new file mode 100644 index 000000000..909a86a09 --- /dev/null +++ b/governance/program/src/tools/ephermal_signers.rs @@ -0,0 +1,47 @@ +//! General purpose ephermal_signers utility functions + +use crate::state::proposal_versioned_transaction::VERSIONED_TRANSACTION_BUFFER_SEED; +use solana_program::pubkey::Pubkey; + +/// Seed prefix for EphermalSigners PDAs +pub const EPHERMAL_SIGNER_SEED: &[u8] = b"ephemeral_signer"; + +/// Return a tuple of ephemeral_signer_keys and ephemeral_signer_seeds derived +/// from the given `ephemeral_signer_bumps` and `transaction_proposal`. +pub fn derive_ephemeral_signers( + program_id: &Pubkey, + transaction_proposal: &Pubkey, + ephemeral_signer_bumps: &[u8], + transaction_index: u16, +) -> (Vec, Vec>>) { + ephemeral_signer_bumps + .iter() + .enumerate() + .map(|(index, bump)| { + let seeds = vec![ + VERSIONED_TRANSACTION_BUFFER_SEED.to_vec(), + transaction_proposal.to_bytes().to_vec(), + EPHERMAL_SIGNER_SEED.to_vec(), + u16::try_from(transaction_index) + .unwrap() + .to_le_bytes() + .to_vec(), + u8::try_from(index).unwrap().to_le_bytes().to_vec(), + vec![*bump], + ]; + + ( + Pubkey::create_program_address( + seeds + .iter() + .map(Vec::as_slice) + .collect::>() + .as_slice(), + program_id, + ) + .unwrap(), + seeds, + ) + }) + .unzip() +} diff --git a/governance/program/src/tools/executable_transaction_message.rs b/governance/program/src/tools/executable_transaction_message.rs new file mode 100644 index 000000000..146425ee4 --- /dev/null +++ b/governance/program/src/tools/executable_transaction_message.rs @@ -0,0 +1,358 @@ +//! General purpose ExecutableTransactionMessage utility functions + +use { + crate::{ + error::GovernanceError, state::proposal_versioned_transaction::ProposalTransactionMessage, + }, + solana_program::{ + account_info::AccountInfo, + address_lookup_table::{self, state::AddressLookupTable}, + instruction::{AccountMeta, Instruction}, + msg, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + }, + std::{collections::HashMap, convert::From}, +}; + +/// Sanitized and validated combination of a `ProposalTransactionMessage` and +/// `AccountInfo`s it references. +pub struct ExecutableTransactionMessage<'a, 'info> { + /// Message which loaded a collection of lookup table addresses. + message: ProposalTransactionMessage, + /// Resolved `account_keys` of the message. + static_accounts: Vec<&'a AccountInfo<'info>>, + /// Concatenated vector of resolved `writable_indexes` from all address + /// lookups. + loaded_writable_accounts: Vec<&'a AccountInfo<'info>>, + /// Concatenated vector of resolved `readonly_indexes` from all address + /// lookups. + loaded_readonly_accounts: Vec<&'a AccountInfo<'info>>, +} + +impl<'a, 'info> ExecutableTransactionMessage<'a, 'info> { + /// # Arguments + /// `message` - a `ProposalTransactionMessage`. + /// `message_account_infos` - AccountInfo's that are expected to be + /// mentioned in the message. `address_lookup_table_account_infos` - + /// AccountInfo's that are expected to correspond to the lookup tables + /// mentioned in `message.address_table_lookups`. `native_treasury_pubkey` - The + /// native_treasury_pubkey PDA that is expected to sign the message. + /// `governance_pubkey` - The governance PDA that is expected to sign the + /// message. `ephemeral_signer_pdas` - The ephemeral signer PDAs that are + /// expected to sign the message. + pub fn new_validated( + message: ProposalTransactionMessage, + message_account_infos: &'a [AccountInfo<'info>], + address_lookup_table_account_infos: &'a [AccountInfo<'info>], + native_treasury_pubkey: &'a Pubkey, + governance_pubkey: &'a Pubkey, + ephemeral_signer_pdas: &'a [Pubkey], + proposal_voting_at_slot: u64 + ) -> Result { + // CHECK: `address_lookup_table_account_infos` must be valid + // `AddressLookupTable`s and be the ones mentioned in + // `message.address_table_lookups` + if address_lookup_table_account_infos.len() != message.address_table_lookups.len() { + return Err(GovernanceError::InvalidNumberOfAccounts.into()); + } + let lookup_tables: HashMap<&Pubkey, &AccountInfo> = address_lookup_table_account_infos + .iter() + .enumerate() + .map(|(index, maybe_lookup_table)| { + // The lookup table account must be owned by SolanaAddressLookupTableProgram. + if maybe_lookup_table.owner != &address_lookup_table::program::ID { + return Err(GovernanceError::InvalidLookupTableAccountOwner.into()); + } + // The lookup table must be mentioned in `message.address_table_lookups` at the + // same index. + if message + .address_table_lookups + .get(index) + .map(|lookup| &lookup.account_key) + != Some(maybe_lookup_table.key) + { + return Err(GovernanceError::InvalidLookupTableAccountKey.into()); + } + Ok((maybe_lookup_table.key, maybe_lookup_table)) + }) + .collect::, ProgramError>>()?; + + // CHECK: `account_infos` should exactly match the number of accounts mentioned + // in the message. + if message_account_infos.len() != message.num_all_account_keys() { + return Err(GovernanceError::InvalidNumberOfAccounts.into()); + } + + let mut static_accounts = Vec::new(); + + // CHECK: `message.account_keys` should come first in `account_infos` and have + // modifiers expected by the message. + for (i, account_key) in message.account_keys.iter().enumerate() { + let account_info = &message_account_infos[i]; + + if account_info.key != account_key { + msg!( + "Account {} does not match expected account key at index {}", + account_info.key, + i + ); + return Err(GovernanceError::InvalidAccountFoundInMessage.into()); + } + // If the account is marked as signer in the message, it must be a signer in the + // account infos too. Unless it's a native_treasury or governance_pubkey or an ephemeral signer + // PDA, as they cannot be passed as signers to `remaining_accounts`, + // because they are PDA's and can't sign the transaction. + if message.is_signer_index(i) + && account_info.key != native_treasury_pubkey + && account_info.key != governance_pubkey + && !ephemeral_signer_pdas.contains(account_info.key) + { + // Verify the account is an authorized signer. + // If not, return an error with the unauthorized account's public key + if !account_info.is_signer { + msg!("Account {} is not an unexpected signer", account_info.key); + return Err(GovernanceError::InvalidAccountSigner.into()); + } + } + // If the account is marked as writable in the message, it must be writable in + // the account infos too. + if message.is_static_writable_index(i) { + if !account_info.is_writable { + return Err(GovernanceError::InvalidAccountWritable.into()); + } + } + static_accounts.push(account_info); + } + + let mut writable_accounts = Vec::new(); + let mut readonly_accounts = Vec::new(); + + // CHECK: `message_account_infos` loaded with lookup tables should come after + // `message.account_keys`, in the same order and with the same + // modifiers as listed in lookups. Track where we are in the message + // account indexes. Start after `message.account_keys`. + let mut message_indexes_cursor = message.account_keys.len(); + for lookup in message.address_table_lookups.iter() { + // This is cheap deserialization, it doesn't allocate/clone space for addresses. + let lookup_table_data = &lookup_tables + .get(&lookup.account_key) + .unwrap() + .data + .borrow()[..]; + + let lookup_table = AddressLookupTable::deserialize(lookup_table_data) + .map_err(|_| GovernanceError::InvalidLookupTableAccountKey)?; + + // CHECK: if the lookup table account is updated/extended after the vote has started + // reject the transaction and return an error + if lookup_table.meta.last_extended_slot > proposal_voting_at_slot { + return Err(GovernanceError::LookupTableAccountHasBeenAltered.into()); + } + // Accounts listed as writable in lookup, should be loaded as writable. + for (i, index_in_lookup_table) in lookup.writable_indexes.iter().enumerate() { + // Check the modifiers. + let index = message_indexes_cursor + i; + let loaded_account_info = &message_account_infos + .get(index) + .ok_or(GovernanceError::InvalidNumberOfAccounts)?; + + if !loaded_account_info.is_writable { + msg!("Loaded account should be writeable"); + return Err(GovernanceError::InvalidAccountWritable.into()); + } + + // Check that the pubkey matches the one from the actual lookup table. + let pubkey_from_lookup_table = lookup_table + .addresses + .get(usize::from(*index_in_lookup_table)) + .ok_or(GovernanceError::MissingAddressInLookuptable)?; + + if !loaded_account_info.key.eq(pubkey_from_lookup_table) { + msg!("Loaded account does not match pubkey from lookup table"); + return Err(GovernanceError::InvalidAccountFound.into()); + } + + writable_accounts.push(*loaded_account_info); + } + message_indexes_cursor += lookup.writable_indexes.len(); + + // Accounts listed as readonly in lookup. + for (i, index_in_lookup_table) in lookup.readonly_indexes.iter().enumerate() { + // Check the modifiers. + let index = message_indexes_cursor + i; + let loaded_account_info = &message_account_infos + .get(index) + .ok_or(GovernanceError::InvalidNumberOfAccounts)?; + // Check that the pubkey matches the one from the actual lookup table. + let pubkey_from_lookup_table = lookup_table + .addresses + .get(usize::from(*index_in_lookup_table)) + .ok_or(GovernanceError::MissingAddressInLookuptable)?; + + if loaded_account_info.key.eq(pubkey_from_lookup_table) { + msg!("Loaded account should not match pubkey from lookup table"); + return Err(GovernanceError::InvalidAccountFound.into()); + } + + readonly_accounts.push(*loaded_account_info); + } + message_indexes_cursor += lookup.readonly_indexes.len(); + } + + Ok(Self { + message, + static_accounts, + loaded_writable_accounts: writable_accounts, + loaded_readonly_accounts: readonly_accounts, + }) + } + + /// Executes all instructions in the message via CPI calls. + /// # Arguments + /// * `governance_signer_seeds` - Seeds for the governance signer PDA. + /// * `ephemeral_signer_seeds` - Seeds for the ephemeral signer PDAs. + /// * `governance_pubkey` - Pubkey of the governance + /// * `treasury_pubkey` - Pubkey of the treasury + /// * `protected_accounts` - Accounts that must not be passed as writable to + /// the CPI calls to prevent potential reentrancy attacks. + pub fn execute_message( + self, + governance_signer_seeds: &[&[u8]], + treasury_seeds: &[&[u8]], + governance_pubkey: &Pubkey, + treasury_pubkey: &Pubkey, + ephemeral_signer_seeds: &[Vec>], + protected_accounts: &[Pubkey], + ) -> Result<(), ProgramError> { + // First round of type conversion; from Vec>> to Vec>. + let ephemeral_signer_seeds = &ephemeral_signer_seeds + .iter() + .map(|seeds| seeds.iter().map(Vec::as_slice).collect::>()) + .collect::>>(); + + for (ix, account_infos) in self.to_instructions_and_accounts().iter() { + // A new round of type conversion; from Vec> to Vec<&[&[u8]]>. + // creates new instance of signer_seeds based on the instruction + // this is to avoid multiple signer_seeds entry below + let mut signer_seeds = ephemeral_signer_seeds + .iter() + .map(Vec::as_slice) + .collect::>(); + + for account_meta in ix.accounts.iter() { + // Check for protected accounts + // Make sure we don't pass protected accounts as writable to CPI calls. + if account_meta.is_writable && protected_accounts.contains(&account_meta.pubkey) { + return Err(GovernanceError::ProtectedAccount.into()); + } + + // Check for signer accounts and add seeds if needed + if account_meta.is_signer { + if account_meta.pubkey == *governance_pubkey { + signer_seeds.push(governance_signer_seeds); + } + if account_meta.pubkey == *treasury_pubkey { + signer_seeds.push(treasury_seeds); + } + } + } + invoke_signed(&ix, &account_infos, &signer_seeds)?; + } + Ok(()) + } + + /// Account indices are resolved in the following order: + /// 1. Static accounts. + /// 2. All loaded writable accounts. + /// 3. All loaded readonly accounts. + fn get_account_by_index(&self, index: usize) -> Result<&'a AccountInfo<'info>, ProgramError> { + if index < self.static_accounts.len() { + return Ok(self.static_accounts[index]); + } + + let index = index - self.static_accounts.len(); + if index < self.loaded_writable_accounts.len() { + return Ok(self.loaded_writable_accounts[index]); + } + + let index = index - self.loaded_writable_accounts.len(); + if index < self.loaded_readonly_accounts.len() { + return Ok(self.loaded_readonly_accounts[index]); + } + + Err(GovernanceError::InvalidTransactionMessage.into()) + } + + /// Whether the account at the `index` is requested as writable. + fn is_writable_index(&self, index: usize) -> bool { + if self.message.is_static_writable_index(index) { + return true; + } + + if index < self.static_accounts.len() { + // Index is within static accounts but is not writable. + return false; + } + + // "Skip" the static account indexes. + let index = index - self.static_accounts.len(); + + index < self.loaded_writable_accounts.len() + } + + /// Tranforms ExectuableTransactionMessage into instructions and + /// account_infos + pub fn to_instructions_and_accounts(mut self) -> Vec<(Instruction, Vec>)> { + let mut executable_instructions = vec![]; + + for gov_compiled_instruction in core::mem::take(&mut self.message.instructions) { + let ix_accounts: Vec<(AccountInfo<'info>, AccountMeta)> = gov_compiled_instruction + .account_indexes + .iter() + .map(|account_index| { + let account_index = usize::from(*account_index); + let account_info = self.get_account_by_index(account_index).unwrap(); + + // `is_signer` cannot just be taken from the account info, because for + // `authority` it's always false in the passed account + // infos, but might be true in the actual instructions. + let is_signer = self.message.is_signer_index(account_index); + + let account_meta = if self.is_writable_index(account_index) { + AccountMeta::new(*account_info.key, is_signer) + } else { + AccountMeta::new_readonly(*account_info.key, is_signer) + }; + + (account_info.clone(), account_meta) + }) + .collect(); + + let ix_program_account_info = self + .get_account_by_index(usize::from(gov_compiled_instruction.program_id_index)) + .unwrap(); + + let ix = Instruction { + program_id: *ix_program_account_info.key, + accounts: ix_accounts + .iter() + .map(|(_, account_meta)| account_meta.clone()) + .collect(), + data: gov_compiled_instruction.data, + }; + + let mut account_infos: Vec = ix_accounts + .into_iter() + .map(|(account_info, _)| account_info) + .collect(); + // Add Program ID + account_infos.push(ix_program_account_info.clone()); + + executable_instructions.push((ix, account_infos)); + } + + executable_instructions + } +} diff --git a/governance/program/src/tools/mod.rs b/governance/program/src/tools/mod.rs index c434f5bbb..c456beaa1 100644 --- a/governance/program/src/tools/mod.rs +++ b/governance/program/src/tools/mod.rs @@ -7,3 +7,9 @@ pub mod bpf_loader_upgradeable; pub mod pack; pub mod structs; + +pub mod ephermal_signers; + +pub mod executable_transaction_message; + +pub mod transaction_message; diff --git a/governance/program/src/tools/spl_token.rs b/governance/program/src/tools/spl_token.rs index a5f19ce0e..a0f81f106 100644 --- a/governance/program/src/tools/spl_token.rs +++ b/governance/program/src/tools/spl_token.rs @@ -5,7 +5,9 @@ use { arrayref::array_ref, solana_program::{ account_info::AccountInfo, + clock::Clock, entrypoint::ProgramResult, + instruction::AccountMeta, msg, program::{invoke, invoke_signed}, program_error::ProgramError, @@ -14,13 +16,26 @@ use { pubkey::Pubkey, rent::Rent, system_instruction, + sysvar::Sysvar, }, - spl_token::{ - instruction::{set_authority, AuthorityType}, - state::{Account, Mint}, + spl_token_2022::{ + extension::{ + transfer_fee::TransferFeeConfig, transfer_hook, AccountType, BaseStateWithExtensions, + ExtensionType, PodStateWithExtensions, StateWithExtensions, + }, + generic_token_account::GenericTokenAccount, + instruction::AuthorityType, + pod::PodMint, + state::{Account, Mint, Multisig}, }, + spl_transfer_hook_interface::onchain::add_extra_accounts_for_execute_cpi, }; +/// Used to determine if the spl_mint is valid +pub mod inline_spl_token { + solana_program::declare_id!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); +} + /// Creates and initializes SPL token account with PDA using the provided PDA /// seeds #[allow(clippy::too_many_arguments)] @@ -36,12 +51,28 @@ pub fn create_spl_token_account_signed<'a>( rent_sysvar_info: &AccountInfo<'a>, rent: &Rent, ) -> Result<(), ProgramError> { + let spl_token_program_id = spl_token_info.key; + + // Get the token space for if the token has extensions. + let space = if spl_token_program_id.eq(&spl_token_2022::id()) { + let mint_data = token_mint_info.data.borrow(); + + let state = PodStateWithExtensions::::unpack(&mint_data) + .map_err(|_| Into::::into(GovernanceError::InvalidGoverningTokenMint))?; + let mint_extensions = state.get_extension_types()?; + let required_extensions = + ExtensionType::get_required_init_account_extensions(&mint_extensions); + ExtensionType::try_calculate_account_len::(&required_extensions)? + } else { + spl_token_2022::state::Account::get_packed_len() + }; + let create_account_instruction = system_instruction::create_account( payer_info.key, token_account_info.key, - 1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())), - spl_token::state::Account::get_packed_len() as u64, - &spl_token::id(), + 1.max(rent.minimum_balance(space)), + space as u64, + spl_token_program_id, ); let (account_address, bump_seed) = @@ -70,8 +101,8 @@ pub fn create_spl_token_account_signed<'a>( &[&signers_seeds[..]], )?; - let initialize_account_instruction = spl_token::instruction::initialize_account( - &spl_token::id(), + let initialize_account_instruction = spl_token_2022::instruction::initialize_account( + spl_token_program_id, token_account_info.key, token_mint_info.key, token_account_owner_info.key, @@ -100,8 +131,14 @@ pub fn transfer_spl_tokens<'a>( amount: u64, spl_token_info: &AccountInfo<'a>, ) -> ProgramResult { - let transfer_instruction = spl_token::instruction::transfer( - &spl_token::id(), + let spl_token_program_id = spl_token_info.key; + + // Maintain backwards compatibility + // spl_token_2022::instruction::transfer() is a replica of spl_token::instruction::transfer() + // if spl_token program_id is used, it would cpi to spl_token program. + #[allow(deprecated)] + let transfer_instruction = spl_token_2022::instruction::transfer( + spl_token_program_id, source_info.key, destination_info.key, authority_info.key, @@ -123,6 +160,72 @@ pub fn transfer_spl_tokens<'a>( Ok(()) } +/// Transfers SPL Tokens +pub fn transfer_checked_spl_tokens<'a>( + source_info: &AccountInfo<'a>, + destination_info: &AccountInfo<'a>, + authority_info: &AccountInfo<'a>, + amount: u64, + spl_token_info: &AccountInfo<'a>, + mint_info: &AccountInfo<'a>, + additional_accounts: &[AccountInfo<'a>], +) -> ProgramResult { + let spl_token_program_id = spl_token_info.key; + + let mut transfer_instruction = spl_token_2022::instruction::transfer_checked( + spl_token_program_id, + source_info.key, + mint_info.key, + destination_info.key, + authority_info.key, + &[], + amount, + get_mint_decimals(mint_info)?, + ) + .unwrap(); + + let mut cpi_account_infos = vec![ + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + ]; + + // if it's a signer, it might be a multisig signer, throw it in! + additional_accounts + .iter() + .filter(|ai| ai.is_signer) + .for_each(|ai| { + cpi_account_infos.push(ai.clone()); + transfer_instruction + .accounts + .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); + }); + // used for transfer_hooks + // scope the borrowing to avoid a double-borrow during CPI + { + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + add_extra_accounts_for_execute_cpi( + &mut transfer_instruction, + &mut cpi_account_infos, + &program_id, + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + amount, + additional_accounts, + )?; + } + } + + invoke(&transfer_instruction, &cpi_account_infos)?; + + Ok(()) +} + /// Mint SPL Tokens pub fn mint_spl_tokens_to<'a>( mint_info: &AccountInfo<'a>, @@ -131,8 +234,10 @@ pub fn mint_spl_tokens_to<'a>( amount: u64, spl_token_info: &AccountInfo<'a>, ) -> ProgramResult { - let mint_to_ix = spl_token::instruction::mint_to( - &spl_token::id(), + let spl_token_program_id = spl_token_info.key; + + let mint_to_ix = spl_token_2022::instruction::mint_to( + spl_token_program_id, mint_info.key, destination_info.key, mint_authority_info.key, @@ -176,8 +281,11 @@ pub fn transfer_spl_tokens_signed<'a>( return Err(ProgramError::InvalidSeeds); } - let transfer_instruction = spl_token::instruction::transfer( - &spl_token::id(), + let spl_token_program_id = spl_token_info.key; + // for backwards compatibility we do not use transfer_checked() here. + #[allow(deprecated)] + let transfer_instruction = spl_token_2022::instruction::transfer( + spl_token_program_id, source_info.key, destination_info.key, authority_info.key, @@ -204,6 +312,95 @@ pub fn transfer_spl_tokens_signed<'a>( Ok(()) } +/// Transfers SPL Tokens checked from a token account owned by the provided PDA +/// authority with seeds +pub fn transfer_spl_tokens_signed_checked<'a>( + source_info: &AccountInfo<'a>, + destination_info: &AccountInfo<'a>, + authority_info: &AccountInfo<'a>, + authority_seeds: &[&[u8]], + program_id: &Pubkey, + amount: u64, + spl_token_info: &AccountInfo<'a>, + mint_info: &AccountInfo<'a>, + additional_accounts: &[AccountInfo<'a>], +) -> ProgramResult { + let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id); + + if authority_address != *authority_info.key { + msg!( + "Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected", + authority_info.key, + authority_address + ); + return Err(ProgramError::InvalidSeeds); + } + + let spl_token_program_id = spl_token_info.key; + + let mut transfer_instruction = spl_token_2022::instruction::transfer_checked( + spl_token_program_id, + source_info.key, + mint_info.key, + destination_info.key, + authority_info.key, + &[], + amount, + get_mint_decimals(mint_info)?, + ) + .unwrap(); + + let mut signers_seeds = authority_seeds.to_vec(); + let bump = &[bump_seed]; + signers_seeds.push(bump); + + let mut cpi_account_infos = vec![ + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + ]; + + // if it's a signer, it might be a multisig signer, throw it in! + additional_accounts + .iter() + .filter(|ai| ai.is_signer) + .for_each(|ai| { + cpi_account_infos.push(ai.clone()); + transfer_instruction + .accounts + .push(AccountMeta::new_readonly(*ai.key, ai.is_signer)); + }); + + // used for transfer_hooks + // scope the borrowing to avoid a double-borrow during CPI + { + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + if let Some(program_id) = transfer_hook::get_program_id(&mint) { + add_extra_accounts_for_execute_cpi( + &mut transfer_instruction, + &mut cpi_account_infos, + &program_id, + source_info.clone(), + mint_info.clone(), + destination_info.clone(), + authority_info.clone(), + amount, + additional_accounts, + )?; + } + } + + invoke_signed( + &transfer_instruction, + &cpi_account_infos, + &[&signers_seeds[..]], + )?; + + Ok(()) +} + /// Burns SPL Tokens from a token account owned by the provided PDA authority /// with seeds pub fn burn_spl_tokens_signed<'a>( @@ -226,8 +423,9 @@ pub fn burn_spl_tokens_signed<'a>( return Err(ProgramError::InvalidSeeds); } - let burn_ix = spl_token::instruction::burn( - &spl_token::id(), + let spl_token_program_id = spl_token_info.key; + let burn_ix = spl_token_2022::instruction::burn( + spl_token_program_id, token_account_info.key, token_mint_info.key, authority_info.key, @@ -261,28 +459,17 @@ pub fn assert_is_valid_spl_token_account(account_info: &AccountInfo) -> Result<( return Err(GovernanceError::SplTokenAccountDoesNotExist.into()); } - if account_info.owner != &spl_token::id() { + // inline_spl_token is used to avoid including the whole package. + if account_info.owner != &spl_token_2022::id() && account_info.owner != &inline_spl_token::ID { return Err(GovernanceError::SplTokenAccountWithInvalidOwner.into()); } - if account_info.data_len() != Account::LEN { + // Check if the account data is a valid token account + // also checks if the account is initialized or not. + if !Account::valid_account_data(&account_info.try_borrow_data()?) { return Err(GovernanceError::SplTokenInvalidTokenAccountData.into()); } - // TokenAccount layout: - // mint(32) - // owner(32) - // amount(8) - // delegate(36) - // state(1) - // ... - let data = account_info.try_borrow_data()?; - let state = array_ref![data, 108, 1]; - - if state == &[0] { - return Err(GovernanceError::SplTokenAccountNotInitialized.into()); - } - Ok(()) } @@ -298,11 +485,13 @@ pub fn assert_is_valid_spl_token_mint(mint_info: &AccountInfo) -> Result<(), Pro return Err(GovernanceError::SplTokenMintDoesNotExist.into()); } - if mint_info.owner != &spl_token::id() { + // inline_spl_token is used to avoid including the whole package. + if mint_info.owner != &spl_token_2022::id() && mint_info.owner != &inline_spl_token::ID { return Err(GovernanceError::SplTokenMintWithInvalidOwner.into()); } - if mint_info.data_len() != Mint::LEN { + // assert that length is mint + if !valid_mint_length(&mint_info.try_borrow_data()?) { return Err(GovernanceError::SplTokenInvalidMintAccountData.into()); } @@ -419,8 +608,9 @@ pub fn set_spl_token_account_authority<'a>( authority_type: AuthorityType, spl_token_info: &AccountInfo<'a>, ) -> Result<(), ProgramError> { - let set_authority_ix = set_authority( - &spl_token::id(), + let spl_token_program_id = spl_token_info.key; + let set_authority_ix = spl_token_2022::instruction::set_authority( + spl_token_program_id, account_info.key, Some(new_account_authority), authority_type, @@ -439,3 +629,40 @@ pub fn set_spl_token_account_authority<'a>( Ok(()) } + +/// Computationally cheap method to just get supply off a mint without unpacking whole object +pub fn get_mint_decimals(account_info: &AccountInfo) -> Result { + // In token program, 36, 8, 1, 1, is the layout, where the first 1 is decimals u8. + // so we start at 36. + let data = account_info.try_borrow_data()?; + + // If we don't check this and an empty account is passed in, we get a panic when + // we try to index into the data. + if data.is_empty() { + return Err(ProgramError::InvalidAccountData); + } + + Ok(data[44]) +} + +const ACCOUNTTYPE_MINT: u8 = AccountType::Mint as u8; +fn valid_mint_length(mint_data: &[u8]) -> bool { + mint_data.len() == Mint::LEN + || (mint_data.len() > Mint::LEN + && mint_data.len() != Multisig::LEN + && ACCOUNTTYPE_MINT == mint_data[Mint::LEN]) +} + +/// Get current TransferFee, returns 0 if no TransferFeeConfig exist. +pub fn get_current_mint_fee(mint_info: &AccountInfo, amount: u64) -> Result { + let mint_data = mint_info.try_borrow_data()?; + let mint = PodStateWithExtensions::::unpack(&mint_data)?; + + if let Ok(transfer_fee_config) = mint.get_extension::() { + Ok(transfer_fee_config + .calculate_epoch_fee(Clock::get()?.epoch, amount) + .ok_or(GovernanceError::MathematicalOverflow)?) + } else { + Ok(0) + } +} diff --git a/governance/program/src/tools/transaction_message.rs b/governance/program/src/tools/transaction_message.rs new file mode 100644 index 000000000..013cb6591 --- /dev/null +++ b/governance/program/src/tools/transaction_message.rs @@ -0,0 +1,47 @@ +//! General purpose TransactionMessage utility functions + +use { + borsh::{BorshDeserialize, BorshSerialize}, + solana_program::pubkey::Pubkey, +}; + +/// Unvalidated instruction data, must be treated as untrusted. +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct TransactionMessage { + /// The number of signer pubkeys in the account_keys vec. + pub num_signers: u8, + /// The number of writable signer pubkeys in the account_keys vec. + pub num_writable_signers: u8, + /// The number of writable non-signer pubkeys in the account_keys vec. + pub num_writable_non_signers: u8, + /// The list of unique account public keys (including program IDs) that will be used in the provided instructions. + pub account_keys: Vec, + /// The list of instructions to execute. + pub instructions: Vec, + /// List of address table lookups used to load additional accounts + /// for this transaction. + pub address_table_lookups: Vec, +} + +/// Concise serialization schema for instructions that make up transaction. +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct CompiledInstruction { + /// Indices of the program_id in tx's account_keys + pub program_id_index: u8, + /// Indices into the tx's `account_keys` list indicating which accounts to pass to the instruction. + pub account_indexes: Vec, + /// Instruction data. + pub data: Vec, +} + +/// Address table lookups describe an on-chain address lookup table to use +/// for loading more readonly and writable accounts in a single tx. +#[derive(Clone, Debug, BorshDeserialize, BorshSerialize)] +pub struct MessageAddressTableLookup { + /// Address lookup table account key + pub account_key: Pubkey, + /// List of indexes used to load writable account addresses + pub writable_indexes: Vec, + /// List of indexes used to load readonly account addresses + pub readonly_indexes: Vec, +} diff --git a/governance/program/tests/fixtures/mpl_core.so b/governance/program/tests/fixtures/mpl_core.so new file mode 100644 index 000000000..71050ecc6 Binary files /dev/null and b/governance/program/tests/fixtures/mpl_core.so differ diff --git a/governance/program/tests/process_add_signatory.rs b/governance/program/tests/process_add_signatory.rs index 7c9b15507..cc8067961 100644 --- a/governance/program/tests/process_add_signatory.rs +++ b/governance/program/tests/process_add_signatory.rs @@ -3,7 +3,6 @@ mod program_test; use { - borsh::BorshSerialize, program_test::*, solana_program::program_error::ProgramError, solana_program_test::tokio, @@ -453,10 +452,9 @@ pub async fn test_add_non_matching_required_signatory_to_proposal_err() { &signatory.pubkey(), ); - create_signatory_record_ix.data = GovernanceInstruction::AddSignatory { + create_signatory_record_ix.data = borsh::to_vec(&GovernanceInstruction::AddSignatory { signatory: Pubkey::new_unique(), - } - .try_to_vec() + }) .unwrap(); // Act diff --git a/governance/program/tests/process_complete_proposal.rs b/governance/program/tests/process_complete_proposal.rs index 99700613b..37e2473ad 100644 --- a/governance/program/tests/process_complete_proposal.rs +++ b/governance/program/tests/process_complete_proposal.rs @@ -69,24 +69,24 @@ async fn test_complete_proposal_with_wrong_state_error() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut token_governance_cookie = governance_test - .with_token_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_token_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); let proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut token_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); @@ -112,31 +112,32 @@ async fn test_complete_proposal_with_completed_state_transaction_exists_error() let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut token_governance_cookie = governance_test - .with_token_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_token_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_token_cookie = governance_test.with_governed_token().await; let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut token_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &token_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await diff --git a/governance/program/tests/process_create_governance.rs b/governance/program/tests/process_create_governance.rs index 220de723d..271c704b3 100644 --- a/governance/program/tests/process_create_governance.rs +++ b/governance/program/tests/process_create_governance.rs @@ -41,6 +41,70 @@ async fn test_create_governance() { assert_eq!(governance_cookie.account, governance_account); } +#[tokio::test] +async fn test_create_governance_token_2022() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Act + let governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Assert + let governance_account = governance_test + .get_governance_account(&governance_cookie.address) + .await; + + assert_eq!(governance_cookie.account, governance_account); +} + +#[tokio::test] +async fn test_create_governance_token_2022_with_transfer_fees() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let realm_cookie = governance_test + .with_realm_token_2022_with_transfer_fees() + .await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit_with_transfer_fees(&realm_cookie) + .await + .unwrap(); + + // Act + let governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + // Assert + let governance_account = governance_test + .get_governance_account(&governance_cookie.address) + .await; + + assert_eq!(governance_cookie.account, governance_account); +} + #[tokio::test] async fn test_create_governance_with_invalid_realm_error() { // Arrange diff --git a/governance/program/tests/process_create_mint_governance.rs b/governance/program/tests/process_create_mint_governance.rs deleted file mode 100644 index 13852aa77..000000000 --- a/governance/program/tests/process_create_mint_governance.rs +++ /dev/null @@ -1,274 +0,0 @@ -#![cfg(feature = "test-sbf")] -mod program_test; - -use { - program_test::*, - solana_program_test::*, - solana_sdk::{signature::Keypair, signer::Signer}, - spl_governance::error::GovernanceError, - spl_governance_tools::error::GovernanceToolsError, - spl_token::error::TokenError, -}; - -#[tokio::test] -async fn test_create_mint_governance() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - // Act - let mint_governance_cookie = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // // Assert - let mint_governance_account = governance_test - .get_governance_account(&mint_governance_cookie.address) - .await; - - assert_eq!(mint_governance_cookie.account, mint_governance_account); - - let mint_account = governance_test - .get_mint_account(&governed_mint_cookie.address) - .await; - - assert_eq!( - mint_governance_cookie.address, - mint_account.mint_authority.unwrap() - ); -} - -#[tokio::test] -async fn test_create_mint_governance_without_transferring_mint_authority() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_mint_cookie.transfer_mint_authority = false; - // Act - let mint_governance_cookie = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // // Assert - let mint_governance_account = governance_test - .get_governance_account(&mint_governance_cookie.address) - .await; - - assert_eq!(mint_governance_cookie.account, mint_governance_account); - - let mint_account = governance_test - .get_mint_account(&governed_mint_cookie.address) - .await; - - assert_eq!( - governed_mint_cookie.mint_authority.pubkey(), - mint_account.mint_authority.unwrap() - ); -} - -#[tokio::test] -async fn test_create_mint_governance_without_transferring_mint_authority_with_invalid_authority_error( -) { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_mint_cookie.transfer_mint_authority = false; - governed_mint_cookie.mint_authority = Keypair::new(); - - // Act - let err = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::InvalidMintAuthority.into()); -} - -#[tokio::test] -async fn test_create_mint_governance_without_transferring_mint_authority_with_authority_not_signed_error( -) { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_mint_cookie.transfer_mint_authority = false; - - // Act - let err = governance_test - .with_mint_governance_using_instruction( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - |i| { - i.accounts[3].is_signer = false; // governed_mint_authority - }, - Some(&[&token_owner_record_cookie.token_owner]), - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::MintAuthorityMustSign.into()); -} - -#[tokio::test] -async fn test_create_mint_governance_with_invalid_mint_authority_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_mint_cookie.mint_authority = Keypair::new(); - - // Act - let err = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, TokenError::OwnerMismatch.into()); -} - -#[tokio::test] -async fn test_create_mint_governance_with_invalid_realm_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let mut realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - let mint_governance_cookie = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // try to use Governance account other than Realm as realm - realm_cookie.address = mint_governance_cookie.address; - - // Act - let err = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceToolsError::InvalidAccountType.into()); -} - -#[tokio::test] -async fn test_create_mint_governance_with_freeze_authority_transfer() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_freezable_governed_mint().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - // Act - let mint_governance_cookie = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // // Assert - let mint_governance_account = governance_test - .get_governance_account(&mint_governance_cookie.address) - .await; - - assert_eq!(mint_governance_cookie.account, mint_governance_account); - - let mint_account = governance_test - .get_mint_account(&governed_mint_cookie.address) - .await; - - assert_eq!( - mint_governance_cookie.address, - mint_account.mint_authority.unwrap() - ); - - assert_eq!( - mint_governance_cookie.address, - mint_account.freeze_authority.unwrap() - ); -} diff --git a/governance/program/tests/process_create_program_governance.rs b/governance/program/tests/process_create_program_governance.rs deleted file mode 100644 index 2bd19172c..000000000 --- a/governance/program/tests/process_create_program_governance.rs +++ /dev/null @@ -1,238 +0,0 @@ -#![cfg(feature = "test-sbf")] -mod program_test; - -use { - program_test::*, - solana_program_test::*, - solana_sdk::signature::{Keypair, Signer}, - spl_governance::{ - error::GovernanceError, tools::bpf_loader_upgradeable::get_program_upgrade_authority, - }, - spl_governance_test_sdk::tools::ProgramInstructionError, - spl_governance_tools::error::GovernanceToolsError, -}; - -#[tokio::test] -async fn test_create_program_governance() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - // Act - let program_governance_cookie = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let program_governance_account = governance_test - .get_governance_account(&program_governance_cookie.address) - .await; - - assert_eq!( - program_governance_cookie.account, - program_governance_account - ); - - let program_data = governance_test - .get_upgradable_loader_account(&governed_program_cookie.data_address) - .await; - - let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap(); - - assert_eq!(Some(program_governance_cookie.address), upgrade_authority); -} - -#[tokio::test] -async fn test_create_program_governance_without_transferring_upgrade_authority() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_program_cookie.transfer_upgrade_authority = false; - - // Act - let program_governance_cookie = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let program_governance_account = governance_test - .get_governance_account(&program_governance_cookie.address) - .await; - - assert_eq!( - program_governance_cookie.account, - program_governance_account - ); - - let program_data = governance_test - .get_upgradable_loader_account(&governed_program_cookie.data_address) - .await; - - let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap(); - - assert_eq!( - Some(governed_program_cookie.upgrade_authority.pubkey()), - upgrade_authority - ); -} - -#[tokio::test] -async fn test_create_program_governance_without_transferring_upgrade_authority_with_invalid_authority_error( -) { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_program_cookie.transfer_upgrade_authority = false; - governed_program_cookie.upgrade_authority = Keypair::new(); - - // Act - let err = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::InvalidUpgradeAuthority.into()); -} - -#[tokio::test] -async fn test_create_program_governance_without_transferring_upgrade_authority_with_authority_not_signed_error( -) { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_program_cookie.transfer_upgrade_authority = false; - - // Act - let err = governance_test - .with_program_governance_using_instruction( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - |i| { - i.accounts[4].is_signer = false; // governed_program_upgrade_authority - }, - Some(&[&token_owner_record_cookie.token_owner]), - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::UpgradeAuthorityMustSign.into()); -} - -#[tokio::test] -async fn test_create_program_governance_with_incorrect_upgrade_authority_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_program_cookie.upgrade_authority = Keypair::new(); - - // Act - let err = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, ProgramInstructionError::IncorrectAuthority.into()); -} - -#[tokio::test] -async fn test_create_program_governance_with_invalid_realm_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let mut realm_cookie = governance_test.with_realm().await; - let governed_program_cookie = governance_test.with_governed_program().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - let program_governance_cookie = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - realm_cookie.address = program_governance_cookie.address; - - // Act - let err = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceToolsError::InvalidAccountType.into()); -} diff --git a/governance/program/tests/process_create_realm.rs b/governance/program/tests/process_create_realm.rs index d380bcbcb..24b58a528 100644 --- a/governance/program/tests/process_create_realm.rs +++ b/governance/program/tests/process_create_realm.rs @@ -106,3 +106,124 @@ async fn test_create_realm_for_existing_pda() { assert_eq!(realm_cookie.account, realm_account); } + +#[tokio::test] +async fn test_create_realm_with_token_2022() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + // Act + let realm_cookie = governance_test.with_realm_token_2022().await; + + // Assert + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(realm_cookie.account, realm_account); +} + +#[tokio::test] +async fn test_create_realm_token_2022_with_non_default_config() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_setup_args = RealmSetupArgs { + use_council_mint: false, + community_mint_max_voter_weight_source: MintMaxVoterWeightSource::SupplyFraction(1), + min_community_weight_to_create_governance: 1, + ..Default::default() + }; + + // Act + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_setup_args) + .await; + + // Assert + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(realm_cookie.account, realm_account); +} + +#[tokio::test] +async fn test_create_realm_token_2022_with_max_voter_weight_absolute_value() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_setup_args = RealmSetupArgs { + community_mint_max_voter_weight_source: MintMaxVoterWeightSource::Absolute(1), + ..Default::default() + }; + + // Act + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_setup_args) + .await; + + // Assert + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(realm_cookie.account, realm_account); + assert_eq!( + realm_cookie + .account + .config + .community_mint_max_voter_weight_source, + MintMaxVoterWeightSource::Absolute(1) + ); +} + +#[tokio::test] +async fn test_create_realm_token_2022_for_existing_pda() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_name = format!("Realm #{}", governance_test.next_realm_id).to_string(); + let realm_address = get_realm_address(&governance_test.program_id, &realm_name); + + let rent_exempt = governance_test.bench.rent.minimum_balance(0); + + governance_test + .bench + .transfer_sol(&realm_address, rent_exempt) + .await; + + // Act + let realm_cookie = governance_test.with_realm_token_2022().await; + + // Assert + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(realm_cookie.account, realm_account); +} + +#[tokio::test] +async fn test_create_realm_with_token_2022_with_transfer_fees() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_setup_args = RealmSetupArgs { + use_council_mint: true, + community_mint_max_voter_weight_source: MintMaxVoterWeightSource::SupplyFraction(1), + min_community_weight_to_create_governance: 1, + ..Default::default() + }; + // Act + let realm_cookie = governance_test + .with_realm_using_args_token_2022_with_transfer_fees(&realm_setup_args) + .await; + + // Assert + let realm_account = governance_test + .get_realm_account(&realm_cookie.address) + .await; + + assert_eq!(realm_cookie.account, realm_account); +} diff --git a/governance/program/tests/process_create_token_governance.rs b/governance/program/tests/process_create_token_governance.rs deleted file mode 100644 index 6d880ec5a..000000000 --- a/governance/program/tests/process_create_token_governance.rs +++ /dev/null @@ -1,278 +0,0 @@ -#![cfg(feature = "test-sbf")] -mod program_test; - -use { - program_test::*, - solana_program_test::*, - solana_sdk::{signature::Keypair, signer::Signer}, - spl_governance::error::GovernanceError, - spl_governance_tools::error::GovernanceToolsError, - spl_token::{error::TokenError, instruction::AuthorityType}, -}; - -#[tokio::test] -async fn test_create_token_governance() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - // Act - let token_governance_cookie = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let token_governance_account = governance_test - .get_governance_account(&token_governance_cookie.address) - .await; - - assert_eq!(token_governance_cookie.account, token_governance_account); - - let token_account = governance_test - .get_token_account(&governed_token_cookie.address) - .await; - - assert_eq!(token_governance_cookie.address, token_account.owner); -} - -#[tokio::test] -async fn test_create_token_governance_without_transferring_token_owner() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_token_cookie.transfer_token_owner = false; - - // Act - let token_governance_cookie = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let token_governance_account = governance_test - .get_governance_account(&token_governance_cookie.address) - .await; - - assert_eq!(token_governance_cookie.account, token_governance_account); - - let token_account = governance_test - .get_token_account(&governed_token_cookie.address) - .await; - - assert_eq!( - governed_token_cookie.token_owner.pubkey(), - token_account.owner - ); -} - -#[tokio::test] -async fn test_create_token_governance_without_transferring_token_owner_with_invalid_token_owner_error( -) { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_token_cookie.transfer_token_owner = false; - governed_token_cookie.token_owner = Keypair::new(); - - // Act - let err = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::InvalidTokenOwner.into()); -} - -#[tokio::test] -async fn test_create_token_governance_without_transferring_token_owner_with_owner_not_signed_error() -{ - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_token_cookie.transfer_token_owner = false; - - // Act - let err = governance_test - .with_token_governance_using_instruction( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - |i| { - i.accounts[3].is_signer = false; // governed_token_owner - }, - Some(&[&token_owner_record_cookie.token_owner]), - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceError::TokenOwnerMustSign.into()); -} - -#[tokio::test] -async fn test_create_token_governance_with_invalid_token_owner_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let mut governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - governed_token_cookie.token_owner = Keypair::new(); - - // Act - let err = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, TokenError::OwnerMismatch.into()); -} - -#[tokio::test] -async fn test_create_token_governance_with_invalid_realm_error() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let mut realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - let token_governance_cookie = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // try to use Governance account other than Realm as realm - realm_cookie.address = token_governance_cookie.address; - - // Act - let err = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .err() - .unwrap(); - - // Assert - assert_eq!(err, GovernanceToolsError::InvalidAccountType.into()); -} - -#[tokio::test] -async fn test_create_token_governance_with_close_authority_transfer() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_new().await; - - let realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; - - governance_test - .bench - .set_spl_token_account_authority( - &governed_token_cookie.address, - &governed_token_cookie.token_owner, - Some(&governed_token_cookie.token_owner.pubkey()), - AuthorityType::CloseAccount, - ) - .await; - - let token_owner_record_cookie = governance_test - .with_community_token_deposit(&realm_cookie) - .await - .unwrap(); - - // Act - let token_governance_cookie = governance_test - .with_token_governance( - &realm_cookie, - &governed_token_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let token_governance_account = governance_test - .get_governance_account(&token_governance_cookie.address) - .await; - - assert_eq!(token_governance_cookie.account, token_governance_account); - - let token_account = governance_test - .get_token_account(&governed_token_cookie.address) - .await; - - assert_eq!(token_governance_cookie.address, token_account.owner); - assert_eq!( - token_governance_cookie.address, - token_account.close_authority.unwrap() - ); -} diff --git a/governance/program/tests/process_deposit_governing_tokens.rs b/governance/program/tests/process_deposit_governing_tokens.rs index bd921685a..b00511973 100644 --- a/governance/program/tests/process_deposit_governing_tokens.rs +++ b/governance/program/tests/process_deposit_governing_tokens.rs @@ -7,6 +7,7 @@ mod program_test; use { crate::program_test::args::*, program_test::*, + solana_sdk::pubkey::Pubkey, solana_sdk::signature::{Keypair, Signer}, spl_governance::{ error::GovernanceError, @@ -153,6 +154,299 @@ async fn test_deposit_subsequent_community_tokens() { assert_eq!(total_deposit_amount, holding_account.amount); } +#[tokio::test] +async fn test_deposit_initial_community_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + // Act + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record_cookie.account, token_owner_record); + + assert_eq!( + TOKEN_OWNER_RECORD_LAYOUT_VERSION, + token_owner_record.version + ); + assert_eq!(0, token_owner_record.unrelinquished_votes_count); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount, + source_account.amount + ); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); +} + +#[tokio::test] +async fn test_deposit_initial_council_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap(); + + // Act + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record_cookie.account, token_owner_record); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount, + source_account.amount + ); + + let holding_account = governance_test + .get_token_account(&council_token_holding_account) + .await; + + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); +} + +#[tokio::test] +async fn test_deposit_subsequent_community_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let deposit_amount = 5; + let total_deposit_amount = token_owner_record_cookie + .account + .governing_token_deposit_amount + + deposit_amount; + + governance_test.advance_clock().await; + + // Act + governance_test + .with_subsequent_community_token_2022_deposit( + &realm_cookie, + &token_owner_record_cookie, + deposit_amount, + ) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!( + total_deposit_amount, + token_owner_record.governing_token_deposit_amount + ); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!(total_deposit_amount, holding_account.amount); +} + +#[tokio::test] +async fn test_deposit_subsequent_council_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap(); + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + let deposit_amount = 5; + let total_deposit_amount = token_owner_record_cookie + .account + .governing_token_deposit_amount + + deposit_amount; + + governance_test.advance_clock().await; + + // Act + governance_test + .with_subsequent_council_token_deposit_2022( + &realm_cookie, + &token_owner_record_cookie, + deposit_amount, + ) + .await; + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!( + total_deposit_amount, + token_owner_record.governing_token_deposit_amount + ); + + let holding_account = governance_test + .get_token_account(&council_token_holding_account) + .await; + + assert_eq!(total_deposit_amount, holding_account.amount); +} + +#[tokio::test] +async fn test_deposit_initial_community_2022_tokens_with_transfer_fees() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test + .with_realm_token_2022_with_transfer_fees() + .await; + + // Act + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit_with_transfer_fees(&realm_cookie) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record_cookie.account, token_owner_record); + + assert_eq!( + TOKEN_OWNER_RECORD_LAYOUT_VERSION, + token_owner_record.version + ); + assert_eq!(0, token_owner_record.unrelinquished_votes_count); + + // expected transfer_fee + let transfer_fee = 3; + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount + - transfer_fee, + source_account.amount + ); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); +} + +#[tokio::test] +async fn test_deposit_initial_community_2022_tokens_with_transfer_hook() { + let transfer_hook_program_id = Pubkey::new_unique(); + // Arrange + let mut governance_test = + GovernanceProgramTest::start_with_transfer_hook(Some(&transfer_hook_program_id)).await; + let realm_cookie = governance_test + .with_realm_token_2022_with_transfer_hook(&transfer_hook_program_id) + .await; + let writable_pubkey = Pubkey::new_unique(); + + // Act + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit_with_transfer_hook( + &realm_cookie, + &transfer_hook_program_id, + &writable_pubkey, + ) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record_cookie.account, token_owner_record); + + assert_eq!( + TOKEN_OWNER_RECORD_LAYOUT_VERSION, + token_owner_record.version + ); + assert_eq!(0, token_owner_record.unrelinquished_votes_count); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount + - token_owner_record_cookie + .account + .governing_token_deposit_amount, + source_account.amount + ); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); +} + #[tokio::test] async fn test_deposit_subsequent_council_tokens() { // Arrange @@ -233,6 +527,7 @@ async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() { &governance_test.bench.context.payer.pubkey(), amount, &realm_cookie.account.community_mint, + false, // is_token_2022 ); deposit_ix.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false); @@ -282,6 +577,7 @@ async fn test_deposit_community_tokens_with_malicious_holding_account_error() { &governance_test.bench.context.payer.pubkey(), amount, &realm_cookie.account.community_mint, + false, // is_token_2022 ); // Try to maliciously deposit to the source @@ -364,3 +660,170 @@ async fn test_deposit_comunity_tokens_with_cannot_deposit_dormant_tokens_error() // Assert assert_eq!(err, GovernanceError::CannotDepositDormantTokens.into()); } + +#[tokio::test] +async fn test_deposit_initial_community_2022_tokens_with_owner_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner = Keypair::new(); + let transfer_authority = Keypair::new(); + let token_source = Keypair::new(); + + let amount = 10; + + governance_test + .bench + .create_token_2022_account_with_transfer_authority( + &token_source, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + amount, + &token_owner, + &transfer_authority.pubkey(), + ) + .await; + + let mut deposit_ix = deposit_governing_tokens( + &governance_test.program_id, + &realm_cookie.address, + &token_source.pubkey(), + &token_owner.pubkey(), + &transfer_authority.pubkey(), + &governance_test.bench.context.payer.pubkey(), + amount, + &realm_cookie.account.community_mint, + true, // is_token_2022 + ); + + deposit_ix.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false); + + // Act + + let error = governance_test + .bench + .process_transaction(&[deposit_ix], Some(&[&transfer_authority])) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into()); +} + +#[tokio::test] +async fn test_deposit_community_2022_tokens_with_malicious_holding_account_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let amount = 50; + + governance_test + .bench + .mint_2022_tokens( + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + &token_owner_record_cookie.token_source, + amount, + ) + .await; + + let mut deposit_ix = deposit_governing_tokens( + &governance_test.program_id, + &realm_cookie.address, + &token_owner_record_cookie.token_source, + &token_owner_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + &governance_test.bench.context.payer.pubkey(), + amount, + &realm_cookie.account.community_mint, + true, // is_token_2022 + ); + + // Try to maliciously deposit to the source + deposit_ix.accounts[1].pubkey = token_owner_record_cookie.token_source; + + // Act + + let err = governance_test + .bench + .process_transaction( + &[deposit_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::InvalidGoverningTokenHoldingAccount.into() + ); +} + +#[tokio::test] +async fn test_deposit_community_2022_tokens_using_mint() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + // Act + let token_owner_record_cookie = governance_test + .with_initial_governing_token_deposit_using_mint_2022( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + 10, + None, + ) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record_cookie.account, token_owner_record); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!( + token_owner_record.governing_token_deposit_amount, + holding_account.amount + ); +} + +#[tokio::test] +async fn test_deposit_comunity_2022_tokens_with_cannot_deposit_dormant_tokens_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Dormant; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + // Act + let err = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::CannotDepositDormantTokens.into()); +} diff --git a/governance/program/tests/process_ephermal_signers.rs b/governance/program/tests/process_ephermal_signers.rs new file mode 100644 index 000000000..872d61470 --- /dev/null +++ b/governance/program/tests/process_ephermal_signers.rs @@ -0,0 +1,381 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; + +use { + program_test::*, + solana_program_test::tokio, + solana_sdk::{signature::Keypair, signer::Signer}, + spl_governance::{ + state::{ + enums::{ProposalState, TransactionExecutionStatus}, + native_treasury::get_native_treasury_address, + proposal_versioned_transaction::get_proposal_versioned_transaction_address, + }, + tools::transaction_message::TransactionMessage, + }, + spl_governance_test_sdk::{ + mpl_core_tools::{ + assert_asset, assert_collection, create_asset, create_collection, + AssertAssetHelperArgs, AssertCollectionHelperArgs, CreateAssetHelperArgs, + CreateCollectionHelperArgs, UpdateAuthority, + }, + versioned_transaction::get_ephemeral_signer_pda, + }, + versioned_transaction_ext::VaultTransactionMessageExt, +}; + +#[tokio::test] +async fn test_create_asset_mpl_core_via_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let governed_mint_cookie = governance_test.with_governed_mint().await; + + governance_test + .with_native_treasury(&governance_cookie) + .await; + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + let proposal_versioned_tx_address = get_proposal_versioned_transaction_address( + &governance_test.program_id, + &proposal_cookie.address, + &0_u8.to_le_bytes(), + &0_u16.to_le_bytes(), + ); + let (asset_pubkey, _bump) = get_ephemeral_signer_pda( + &proposal_versioned_tx_address, + 0, + &governance_test.program_id, + 0, + ); + let instruction = create_asset( + CreateAssetHelperArgs { + owner: None, + payer: None, + asset: &asset_pubkey, + data_state: None, + name: None, + uri: None, + authority: None, + update_authority: None, + collection: None, + plugins: vec![], + external_plugin_adapters: vec![], + }, + treasury_address, + ); + + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &[instruction], &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 1, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 1, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + assert_asset( + &mut governance_test.bench.context, + AssertAssetHelperArgs { + asset: asset_pubkey, + owner: treasury_address, + update_authority: Some(UpdateAuthority::Address(treasury_address)), + name: None, + uri: None, + plugins: vec![], + external_plugin_adapters: vec![], + }, + ) + .await; +} + +#[tokio::test] +async fn test_create_collection_mpl_core_via_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let governed_mint_cookie = governance_test.with_governed_mint().await; + + governance_test + .with_native_treasury(&governance_cookie) + .await; + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + let proposal_versioned_tx_address = get_proposal_versioned_transaction_address( + &governance_test.program_id, + &proposal_cookie.address, + &0_u8.to_le_bytes(), + &0_u16.to_le_bytes(), + ); + let (collection_pubkey, _bump) = get_ephemeral_signer_pda( + &proposal_versioned_tx_address, + 0, + &governance_test.program_id, + 0, + ); + let instruction = create_collection( + CreateCollectionHelperArgs { + collection: &collection_pubkey, + update_authority: None, + payer: None, + name: None, + uri: None, + plugins: vec![], + external_plugin_adapters: vec![], + }, + treasury_address, + ); + + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &[instruction], &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 1, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 1, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + assert_collection( + &mut governance_test.bench.context, + AssertCollectionHelperArgs { + collection: collection_pubkey, + update_authority: treasury_address, + name: None, + uri: None, + num_minted: 0, + current_size: 0, + plugins: vec![], + external_plugin_adapters: vec![], + }, + ) + .await; +} diff --git a/governance/program/tests/process_execute_transaction.rs b/governance/program/tests/process_execute_transaction.rs index 6cb2bdfa6..3da5da0b8 100644 --- a/governance/program/tests/process_execute_transaction.rs +++ b/governance/program/tests/process_execute_transaction.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "test-sbf")] +// #![cfg(feature = "test-sbf")] mod program_test; @@ -22,31 +22,36 @@ async fn test_execute_mint_transaction() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -127,31 +132,35 @@ async fn test_execute_transfer_transaction() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_token_cookie = governance_test.with_governed_token().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut token_governance_cookie = governance_test - .with_token_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_token_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_token_cookie = governance_test + .with_governed_token_governed_authority(&governance_cookie) + .await; + let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut token_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &token_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -234,7 +243,7 @@ async fn test_execute_upgrade_program_transaction() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_program_cookie = governance_test.with_governed_program().await; + let governed_program_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) @@ -242,7 +251,7 @@ async fn test_execute_upgrade_program_transaction() { .unwrap(); let mut program_governance_cookie = governance_test - .with_program_governance( + .with_governance( &realm_cookie, &governed_program_cookie, &token_owner_record_cookie, @@ -364,31 +373,34 @@ async fn test_execute_proposal_transaction_with_invalid_state_errors() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie1 = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -397,7 +409,7 @@ async fn test_execute_proposal_transaction_with_invalid_state_errors() { let signatory_record_cookie2 = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -537,31 +549,34 @@ async fn test_execute_proposal_transaction_for_other_proposal_error() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -601,7 +616,7 @@ async fn test_execute_proposal_transaction_for_other_proposal_error() { .unwrap(); let proposal_cookie2 = governance_test - .with_proposal(&token_owner_record_cookie2, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie2, &mut governance_cookie) .await .unwrap(); @@ -627,31 +642,34 @@ async fn test_execute_mint_transaction_twice_error() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -715,7 +733,7 @@ async fn test_execute_transaction_with_create_proposal_and_execute_in_single_slo let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) @@ -725,25 +743,28 @@ async fn test_execute_transaction_with_create_proposal_and_execute_in_single_slo let mut governance_config = governance_test.get_default_governance_config(); governance_config.min_transaction_hold_up_time = 0; - let mut mint_governance_cookie = governance_test - .with_mint_governance_using_config( + let mut governance_cookie = governance_test + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, &governance_config, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -784,3 +805,111 @@ async fn test_execute_transaction_with_create_proposal_and_execute_in_single_slo GovernanceError::CannotExecuteTransactionWithinHoldUpTime.into() ); } + +#[tokio::test] +async fn test_execute_mint_transaction_with_community_token_2022() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority_token_2022(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_transaction_cookie = governance_test + .with_mint_tokens_token_2022_transaction( + &governed_mint_cookie, + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + None, + None, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan(proposal_transaction_cookie.account.hold_up_time as u64) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .execute_proposal_transaction(&proposal_cookie, &proposal_transaction_cookie) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + let instruction_token_account = governance_test + .get_token_account(&proposal_transaction_cookie.account.instructions[0].accounts[1].pubkey) + .await; + + assert_eq!(10, instruction_token_account.amount); +} diff --git a/governance/program/tests/process_execute_versioned_transaction.rs b/governance/program/tests/process_execute_versioned_transaction.rs new file mode 100644 index 000000000..899363408 --- /dev/null +++ b/governance/program/tests/process_execute_versioned_transaction.rs @@ -0,0 +1,604 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; + +use { + program_test::*, + solana_program_test::tokio, + solana_sdk::{signature::Keypair, signer::Signer}, + spl_governance::{ + error::GovernanceError, + state::{ + enums::{ProposalState, TransactionExecutionStatus}, + native_treasury::get_native_treasury_address, + }, + tools::{spl_token::inline_spl_token, transaction_message::TransactionMessage}, + }, + versioned_transaction_ext::VaultTransactionMessageExt, +}; + +#[tokio::test] +async fn test_execute_mint_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let instruction = spl_token_2022::instruction::mint_to( + &inline_spl_token::id(), + &governed_mint_cookie.address, + &token_account_keypair.pubkey(), + &governance_cookie.address, + &[], + 10, + ) + .unwrap(); + + let transaction_message = ::try_compile( + &governance_cookie.address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + let instruction_token_account = governance_test + .get_token_account(&token_account_keypair.pubkey()) + .await; + + assert_eq!(10, instruction_token_account.amount); +} + +#[tokio::test] +async fn test_execute_transfer_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let governed_token_account_cookie = governance_test + .with_governed_token_governed_authority(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_token_account_cookie.token_mint, + &governance_test.bench.payer.pubkey(), + ) + .await; + + #[allow(deprecated)] + let instruction = spl_token_2022::instruction::transfer( + &inline_spl_token::id(), + &governed_token_account_cookie.address, + &token_account_keypair.pubkey(), + &proposal_cookie.account.governance, + &[], + 15, + ) + .unwrap(); + + let transaction_message = ::try_compile( + &proposal_cookie.account.governance, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + let instruction_token_account = governance_test + .get_token_account(&token_account_keypair.pubkey()) + .await; + + assert_eq!(15, instruction_token_account.amount); +} + +#[tokio::test] +async fn test_execute_versioned_transaction_with_create_proposal_and_execute_in_single_slot_error() +{ + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_config = governance_test.get_default_governance_config(); + governance_config.min_transaction_hold_up_time = 0; + + let mut governance_cookie = governance_test + .with_governance_using_config( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + &governance_config, + ) + .await + .unwrap(); + + let governed_mint_cookie = governance_test.with_governed_mint().await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let instruction = spl_token_2022::instruction::mint_to( + &inline_spl_token::id(), + &governed_mint_cookie.address, + &token_account_keypair.pubkey(), + &proposal_cookie.account.governance, + &[], + 10, + ) + .unwrap(); + + let transaction_message = ::try_compile( + &proposal_cookie.account.governance, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Act + let err = governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::CannotExecuteTransactionWithinHoldUpTime.into() + ); +} + +#[tokio::test] +async fn test_execute_mint_versioned_transaction_token_2022_with_extensions() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority_token_2022_with_extensions(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_2022_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let instruction = spl_token_2022::instruction::mint_to( + &&spl_token_2022::id(), + &governed_mint_cookie.address, + &token_account_keypair.pubkey(), + &governance_cookie.address, + &[], + 10, + ) + .unwrap(); + + let transaction_message = ::try_compile( + &governance_cookie.address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); + + let instruction_token_account = governance_test + .get_token_account(&token_account_keypair.pubkey()) + .await; + + assert_eq!(10, instruction_token_account.amount); +} diff --git a/governance/program/tests/process_flag_transaction_error.rs b/governance/program/tests/process_flag_transaction_error.rs index 6eff0c2ac..e95bd58e9 100644 --- a/governance/program/tests/process_flag_transaction_error.rs +++ b/governance/program/tests/process_flag_transaction_error.rs @@ -105,36 +105,40 @@ async fn test_execute_flag_transaction_error() { } #[tokio::test] +#[ignore] async fn test_execute_proposal_transaction_after_flagged_with_error() { // Arrange let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test.with_governed_mint().await; + let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -206,31 +210,36 @@ async fn test_execute_second_transaction_after_first_transaction_flagged_with_er let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await @@ -298,31 +307,36 @@ async fn test_flag_transaction_error_with_proposal_transaction_already_executed_ let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + + let governed_account_cookie = governance_test.with_governed_account().await; let token_owner_record_cookie = governance_test .with_community_token_deposit(&realm_cookie) .await .unwrap(); - let mut mint_governance_cookie = governance_test - .with_mint_governance( + let mut governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + let mut proposal_cookie = governance_test - .with_proposal(&token_owner_record_cookie, &mut mint_governance_cookie) + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) .await .unwrap(); let signatory_record_cookie = governance_test .with_signatory( &proposal_cookie, - &mint_governance_cookie, + &governance_cookie, &token_owner_record_cookie, ) .await diff --git a/governance/program/tests/process_insert_versioned_transaction.rs b/governance/program/tests/process_insert_versioned_transaction.rs new file mode 100644 index 000000000..3139d463f --- /dev/null +++ b/governance/program/tests/process_insert_versioned_transaction.rs @@ -0,0 +1,449 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; + +use { + program_test::*, + solana_program_test::tokio, + solana_sdk::{signer::Signer, system_instruction}, + spl_governance::{ + error::GovernanceError, state::native_treasury::get_native_treasury_address, + tools::transaction_message::TransactionMessage, + }, + versioned_transaction_ext::VaultTransactionMessageExt, +}; + +#[tokio::test] +async fn test_insert_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + // Assert + + let proposal_versioned_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + proposal_transaction_cookie.option_index, + proposal_versioned_transaction_account.option_index + ); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(yes_option.transactions_count, 1); + assert_eq!(yes_option.transactions_next_index, 1); + assert_eq!(yes_option.transactions_executed_count, 0); +} + +#[tokio::test] +async fn test_insert_multiple_versioned_transactions() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 1, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + // Assert + + let proposal_versioned_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + proposal_transaction_cookie.option_index, + proposal_versioned_transaction_account.option_index + ); + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(yes_option.transactions_count, 2); + assert_eq!(yes_option.transactions_next_index, 2); + assert_eq!(yes_option.transactions_executed_count, 0); +} + +#[tokio::test] +async fn test_insert_versioned_transaction_with_invalid_index_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let err = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + Some(1), + transaction_message_bytes.clone(), + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::InvalidTransactionIndex.into()); +} + +#[tokio::test] +async fn test_insert_transaction_with_proposal_transaction_already_exists_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let _ = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + let err = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 1, + Some(0), + transaction_message_bytes, + ) + .await + .err() + .unwrap(); + // Assert + assert_eq!( + err, + GovernanceError::VersionedTransactionAlreadyExists.into() + ); +} + +#[tokio::test] +async fn test_insert_versioned_transaction_with_not_editable_proposal_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + // Act + let err = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::InvalidStateCannotEditTransactions.into() + ); +} + +#[tokio::test] +async fn test_insert_versioned_transaction_with_owner_or_delegate_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let mut token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let token_owner_record_cookie2 = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner; + + // Act + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = ::try_compile( + &treasury_address, + &[instruction], + &[], + ) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + // Act + let err = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} diff --git a/governance/program/tests/process_remove_versioned_transaction.rs b/governance/program/tests/process_remove_versioned_transaction.rs new file mode 100644 index 000000000..991f2922b --- /dev/null +++ b/governance/program/tests/process_remove_versioned_transaction.rs @@ -0,0 +1,546 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; + +use { + program_test::*, + solana_program_test::tokio, + solana_sdk::{signer::Signer, system_instruction}, + spl_governance::{ + error::GovernanceError, state::native_treasury::get_native_treasury_address, + tools::transaction_message::TransactionMessage, + }, + versioned_transaction_ext::VaultTransactionMessageExt, +}; + +#[tokio::test] +async fn test_remove_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes, + ) + .await + .unwrap(); + + // Act + + governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie, + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(yes_option.transactions_count, 0); + assert_eq!(yes_option.transactions_next_index, 1); + assert_eq!(yes_option.transactions_executed_count, 0); + + let proposal_transaction_account = governance_test + .bench + .get_account(&proposal_vtransaction_cookie.address) + .await; + + assert_eq!(None, proposal_transaction_account); +} + +#[tokio::test] +async fn test_replace_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + // Act + + governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie, + ) + .await + .unwrap(); + + let proposal_transaction_cookie2 = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + Some(0), + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(yes_option.transactions_count, 2); + assert_eq!(yes_option.transactions_next_index, 2); + + let proposal_transaction_account2 = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie2.address) + .await; + + assert_eq!( + proposal_transaction_cookie2.option_index, + proposal_transaction_account2.option_index + ); +} + +#[tokio::test] +async fn test_remove_front_versioned_transaction() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + // Act + + governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie, + ) + .await + .unwrap(); + // Assert + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(yes_option.transactions_count, 1); + assert_eq!(yes_option.transactions_next_index, 2); + + let proposal_transaction_account = governance_test + .bench + .get_account(&proposal_vtransaction_cookie.address) + .await; + + assert_eq!(None, proposal_transaction_account); +} + +#[tokio::test] +async fn test_remove_versioned_transaction_with_owner_or_delegate_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let mut token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + let token_owner_record_cookie2 = governance_test + .with_council_token_deposit(&realm_cookie) + .await + .unwrap(); + + token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner; + + // Act + let err = governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into() + ); +} + +#[tokio::test] +async fn test_remove_versioned_transaction_with_proposal_not_editable_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + governance_test + .cancel_proposal(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::InvalidStateCannotEditTransactions.into() + ); +} + +#[tokio::test] +async fn test_remove_versioned_transaction_with_proposal_versioned_transaction_from_other_proposal_error( +) { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + let instruction = system_instruction::transfer( + &treasury_address, + &governance_test.bench.payer.pubkey(), + 1_000_000_000, + ); + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction.clone()], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + + let token_owner_record_cookie2 = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut proposal_cookie2 = governance_test + .with_proposal(&token_owner_record_cookie2, &mut governance_cookie) + .await + .unwrap(); + + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &[instruction.clone()], &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + + let proposal_vtransaction_cookie_2 = governance_test + .with_insert_versioned_transaction( + &mut proposal_cookie2, + &token_owner_record_cookie2, + 0, + 0, + None, + transaction_message_bytes.clone(), + ) + .await + .unwrap(); + // Act + let err = governance_test + .remove_versioned_transaction( + &proposal_cookie, + &token_owner_record_cookie, + &proposal_vtransaction_cookie_2, + ) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::InvalidProposalForProposalTransaction.into() + ); +} diff --git a/governance/program/tests/process_revoke_governing_tokens.rs b/governance/program/tests/process_revoke_governing_tokens.rs index 5a038c85f..c6e7005d3 100644 --- a/governance/program/tests/process_revoke_governing_tokens.rs +++ b/governance/program/tests/process_revoke_governing_tokens.rs @@ -34,7 +34,7 @@ async fn test_revoke_community_tokens() { // Act governance_test - .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie) + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .unwrap(); @@ -72,7 +72,7 @@ async fn test_revoke_council_tokens() { // Act governance_test - .revoke_council_tokens(&realm_cookie, &token_owner_record_cookie) + .revoke_council_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .unwrap(); @@ -91,6 +91,125 @@ async fn test_revoke_council_tokens() { assert_eq!(holding_account.amount, 0); } +#[tokio::test] +async fn test_revoke_community_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.community_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record.governing_token_deposit_amount, 0); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!(holding_account.amount, 0); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .revoke_council_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record.governing_token_deposit_amount, 0); + + let holding_account = governance_test + .get_token_account(&realm_cookie.council_token_holding_account.unwrap()) + .await; + + assert_eq!(holding_account.amount, 0); +} + +#[tokio::test] +async fn test_revoke_own_council_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + &token_owner_record_cookie.token_owner, + token_owner_record_cookie + .account + .governing_token_deposit_amount, + NopOverride, + None, + true, + ) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record.governing_token_deposit_amount, 0); +} + #[tokio::test] async fn test_revoke_own_council_tokens() { // Arrange @@ -120,6 +239,7 @@ async fn test_revoke_own_council_tokens() { .governing_token_deposit_amount, NopOverride, None, + false, ) .await .unwrap(); @@ -162,6 +282,7 @@ async fn test_revoke_own_council_tokens_with_owner_must_sign_error() { .governing_token_deposit_amount, |i| i.accounts[4].is_signer = false, // revoke_authority Some(&[]), + false, ) .await .err() @@ -186,7 +307,7 @@ async fn test_revoke_community_tokens_with_cannot_revoke_liquid_token_error() { // Act let err = governance_test - .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie) + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .err() .unwrap(); @@ -218,7 +339,7 @@ async fn test_revoke_community_tokens_with_cannot_revoke_dormant_token_error() { // Act let err = governance_test - .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie) + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .err() .unwrap(); @@ -255,6 +376,7 @@ async fn test_revoke_council_tokens_with_mint_authority_must_sign_error() { 1, |i| i.accounts[4].is_signer = false, // mint_authority Some(&[]), + false, ) .await .err() @@ -292,6 +414,7 @@ async fn test_revoke_council_tokens_with_invalid_revoke_authority_error() { 1, NopOverride, None, + false, ) .await .err() @@ -336,6 +459,7 @@ async fn test_revoke_council_tokens_with_invalid_token_holding_error() { 1, |i| i.accounts[1].pubkey = governing_token_holding_address, // governing_token_holding_address None, + false, ) .await .err() @@ -379,6 +503,7 @@ async fn test_revoke_council_tokens_with_other_realm_config_account_error() { 1, |i| i.accounts[5].pubkey = realm_cookie2.realm_config.address, //realm_config_address None, + false, ) .await .err() @@ -419,6 +544,7 @@ async fn test_revoke_council_tokens_with_invalid_realm_config_account_address_er 1, |i| i.accounts[5].pubkey = realm_config_address, // realm_config_address None, + false, ) .await .err() @@ -462,6 +588,7 @@ async fn test_revoke_council_tokens_with_token_owner_record_for_different_mint_e 1, |i| i.accounts[2].pubkey = token_owner_record_cookie2.address, // token_owner_record_address None, + false, ) .await .err() @@ -502,6 +629,7 @@ async fn test_revoke_council_tokens_with_too_large_amount_error() { 200, NopOverride, None, + false, ) .await .err() @@ -539,6 +667,7 @@ async fn test_revoke_council_tokens_with_partial_revoke_amount() { 5, NopOverride, None, + false, ) .await .unwrap(); @@ -592,6 +721,7 @@ async fn test_revoke_council_tokens_with_community_mint_error() { i.accounts[4].pubkey = governing_token_mint_authority.pubkey(); }, // mint_authority Some(&[&governing_token_mint_authority]), + false, ) .await .err() @@ -636,6 +766,534 @@ async fn test_revoke_council_tokens_with_not_matching_mint_and_authority_error() i.accounts[4].pubkey = governing_token_mint_authority.pubkey(); }, // mint_authority Some(&[&governing_token_mint_authority]), + false, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!( + err, + GovernanceError::InvalidGoverningTokenHoldingAccount.into() + ); +} + +#[tokio::test] +async fn test_revoke_own_council_2022_tokens_with_owner_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + &token_owner_record_cookie.token_owner, + token_owner_record_cookie + .account + .governing_token_deposit_amount, + |i| i.accounts[4].is_signer = false, // revoke_authority + Some(&[]), + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::GoverningTokenOwnerMustSign.into()); +} + +#[tokio::test] +async fn test_revoke_community_2022_tokens_with_cannot_revoke_liquid_token_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::CannotRevokeGoverningTokens.into()); +} + +#[tokio::test] +async fn test_revoke_community_2022_tokens_with_cannot_revoke_dormant_token_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.community_token_config_args.token_type = GoverningTokenType::Dormant; + + governance_test + .set_realm_config(&mut realm_cookie, &realm_config_args) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::CannotRevokeGoverningTokens.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_mint_authority_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| i.accounts[4].is_signer = false, // mint_authority + Some(&[]), + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::MintAuthorityMustSign.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_invalid_revoke_authority_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + &Keypair::new(), // Try to use fake authority + 1, + NopOverride, + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::InvalidMintAuthority.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_invalid_token_holding_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try to revoke from the community holding account + let governing_token_holding_address = get_governing_token_holding_address( + &governance_test.program_id, + &realm_cookie.address, + &realm_cookie.account.community_mint, + ); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| i.accounts[1].pubkey = governing_token_holding_address, // governing_token_holding_address + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!( + err, + GovernanceError::InvalidGoverningTokenHoldingAccount.into() + ); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_other_realm_config_account_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try use other Realm config + let realm_cookie2 = governance_test.with_realm_token_2022().await; + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| i.accounts[5].pubkey = realm_cookie2.realm_config.address, //realm_config_address + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::InvalidRealmConfigForRealm.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_invalid_realm_config_account_address_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try bypass config check by using none existing config account + let realm_config_address = Pubkey::new_unique(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| i.accounts[5].pubkey = realm_config_address, // realm_config_address + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::InvalidRealmConfigAddress.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_token_owner_record_for_different_mint_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try to revoke from the community token owner record + let token_owner_record_cookie2 = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| i.accounts[2].pubkey = token_owner_record_cookie2.address, // token_owner_record_address + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!( + err, + GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into() + ); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_too_large_amount_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 200, + NopOverride, + None, + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::InvalidRevokeAmount.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_partial_revoke_amount() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 5, + NopOverride, + None, + true, + ) + .await + .unwrap(); + + // Assert + + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(token_owner_record.governing_token_deposit_amount, 95); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_community_mint_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try to use mint and authority for Community to revoke Council token + let governing_token_mint = realm_cookie.account.community_mint; + let governing_token_mint_authority = clone_keypair(&realm_cookie.community_mint_authority); + let governing_token_holding_address = get_governing_token_holding_address( + &governance_test.program_id, + &realm_cookie.address, + &governing_token_mint, + ); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| { + i.accounts[1].pubkey = governing_token_holding_address; + i.accounts[3].pubkey = governing_token_mint; + i.accounts[4].pubkey = governing_token_mint_authority.pubkey(); + }, // mint_authority + Some(&[&governing_token_mint_authority]), + true, + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::CannotRevokeGoverningTokens.into()); +} + +#[tokio::test] +async fn test_revoke_council_2022_tokens_with_not_matching_mint_and_authority_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Try to use a valid mint and authority not matching the Council mint + let governing_token_mint = realm_cookie.account.community_mint; + let governing_token_mint_authority = clone_keypair(&realm_cookie.community_mint_authority); + + // Act + let err = governance_test + .revoke_governing_tokens_using_instruction( + &realm_cookie, + &token_owner_record_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 1, + |i| { + i.accounts[3].pubkey = governing_token_mint; + i.accounts[4].pubkey = governing_token_mint_authority.pubkey(); + }, // mint_authority + Some(&[&governing_token_mint_authority]), + true, ) .await .err() diff --git a/governance/program/tests/process_transaction_buffers.rs b/governance/program/tests/process_transaction_buffers.rs new file mode 100644 index 000000000..e946d45be --- /dev/null +++ b/governance/program/tests/process_transaction_buffers.rs @@ -0,0 +1,669 @@ +#![cfg(feature = "test-sbf")] + +mod program_test; + +use { + program_test::*, + solana_program_test::tokio, + solana_sdk::{ + hash::hashv, + instruction::{AccountMeta, Instruction}, + signature::Keypair, + signer::Signer, + system_instruction, system_program, + }, + spl_governance::{ + error::GovernanceError, + state::{ + enums::{ProposalState, TransactionExecutionStatus}, + native_treasury::get_native_treasury_address, + }, + tools::{spl_token::inline_spl_token, transaction_message::TransactionMessage}, + }, + versioned_transaction_ext::VaultTransactionMessageExt, +}; + +#[tokio::test] +async fn test_create_transaction_buffer_and_execute() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let mut instructions = Vec::new(); + + // Number of times to clone the instruction + let instruction_count = 60; + + // Base instruction + let base_instruction = + system_instruction::transfer(&treasury_address, &Keypair::new().pubkey(), 1000000); + + // Fill the vec with cloned instructions + for _ in 0..instruction_count { + instructions.push(base_instruction.clone()); + } + let transaction_message = + TransactionMessage::try_compile(&treasury_address, &instructions, &[]).unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + let final_buffer_size = transaction_message_bytes.len() as u16; + + let final_buffer_hash = hashv(&[transaction_message_bytes.as_slice()]); + governance_test + .with_create_transaction_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + final_buffer_hash.to_bytes(), + final_buffer_size, + vec![], // Start with empty buffer + ) + .await + .unwrap(); + + // Process the buffer in chunks + governance_test + .process_buffer_in_chunks( + &mut proposal_cookie, + &governance_cookie, + transaction_message_bytes, + 700, // chunk size + 0, // buffer index + ) + .await + .unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction_from_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + 0, + ) + .await + .unwrap(); + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); +} + +#[tokio::test] +async fn test_create_mint_transaction_buffer_and_execute() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&governance_cookie) + .await; + + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let signatory_record_cookie = governance_test + .with_signatory( + &proposal_cookie, + &governance_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let mut instructions = Vec::new(); + + // Number of times to clone the instruction + let instruction_count = 61; + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let base_instruction = spl_token_2022::instruction::mint_to( + &inline_spl_token::id(), + &governed_mint_cookie.address, + &token_account_keypair.pubkey(), + &proposal_cookie.account.governance, + &[], + 10, + ) + .unwrap(); + // Fill the vec with cloned instructions + for _ in 0..instruction_count { + instructions.push(base_instruction.clone()); + } + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &instructions, &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + let final_buffer_size = transaction_message_bytes.len() as u16; + + let final_buffer_hash = hashv(&[transaction_message_bytes.as_slice()]); + governance_test + .with_create_transaction_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + final_buffer_hash.to_bytes(), + final_buffer_size, + vec![], // Start with empty buffer + ) + .await + .unwrap(); + + // Process the buffer in chunks + governance_test + .process_buffer_in_chunks( + &mut proposal_cookie, + &governance_cookie, + transaction_message_bytes, + 700, // chunk size + 0, // buffer index + ) + .await + .unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction_from_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + 0, + ) + .await + .unwrap(); + governance_test + .sign_off_proposal(&proposal_cookie, &signatory_record_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Advance timestamp past hold_up_time + governance_test + .advance_clock_by_min_timespan( + governance_cookie + .account + .config + .min_transaction_hold_up_time as u64, + ) + .await; + + let clock = governance_test.bench.get_clock().await; + + // Act + governance_test + .with_execute_versioned_transaction( + &proposal_cookie, + &proposal_transaction_cookie, + transaction_message, + 0, + 0, + &treasury_address, + &proposal_cookie.account.governance, + &[], + ) + .await + .unwrap(); + + // Assert + + let proposal_account = governance_test + .get_proposal_account(&proposal_cookie.address) + .await; + + let yes_option = proposal_account.options.first().unwrap(); + + assert_eq!(1, yes_option.transactions_executed_count); + assert_eq!(ProposalState::Completed, proposal_account.state); + assert_eq!(Some(clock.unix_timestamp), proposal_account.closed_at); + assert_eq!(Some(clock.unix_timestamp), proposal_account.executing_at); + + let proposal_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + Some(clock.unix_timestamp), + proposal_transaction_account.executed_at + ); + + assert_eq!( + TransactionExecutionStatus::Success, + proposal_transaction_account.execution_status + ); +} + +#[tokio::test] +async fn test_close_transaction_buffer() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let treasury_address = + get_native_treasury_address(&governance_test.program_id, &governance_cookie.address); + + let mut instructions = Vec::new(); + + // Number of times to clone the instruction + let instruction_count = 60; + + // Base instruction + let base_instruction = + system_instruction::transfer(&treasury_address, &Keypair::new().pubkey(), 1000000); + + // Fill the vec with cloned instructions + for _ in 0..instruction_count { + instructions.push(base_instruction.clone()); + } + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &instructions, &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + let final_buffer_size = transaction_message_bytes.len() as u16; + println!("{}", transaction_message_bytes.len()); + + let final_buffer_hash = hashv(&[transaction_message_bytes.as_slice()]); + let proposal_transaction_buffer_cookie = governance_test + .with_create_transaction_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + final_buffer_hash.to_bytes(), + final_buffer_size, + vec![], // Start with empty buffer + ) + .await + .unwrap(); + + // Process the buffer in chunks + governance_test + .process_buffer_in_chunks( + &mut proposal_cookie, + &governance_cookie, + transaction_message_bytes, + 700, // chunk size + 0, // buffer index + ) + .await + .unwrap(); + + governance_test + .with_close_transaction_buffer( + &governance_cookie, + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + ) + .await + .unwrap(); + let proposal_transaction_buffer_account = governance_test + .bench + .get_account(&proposal_transaction_buffer_cookie.address) + .await; + + assert_eq!(None, proposal_transaction_buffer_account); +} + +#[tokio::test] +async fn test_transaction_buffer_exceeded_max_err() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&governance_cookie) + .await; + + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let mut instructions = Vec::new(); + + // Number of times to clone the instruction + let instruction_count = 61; + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let data: Vec = vec![1; 210]; + + let base_instruction = Instruction { + program_id: system_program::id(), + accounts: vec![AccountMeta::new(proposal_cookie.account.governance, false)], + data, + }; + // Fill the vec with cloned instructions + for _ in 0..instruction_count { + instructions.push(base_instruction.clone()); + } + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &instructions, &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + let final_buffer_size = transaction_message_bytes.len() as u16; + + let final_buffer_hash = hashv(&[transaction_message_bytes.as_slice()]); + + let err = governance_test + .with_create_transaction_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + final_buffer_hash.to_bytes(), + final_buffer_size, + vec![], // Start with empty buffer + ) + .await + .err() + .unwrap(); + + assert_eq!(err, GovernanceError::FinalBufferSizeExceeded.into()); +} + +#[tokio::test] +async fn test_transaction_buffer_close_to_max() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_native_treasury(&governance_cookie) + .await; + + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; + + let mut proposal_cookie = governance_test + .with_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + let mut instructions = Vec::new(); + + // Number of times to clone the instruction + let instruction_count = 42; + + let token_account_keypair = Keypair::new(); + governance_test + .bench + .create_empty_token_account( + &token_account_keypair, + &governed_mint_cookie.address, + &governance_test.bench.payer.pubkey(), + ) + .await; + + let data: Vec = vec![1; 210]; + + let base_instruction = Instruction { + program_id: system_program::id(), + accounts: vec![AccountMeta::new(proposal_cookie.account.governance, false)], + data, + }; + // Fill the vec with cloned instructions + for _ in 0..instruction_count { + instructions.push(base_instruction.clone()); + } + let transaction_message = + TransactionMessage::try_compile(&proposal_cookie.account.governance, &instructions, &[]) + .unwrap(); + // Act + let transaction_message_bytes = borsh::to_vec(&transaction_message).unwrap(); + let final_buffer_size = transaction_message_bytes.len() as u16; + + let final_buffer_hash = hashv(&[transaction_message_bytes.as_slice()]); + + let _ = governance_test + .with_create_transaction_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + final_buffer_hash.to_bytes(), + final_buffer_size, + vec![], // Start with empty buffer + ) + .await; + + // Process the buffer in chunks + governance_test + .process_buffer_in_chunks( + &mut proposal_cookie, + &governance_cookie, + transaction_message_bytes, + 700, // chunk size + 0, // buffer index + ) + .await + .unwrap(); + + let proposal_transaction_cookie = governance_test + .with_insert_versioned_transaction_from_buffer( + &mut proposal_cookie, + &token_owner_record_cookie, + 0, + 0, + None, + 0, + ) + .await + .unwrap(); + + // Assert + + let proposal_versioned_transaction_account = governance_test + .get_proposal_versioned_transaction_account(&proposal_transaction_cookie.address) + .await; + + assert_eq!( + proposal_cookie.address, + proposal_versioned_transaction_account.proposal + ); +} diff --git a/governance/program/tests/process_withdraw_governing_tokens.rs b/governance/program/tests/process_withdraw_governing_tokens.rs index 09bbd7e21..efed7725f 100644 --- a/governance/program/tests/process_withdraw_governing_tokens.rs +++ b/governance/program/tests/process_withdraw_governing_tokens.rs @@ -33,7 +33,151 @@ async fn test_withdraw_community_tokens() { // Act governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) + .await + .unwrap(); + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(0, token_owner_record.governing_token_deposit_amount); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!(0, holding_account.amount); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount, + source_account.amount + ); +} + +#[tokio::test] +async fn test_withdraw_council_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(0, token_owner_record.governing_token_deposit_amount); + + let holding_account = governance_test + .get_token_account(&realm_cookie.council_token_holding_account.unwrap()) + .await; + + assert_eq!(0, holding_account.amount); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount, + source_account.amount + ); +} + +#[tokio::test] +async fn test_withdraw_community_2022_tokens_with_transfer_fees() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test + .with_realm_token_2022_with_transfer_fees() + .await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit_with_transfer_fees(&realm_cookie) + .await + .unwrap(); + + // Act + governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(0, token_owner_record.governing_token_deposit_amount); + + let holding_account = governance_test + .get_token_account(&realm_cookie.community_token_holding_account) + .await; + + assert_eq!(0, holding_account.amount); + + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + // token is transfered twice, once for deposit and another for withdraw + // for ~100 the fee is 3 + let token_transfer_times = 2; + let transfer_fee = 3 * token_transfer_times; + + assert_eq!( + token_owner_record_cookie.token_source_amount - transfer_fee, + source_account.amount + ); +} + +#[tokio::test] +async fn test_withdraw_community_2022_tokens_with_transfer_hook() { + let transfer_hook_program_id = Pubkey::new_unique(); + + // spl_transfer_hook_example was used below. + // the extra meta accounts has to match what spl_transfer_hook_example used. + // with the only difference is that in here the mint_authority does not sign the transactions. + // Arrange + let mut governance_test = + GovernanceProgramTest::start_with_transfer_hook(Some(&transfer_hook_program_id)).await; + let realm_cookie = governance_test + .with_realm_token_2022_with_transfer_hook(&transfer_hook_program_id) + .await; + let writable_pubkey = Pubkey::new_unique(); + + // Act + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit_with_transfer_hook( + &realm_cookie, + &transfer_hook_program_id, + &writable_pubkey, + ) + .await + .unwrap(); + // Act + governance_test + .withdraw_community_2022_tokens_with_transfer_hook( + &realm_cookie, + &token_owner_record_cookie, + &transfer_hook_program_id, + ) .await .unwrap(); @@ -73,7 +217,7 @@ async fn test_withdraw_council_tokens() { // Act governance_test - .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .unwrap(); @@ -119,6 +263,7 @@ async fn test_withdraw_community_tokens_with_owner_must_sign_error() { &hacker_token_destination, &token_owner_record_cookie.token_owner.pubkey(), &realm_cookie.account.community_mint, + false, ); withdraw_ix.accounts[3] = @@ -166,6 +311,7 @@ async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch &hacker_record_cookie.token_source, &hacker_record_cookie.token_owner.pubkey(), &realm_cookie.account.community_mint, + false, ); withdraw_ix.accounts[4] = AccountMeta::new(vote_record_address, false); @@ -220,7 +366,7 @@ async fn test_withdraw_governing_tokens_with_unrelinquished_votes_error() { // Act let err = governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .err() .unwrap(); @@ -271,7 +417,7 @@ async fn test_withdraw_governing_tokens_after_relinquishing_vote() { // Act governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .unwrap(); @@ -315,6 +461,7 @@ async fn test_withdraw_tokens_with_malicious_holding_account_error() { &token_owner_record_cookie.token_source, &token_owner_record_cookie.token_owner.pubkey(), &realm_cookie.account.community_mint, + false, ); withdraw_ix.accounts[1].pubkey = realm_token_account_cookie.address; @@ -367,7 +514,7 @@ async fn test_withdraw_governing_tokens_with_outstanding_proposals_error() { // Act let err = governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .err() .unwrap(); @@ -413,7 +560,7 @@ async fn test_withdraw_governing_tokens_after_proposal_cancelled() { // Act governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .unwrap(); @@ -447,7 +594,7 @@ async fn test_withdraw_council_tokens_with_cannot_withdraw_membership_tokens_err // Act let err = governance_test - .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie, false) .await .err() .unwrap(); @@ -478,7 +625,400 @@ async fn test_withdraw_dormant_community_tokens() { // Act governance_test - .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie) + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, false) + .await + .unwrap(); + + // Assert + let token_owner_record = governance_test + .get_token_owner_record_account(&token_owner_record_cookie.address) + .await; + + assert_eq!(0, token_owner_record.governing_token_deposit_amount); +} + +#[tokio::test] +async fn test_withdraw_community_2022_tokens_with_owner_must_sign_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let hacker_token_destination = Pubkey::new_unique(); + + let mut withdraw_ix = withdraw_governing_tokens( + &governance_test.program_id, + &realm_cookie.address, + &hacker_token_destination, + &token_owner_record_cookie.token_owner.pubkey(), + &realm_cookie.account.community_mint, + true, + ); + + withdraw_ix.accounts[3] = + AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false); + + // Act + let err = governance_test + .bench + .process_transaction(&[withdraw_ix], None) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!(err, GovernanceError::GoverningTokenOwnerMustSign.into()); +} + +#[tokio::test] +async fn test_withdraw_community_2022_tokens_with_token_owner_record_address_mismatch_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let vote_record_address = get_token_owner_record_address( + &governance_test.program_id, + &realm_cookie.address, + &realm_cookie.account.community_mint, + &token_owner_record_cookie.token_owner.pubkey(), + ); + + let hacker_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut withdraw_ix = withdraw_governing_tokens( + &governance_test.program_id, + &realm_cookie.address, + &hacker_record_cookie.token_source, + &hacker_record_cookie.token_owner.pubkey(), + &realm_cookie.account.community_mint, + true, // is_token_2022 + ); + + withdraw_ix.accounts[4] = AccountMeta::new(vote_record_address, false); + + // Act + let err = governance_test + .bench + .process_transaction(&[withdraw_ix], Some(&[&hacker_record_cookie.token_owner])) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!( + err, + GovernanceError::InvalidTokenOwnerRecordAccountAddress.into() + ); +} + +#[tokio::test] +async fn test_withdraw_governing_token_2022_tokens_with_unrelinquished_votes_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + // Act + let err = governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.into() + ); +} + +#[tokio::test] +async fn test_withdraw_governing_token_2022_tokens_after_relinquishing_vote() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .with_cast_yes_no_vote(&proposal_cookie, &token_owner_record_cookie, YesNoVote::Yes) + .await + .unwrap(); + + governance_test + .relinquish_vote(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount, + source_account.amount + ); +} + +#[tokio::test] +async fn test_withdraw_token_2022_tokens_with_malicious_holding_account_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + let realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + // Try to maliciously withdraw from other token account owned by realm + + let realm_token_account_cookie = governance_test + .bench + .with_token_2022_account( + &realm_cookie.account.community_mint, + &realm_cookie.address, + &realm_cookie.community_mint_authority, + 200, + ) + .await; + + let mut withdraw_ix = withdraw_governing_tokens( + &governance_test.program_id, + &realm_cookie.address, + &token_owner_record_cookie.token_source, + &token_owner_record_cookie.token_owner.pubkey(), + &realm_cookie.account.community_mint, + true, // is_token_2022 + ); + + withdraw_ix.accounts[1].pubkey = realm_token_account_cookie.address; + + // Act + let err = governance_test + .bench + .process_transaction( + &[withdraw_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await + .err() + .unwrap(); + + // Assert + + assert_eq!( + err, + GovernanceError::InvalidGoverningTokenHoldingAccount.into() + ); +} + +#[tokio::test] +async fn test_withdraw_governing_token_2022_tokens_with_outstanding_proposals_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .err() + .unwrap(); + + // Assert + assert_eq!( + err, + GovernanceError::AllProposalsMustBeFinalisedToWithdrawGoverningTokens.into() + ); +} + +#[tokio::test] +async fn test_withdraw_governing_token_2022_tokens_after_proposal_cancelled() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let realm_cookie = governance_test.with_realm_token_2022().await; + let governed_account_cookie = governance_test.with_governed_account().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut governance_cookie = governance_test + .with_governance( + &realm_cookie, + &governed_account_cookie, + &token_owner_record_cookie, + ) + .await + .unwrap(); + + let proposal_cookie = governance_test + .with_signed_off_proposal(&token_owner_record_cookie, &mut governance_cookie) + .await + .unwrap(); + + governance_test + .cancel_proposal(&proposal_cookie, &token_owner_record_cookie) + .await + .unwrap(); + + // Act + governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .unwrap(); + + // Assert + let source_account = governance_test + .get_token_account(&token_owner_record_cookie.token_source) + .await; + + assert_eq!( + token_owner_record_cookie.token_source_amount, + source_account.amount + ); +} + +#[tokio::test] +async fn test_withdraw_council_2022_tokens_with_cannot_withdraw_membership_tokens_error() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_config_args = RealmSetupArgs::default(); + realm_config_args.council_token_config_args.token_type = GoverningTokenType::Membership; + + let realm_cookie = governance_test + .with_realm_using_args_token_2022(&realm_config_args) + .await; + + let token_owner_record_cookie = governance_test + .with_council_token_deposit_2022(&realm_cookie) + .await + .unwrap(); + + // Act + let err = governance_test + .withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie, true) + .await + .err() + .unwrap(); + + // Assert + assert_eq!(err, GovernanceError::CannotWithdrawMembershipTokens.into()); +} + +#[tokio::test] +async fn test_withdraw_dormant_community_token_2022_tokens() { + // Arrange + let mut governance_test = GovernanceProgramTest::start_new().await; + + let mut realm_cookie = governance_test.with_realm_token_2022().await; + + let token_owner_record_cookie = governance_test + .with_community_2022_token_deposit(&realm_cookie) + .await + .unwrap(); + + let mut realm_setup_args = RealmSetupArgs::default(); + realm_setup_args.community_token_config_args.token_type = GoverningTokenType::Dormant; + + governance_test + .set_realm_config(&mut realm_cookie, &realm_setup_args) + .await + .unwrap(); + + // Act + governance_test + .withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie, true) .await .unwrap(); diff --git a/governance/program/tests/program_test/cookies.rs b/governance/program/tests/program_test/cookies.rs index 9c282abc6..da336ee25 100644 --- a/governance/program/tests/program_test/cookies.rs +++ b/governance/program/tests/program_test/cookies.rs @@ -14,10 +14,12 @@ use { spl_governance_test_sdk::tools::clone_keypair, }; +#[allow(dead_code)] pub trait AccountCookie { fn get_address(&self) -> Pubkey; } +#[allow(dead_code)] #[derive(Debug)] pub struct RealmCookie { pub address: Pubkey, @@ -37,12 +39,14 @@ pub struct RealmCookie { pub realm_config: RealmConfigCookie, } +#[allow(dead_code)] #[derive(Debug)] pub struct RealmConfigCookie { pub address: Pubkey, pub account: RealmConfigAccount, } +#[allow(dead_code)] #[derive(Debug)] pub struct TokenOwnerRecordCookie { pub address: Pubkey, @@ -78,6 +82,7 @@ impl TokenOwnerRecordCookie { } } +#[allow(dead_code)] #[derive(Debug)] pub struct GovernedProgramCookie { pub address: Pubkey, @@ -92,6 +97,7 @@ impl AccountCookie for GovernedProgramCookie { } } +#[allow(dead_code)] #[derive(Debug)] pub struct GovernedMintCookie { pub address: Pubkey, @@ -105,6 +111,7 @@ impl AccountCookie for GovernedMintCookie { } } +#[allow(dead_code)] #[derive(Debug)] pub struct GovernedTokenCookie { pub address: Pubkey, @@ -137,6 +144,7 @@ pub struct GovernanceCookie { pub next_proposal_index: u32, } +#[allow(dead_code)] #[derive(Debug)] pub struct ProposalCookie { pub address: Pubkey, @@ -148,12 +156,14 @@ pub struct ProposalCookie { pub proposal_deposit: ProposalDepositCookie, } +#[allow(dead_code)] #[derive(Debug)] pub struct ProposalDepositCookie { pub address: Pubkey, pub account: ProposalDeposit, } +#[allow(dead_code)] #[derive(Debug)] pub struct SignatoryRecordCookie { pub address: Pubkey, @@ -161,6 +171,7 @@ pub struct SignatoryRecordCookie { pub signatory: Option, } +#[allow(dead_code)] #[derive(Debug)] pub struct VoteRecordCookie { pub address: Pubkey, @@ -174,26 +185,48 @@ pub struct ProposalTransactionCookie { pub instruction: Instruction, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct VoterWeightRecordCookie { pub address: Pubkey, pub account: VoterWeightRecord, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct MaxVoterWeightRecordCookie { pub address: Pubkey, pub account: MaxVoterWeightRecord, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct ProgramMetadataCookie { pub address: Pubkey, pub account: ProgramMetadata, } +#[allow(dead_code)] #[derive(Debug, Clone)] pub struct NativeTreasuryCookie { pub address: Pubkey, pub account: NativeTreasury, } + +#[allow(dead_code)] +#[derive(Debug)] +pub struct ProposalTransactionBufferCookie { + pub address: Pubkey, + pub buffer_index: u8, + + pub buffer: Vec, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct ProposalVersionedTransactionCookie { + pub address: Pubkey, + pub option_index: u8, + + pub transaction_index: u16, +} diff --git a/governance/program/tests/program_test/mod.rs b/governance/program/tests/program_test/mod.rs index aa8c45f86..3a7b4604f 100644 --- a/governance/program/tests/program_test/mod.rs +++ b/governance/program/tests/program_test/mod.rs @@ -1,4 +1,6 @@ #![allow(clippy::arithmetic_side_effects)] + +#[allow(deprecated)] use { borsh::BorshSerialize, solana_program::{ @@ -15,25 +17,23 @@ use { spl_governance::{ instruction::{ add_required_signatory, add_signatory, cancel_proposal, cast_vote, complete_proposal, - create_governance, create_mint_governance, create_native_treasury, - create_program_governance, create_proposal, create_realm, create_token_governance, - create_token_owner_record, deposit_governing_tokens, execute_transaction, - finalize_vote, flag_transaction_error, insert_transaction, refund_proposal_deposit, - relinquish_vote, remove_required_signatory, remove_transaction, - revoke_governing_tokens, set_governance_config, set_governance_delegate, - set_realm_authority, set_realm_config, sign_off_proposal, upgrade_program_metadata, - withdraw_governing_tokens, AddSignatoryAuthority, + create_governance, create_native_treasury, create_proposal, create_realm, + create_token_owner_record, deposit_governing_tokens, + deposit_governing_tokens_with_extra_account_metas, execute_transaction, finalize_vote, + flag_transaction_error, insert_transaction, refund_proposal_deposit, relinquish_vote, + remove_required_signatory, remove_transaction, revoke_governing_tokens, + set_governance_config, set_governance_delegate, set_realm_authority, set_realm_config, + sign_off_proposal, upgrade_program_metadata, withdraw_governing_tokens, + withdraw_governing_tokens_with_extra_account_metas, AddSignatoryAuthority, }, - processor::process_instruction, state::{ enums::{ GovernanceAccountType, InstructionExecutionFlags, MintMaxVoterWeightSource, ProposalState, TransactionExecutionStatus, VoteThreshold, }, governance::{ - get_governance_address, get_mint_governance_address, - get_program_governance_address, get_token_governance_address, GovernanceConfig, - GovernanceV2, DEFAULT_DEPOSIT_EXEMPT_PROPOSAL_COUNT, + get_governance_address, GovernanceConfig, GovernanceV2, + DEFAULT_DEPOSIT_EXEMPT_PROPOSAL_COUNT, }, native_treasury::{get_native_treasury_address, NativeTreasury}, program_metadata::{get_program_metadata_address, ProgramMetadata}, @@ -59,6 +59,7 @@ use { }, tools::{ bpf_loader_upgradeable::get_program_data_address, + spl_token::inline_spl_token, structs::{Reserved110, Reserved119}, }, }, @@ -69,12 +70,14 @@ use { spl_governance_addin_mock::instruction::{ setup_max_voter_weight_record, setup_voter_weight_record, }, + spl_governance_test_sdk::addins::ensure_transfer_hook_example_is_built, std::str::FromStr, }; pub mod args; pub mod cookies; pub mod legacy; +pub mod versioned_transaction_ext; use { self::cookies::ProposalDepositCookie, @@ -86,9 +89,28 @@ use { ProgramMetadataCookie, ProposalCookie, ProposalTransactionCookie, RealmCookie, TokenOwnerRecordCookie, VoteRecordCookie, }, - program_test::cookies::{ - RealmConfigCookie, SignatoryRecordCookie, VoterWeightRecordCookie, + program_test::{ + cookies::{RealmConfigCookie, SignatoryRecordCookie, VoterWeightRecordCookie}, + versioned_transaction_ext::VaultTransactionMessageExt, + }, + }, + cookies::{ProposalTransactionBufferCookie, ProposalVersionedTransactionCookie}, + solana_sdk::{ + address_lookup_table::AddressLookupTableAccount, compute_budget::ComputeBudgetInstruction, + }, + spl_governance::{ + instruction::{ + close_transaction_buffer, create_transaction_buffer, execute_versioned_transaction, + extend_transaction_buffer, insert_versioned_transaction, + insert_versioned_transaction_from_buffer, remove_versioned_transaction, + }, + state::{ + proposal_transaction_buffer::get_proposal_transaction_buffer_address, + proposal_versioned_transaction::{ + get_proposal_versioned_transaction_address, ProposalVersionedTransaction, + }, }, + tools::transaction_message::TransactionMessage, }, spl_governance_test_sdk::{ addins::ensure_addin_mock_is_built, @@ -119,7 +141,7 @@ pub struct GovernanceProgramTest { impl GovernanceProgramTest { #[allow(dead_code)] pub async fn start_new() -> Self { - Self::start_impl(false, false).await + Self::start_impl(false, false, None).await } #[allow(dead_code)] @@ -151,20 +173,47 @@ impl GovernanceProgramTest { // executed from the script. ensure_addin_mock_is_built(); - Self::start_impl(use_voter_weight_addin, use_max_voter_weight_addin).await + Self::start_impl(use_voter_weight_addin, use_max_voter_weight_addin, None).await + } + + #[allow(dead_code)] + pub async fn start_with_transfer_hook(transfer_hook_program_id: Option<&Pubkey>) -> Self { + // We only ensure the transfer-hook-example program is built but it doesn't + // detect changes. + ensure_transfer_hook_example_is_built(); + + Self::start_impl(false, false, transfer_hook_program_id).await } #[allow(dead_code)] - async fn start_impl(use_voter_weight_addin: bool, use_max_voter_weight_addin: bool) -> Self { + async fn start_impl( + use_voter_weight_addin: bool, + use_max_voter_weight_addin: bool, + transfer_hook_program_id: Option<&Pubkey>, + ) -> Self { let mut program_test = ProgramTest::default(); let program_id = Pubkey::from_str("Governance111111111111111111111111111111111").unwrap(); program_test.add_program( "spl_governance", program_id, - processor!(process_instruction), + // processor!(process_instruction), + None, + ); + + // loaded to test ephermal signers + program_test.add_program( + "mpl_core", + spl_governance_test_sdk::mpl_core_tools::program_id(), + None, ); + // Must be loaded now + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); let voter_weight_addin_id = if use_voter_weight_addin { let addin_mock_id = Pubkey::from_str("VoterWeightAddin111111111111111111111111111").unwrap(); @@ -185,6 +234,14 @@ impl GovernanceProgramTest { None }; + if transfer_hook_program_id.is_some() { + program_test.add_program( + "spl_transfer_hook_example", + *transfer_hook_program_id.unwrap(), + processor!(spl_transfer_hook_example::processor::process), + ); + }; + let bench = ProgramTestBench::start_new(program_test).await; Self { @@ -202,6 +259,33 @@ impl GovernanceProgramTest { self.with_realm_using_args(&realm_setup_args).await } + #[allow(dead_code)] + pub async fn with_realm_token_2022(&mut self) -> RealmCookie { + let realm_setup_args = RealmSetupArgs::default(); + self.with_realm_using_args_token_2022(&realm_setup_args) + .await + } + + #[allow(dead_code)] + pub async fn with_realm_token_2022_with_transfer_fees(&mut self) -> RealmCookie { + let realm_setup_args = RealmSetupArgs::default(); + self.with_realm_using_args_token_2022_with_transfer_fees(&realm_setup_args) + .await + } + + #[allow(dead_code)] + pub async fn with_realm_token_2022_with_transfer_hook( + &mut self, + transfer_hook_program_id: &Pubkey, + ) -> RealmCookie { + let realm_setup_args = RealmSetupArgs::default(); + self.with_realm_using_args_token_2022_with_transfer_hook( + &realm_setup_args, + transfer_hook_program_id, + ) + .await + } + #[allow(dead_code)] pub async fn with_realm_using_addins( &mut self, @@ -335,6 +419,8 @@ impl GovernanceProgramTest { realm_setup_args .community_mint_max_voter_weight_source .clone(), + false, // is_token_2022_for_community_token + false, // is_token_2022_for_council_token ); self.bench @@ -415,29 +501,106 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn with_realm_using_mints(&mut self, realm_cookie: &RealmCookie) -> RealmCookie { + pub async fn with_realm_using_args_token_2022( + &mut self, + realm_setup_args: &RealmSetupArgs, + ) -> RealmCookie { let name = format!("Realm #{}", self.next_realm_id).to_string(); self.next_realm_id += 1; let realm_address = get_realm_address(&self.program_id, &name); - let council_mint = realm_cookie.account.config.council_mint.unwrap(); + + let community_token_mint_keypair = Keypair::new(); + let community_token_mint_authority = Keypair::new(); + + let community_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &community_token_mint_keypair.pubkey(), + ); + + self.bench + .create_mint_2022( + &community_token_mint_keypair, + &community_token_mint_authority.pubkey(), + None, + ) + .await; + + let ( + council_token_mint_pubkey, + council_token_holding_address, + council_token_mint_authority, + ) = if realm_setup_args.use_council_mint { + let council_token_mint_keypair = Keypair::new(); + let council_token_mint_authority = Keypair::new(); + + let council_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &council_token_mint_keypair.pubkey(), + ); + + self.bench + .create_mint_2022( + &council_token_mint_keypair, + &council_token_mint_authority.pubkey(), + None, + ) + .await; + + ( + Some(council_token_mint_keypair.pubkey()), + Some(council_token_holding_address), + Some(council_token_mint_authority), + ) + } else { + (None, None, None) + }; let realm_authority = Keypair::new(); - let community_mint_max_voter_weight_source = MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION; - let min_community_weight_to_create_governance = 10; + let community_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + }; + + let council_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + }; let create_realm_ix = create_realm( &self.program_id, &realm_authority.pubkey(), - &realm_cookie.account.community_mint, - &self.bench.context.payer.pubkey(), - Some(council_mint), - None, - None, + &community_token_mint_keypair.pubkey(), + &self.bench.payer.pubkey(), + council_token_mint_pubkey, + Some(community_token_args), + Some(council_token_args), name.clone(), - min_community_weight_to_create_governance, - community_mint_max_voter_weight_source, + realm_setup_args.min_community_weight_to_create_governance, + realm_setup_args + .community_mint_max_voter_weight_source + .clone(), + true, // is_token_2022_for_community_token + true, // is_token_2022_for_council_token ); self.bench @@ -447,18 +610,20 @@ impl GovernanceProgramTest { let account = RealmV2 { account_type: GovernanceAccountType::RealmV2, - community_mint: realm_cookie.account.community_mint, + community_mint: community_token_mint_keypair.pubkey(), name, reserved: [0; 6], authority: Some(realm_authority.pubkey()), config: RealmConfig { - council_mint: Some(council_mint), + council_mint: council_token_mint_pubkey, reserved: [0; 6], - community_mint_max_voter_weight_source: - MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION, - min_community_weight_to_create_governance, + min_community_weight_to_create_governance: realm_setup_args + .min_community_weight_to_create_governance, + community_mint_max_voter_weight_source: realm_setup_args + .community_mint_max_voter_weight_source + .clone(), legacy1: 0, legacy2: 0, }, @@ -466,23 +631,38 @@ impl GovernanceProgramTest { reserved_v2: [0; 128], }; - let community_token_holding_address = get_governing_token_holding_address( - &self.program_id, - &realm_address, - &realm_cookie.account.community_mint, - ); - - let council_token_holding_address = - get_governing_token_holding_address(&self.program_id, &realm_address, &council_mint); - let realm_config_cookie = RealmConfigCookie { address: get_realm_config_address(&self.program_id, &realm_address), account: RealmConfigAccount { account_type: GovernanceAccountType::RealmConfig, realm: realm_address, - council_token_config: GoverningTokenConfig::default(), reserved: Reserved110::default(), - community_token_config: GoverningTokenConfig::default(), + community_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, + council_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, }, }; @@ -490,78 +670,675 @@ impl GovernanceProgramTest { address: realm_address, account, - community_mint_authority: clone_keypair(&realm_cookie.community_mint_authority), + community_mint_authority: community_token_mint_authority, community_token_holding_account: community_token_holding_address, - council_token_holding_account: Some(council_token_holding_address), - council_mint_authority: Some(clone_keypair( - realm_cookie.council_mint_authority.as_ref().unwrap(), - )), + council_token_holding_account: council_token_holding_address, + council_mint_authority: council_token_mint_authority, realm_authority: Some(realm_authority), realm_config: realm_config_cookie, } } - // Creates TokenOwner which owns 100 community tokens and deposits them into the - // given Realm - #[allow(dead_code)] - pub async fn with_community_token_deposit( - &mut self, - realm_cookie: &RealmCookie, - ) -> Result { - self.with_initial_governing_token_deposit( - &realm_cookie.address, - &realm_cookie.account.community_mint, - &realm_cookie.community_mint_authority, - 100, - None, - ) - .await - } - #[allow(dead_code)] - pub async fn with_community_token_owner_record( + pub async fn with_realm_using_args_token_2022_with_transfer_fees( &mut self, - realm_cookie: &RealmCookie, - ) -> TokenOwnerRecordCookie { - self.with_token_owner_record(realm_cookie, &realm_cookie.account.community_mint) - .await - } + realm_setup_args: &RealmSetupArgs, + ) -> RealmCookie { + let name = format!("Realm #{}", self.next_realm_id).to_string(); + self.next_realm_id += 1; - #[allow(dead_code)] - pub async fn with_council_token_owner_record( - &mut self, - realm_cookie: &RealmCookie, - ) -> TokenOwnerRecordCookie { - self.with_token_owner_record( - realm_cookie, - &realm_cookie.account.config.council_mint.unwrap(), - ) - .await - } + let realm_address = get_realm_address(&self.program_id, &name); - #[allow(dead_code)] - pub async fn with_token_owner_record( - &mut self, - realm_cookie: &RealmCookie, - governing_token_mint: &Pubkey, - ) -> TokenOwnerRecordCookie { - let token_owner = Keypair::new(); + let community_token_mint_keypair = Keypair::new(); + let community_token_mint_authority = Keypair::new(); - let create_token_owner_record_ix = create_token_owner_record( + let community_token_holding_address = get_governing_token_holding_address( &self.program_id, - &realm_cookie.address, - &token_owner.pubkey(), - governing_token_mint, - &self.bench.payer.pubkey(), + &realm_address, + &community_token_mint_keypair.pubkey(), ); self.bench - .process_transaction(&[create_token_owner_record_ix], None) - .await - .unwrap(); - - let account = TokenOwnerRecordV2 { + .create_mint_2022_transfer_fee( + &community_token_mint_keypair, + &community_token_mint_authority.pubkey(), + None, + ) + .await; + + let ( + council_token_mint_pubkey, + council_token_holding_address, + council_token_mint_authority, + ) = if realm_setup_args.use_council_mint { + let council_token_mint_keypair = Keypair::new(); + let council_token_mint_authority = Keypair::new(); + + let council_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &council_token_mint_keypair.pubkey(), + ); + + self.bench + .create_mint_2022( + &council_token_mint_keypair, + &council_token_mint_authority.pubkey(), + None, + ) + .await; + + ( + Some(council_token_mint_keypair.pubkey()), + Some(council_token_holding_address), + Some(council_token_mint_authority), + ) + } else { + (None, None, None) + }; + + let realm_authority = Keypair::new(); + + let community_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + }; + + let council_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + }; + + let create_realm_ix = create_realm( + &self.program_id, + &realm_authority.pubkey(), + &community_token_mint_keypair.pubkey(), + &self.bench.payer.pubkey(), + council_token_mint_pubkey, + Some(community_token_args), + Some(council_token_args), + name.clone(), + realm_setup_args.min_community_weight_to_create_governance, + realm_setup_args + .community_mint_max_voter_weight_source + .clone(), + true, // is_token_2022_for_community_token + true, // is_token_2022_for_council_token + ); + + self.bench + .process_transaction(&[create_realm_ix], None) + .await + .unwrap(); + + let account = RealmV2 { + account_type: GovernanceAccountType::RealmV2, + community_mint: community_token_mint_keypair.pubkey(), + + name, + reserved: [0; 6], + authority: Some(realm_authority.pubkey()), + config: RealmConfig { + council_mint: council_token_mint_pubkey, + reserved: [0; 6], + + min_community_weight_to_create_governance: realm_setup_args + .min_community_weight_to_create_governance, + community_mint_max_voter_weight_source: realm_setup_args + .community_mint_max_voter_weight_source + .clone(), + legacy1: 0, + legacy2: 0, + }, + legacy1: 0, + reserved_v2: [0; 128], + }; + + let realm_config_cookie = RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_address, + reserved: Reserved110::default(), + community_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, + council_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, + }, + }; + + RealmCookie { + address: realm_address, + account, + + community_mint_authority: community_token_mint_authority, + community_token_holding_account: community_token_holding_address, + + council_token_holding_account: council_token_holding_address, + council_mint_authority: council_token_mint_authority, + realm_authority: Some(realm_authority), + realm_config: realm_config_cookie, + } + } + + #[allow(dead_code)] + pub async fn with_realm_using_args_token_2022_with_transfer_hook( + &mut self, + realm_setup_args: &RealmSetupArgs, + transfer_hook_program_id: &Pubkey, + ) -> RealmCookie { + let name = format!("Realm #{}", self.next_realm_id).to_string(); + self.next_realm_id += 1; + + let realm_address = get_realm_address(&self.program_id, &name); + + let community_token_mint_keypair = Keypair::new(); + let community_token_mint_authority = Keypair::new(); + + let community_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &community_token_mint_keypair.pubkey(), + ); + + self.bench + .create_mint_2022_transfer_hook( + &community_token_mint_keypair, + &community_token_mint_authority.pubkey(), + transfer_hook_program_id, + None, + ) + .await; + + let ( + council_token_mint_pubkey, + council_token_holding_address, + council_token_mint_authority, + ) = if realm_setup_args.use_council_mint { + let council_token_mint_keypair = Keypair::new(); + let council_token_mint_authority = Keypair::new(); + + let council_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &council_token_mint_keypair.pubkey(), + ); + + self.bench + .create_mint_2022_transfer_hook( + &council_token_mint_keypair, + &council_token_mint_authority.pubkey(), + transfer_hook_program_id, + None, + ) + .await; + + ( + Some(council_token_mint_keypair.pubkey()), + Some(council_token_holding_address), + Some(council_token_mint_authority), + ) + } else { + (None, None, None) + }; + + let realm_authority = Keypair::new(); + + let community_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + }; + + let council_token_args = GoverningTokenConfigAccountArgs { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + }; + + let create_realm_ix = create_realm( + &self.program_id, + &realm_authority.pubkey(), + &community_token_mint_keypair.pubkey(), + &self.bench.payer.pubkey(), + council_token_mint_pubkey, + Some(community_token_args), + Some(council_token_args), + name.clone(), + realm_setup_args.min_community_weight_to_create_governance, + realm_setup_args + .community_mint_max_voter_weight_source + .clone(), + true, // is_token_2022_for_community_token + true, // is_token_2022_for_council_token + ); + + self.bench + .process_transaction(&[create_realm_ix], None) + .await + .unwrap(); + + let account = RealmV2 { + account_type: GovernanceAccountType::RealmV2, + community_mint: community_token_mint_keypair.pubkey(), + + name, + reserved: [0; 6], + authority: Some(realm_authority.pubkey()), + config: RealmConfig { + council_mint: council_token_mint_pubkey, + reserved: [0; 6], + + min_community_weight_to_create_governance: realm_setup_args + .min_community_weight_to_create_governance, + community_mint_max_voter_weight_source: realm_setup_args + .community_mint_max_voter_weight_source + .clone(), + legacy1: 0, + legacy2: 0, + }, + legacy1: 0, + reserved_v2: [0; 128], + }; + + let realm_config_cookie = RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_address, + reserved: Reserved110::default(), + community_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .community_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .community_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .community_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, + council_token_config: GoverningTokenConfig { + voter_weight_addin: realm_setup_args + .council_token_config_args + .voter_weight_addin, + max_voter_weight_addin: realm_setup_args + .council_token_config_args + .max_voter_weight_addin, + token_type: realm_setup_args + .council_token_config_args + .token_type + .clone(), + reserved: [0; 8], + }, + }, + }; + + RealmCookie { + address: realm_address, + account, + + community_mint_authority: community_token_mint_authority, + community_token_holding_account: community_token_holding_address, + + council_token_holding_account: council_token_holding_address, + council_mint_authority: council_token_mint_authority, + realm_authority: Some(realm_authority), + realm_config: realm_config_cookie, + } + } + + #[allow(dead_code)] + pub async fn with_realm_using_mints(&mut self, realm_cookie: &RealmCookie) -> RealmCookie { + let name = format!("Realm #{}", self.next_realm_id).to_string(); + self.next_realm_id += 1; + + let realm_address = get_realm_address(&self.program_id, &name); + let council_mint = realm_cookie.account.config.council_mint.unwrap(); + + let realm_authority = Keypair::new(); + + let community_mint_max_voter_weight_source = MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION; + let min_community_weight_to_create_governance = 10; + + let create_realm_ix = create_realm( + &self.program_id, + &realm_authority.pubkey(), + &realm_cookie.account.community_mint, + &self.bench.context.payer.pubkey(), + Some(council_mint), + None, + None, + name.clone(), + min_community_weight_to_create_governance, + community_mint_max_voter_weight_source, + false, // is_token_2022_for_community_token + false, // is_token_2022_for_council_token + ); + + self.bench + .process_transaction(&[create_realm_ix], None) + .await + .unwrap(); + + let account = RealmV2 { + account_type: GovernanceAccountType::RealmV2, + community_mint: realm_cookie.account.community_mint, + + name, + reserved: [0; 6], + authority: Some(realm_authority.pubkey()), + config: RealmConfig { + council_mint: Some(council_mint), + reserved: [0; 6], + + community_mint_max_voter_weight_source: + MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION, + min_community_weight_to_create_governance, + legacy1: 0, + legacy2: 0, + }, + legacy1: 0, + reserved_v2: [0; 128], + }; + + let community_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &realm_cookie.account.community_mint, + ); + + let council_token_holding_address = + get_governing_token_holding_address(&self.program_id, &realm_address, &council_mint); + + let realm_config_cookie = RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_address, + council_token_config: GoverningTokenConfig::default(), + reserved: Reserved110::default(), + community_token_config: GoverningTokenConfig::default(), + }, + }; + + RealmCookie { + address: realm_address, + account, + + community_mint_authority: clone_keypair(&realm_cookie.community_mint_authority), + community_token_holding_account: community_token_holding_address, + + council_token_holding_account: Some(council_token_holding_address), + council_mint_authority: Some(clone_keypair( + realm_cookie.council_mint_authority.as_ref().unwrap(), + )), + realm_authority: Some(realm_authority), + realm_config: realm_config_cookie, + } + } + + #[allow(dead_code)] + pub async fn with_realm_using_2022_mints(&mut self, realm_cookie: &RealmCookie) -> RealmCookie { + let name = format!("Realm #{}", self.next_realm_id).to_string(); + self.next_realm_id += 1; + + let realm_address = get_realm_address(&self.program_id, &name); + let council_mint = realm_cookie.account.config.council_mint.unwrap(); + + let realm_authority = Keypair::new(); + + let community_mint_max_voter_weight_source = MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION; + let min_community_weight_to_create_governance = 10; + + let create_realm_ix = create_realm( + &self.program_id, + &realm_authority.pubkey(), + &realm_cookie.account.community_mint, + &self.bench.context.payer.pubkey(), + Some(council_mint), + None, + None, + name.clone(), + min_community_weight_to_create_governance, + community_mint_max_voter_weight_source, + true, // is_token_2022_for_community_token + false, // is_token_2022_for_council_token + ); + + self.bench + .process_transaction(&[create_realm_ix], None) + .await + .unwrap(); + + let account = RealmV2 { + account_type: GovernanceAccountType::RealmV2, + community_mint: realm_cookie.account.community_mint, + + name, + reserved: [0; 6], + authority: Some(realm_authority.pubkey()), + config: RealmConfig { + council_mint: Some(council_mint), + reserved: [0; 6], + + community_mint_max_voter_weight_source: + MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION, + min_community_weight_to_create_governance, + legacy1: 0, + legacy2: 0, + }, + legacy1: 0, + reserved_v2: [0; 128], + }; + + let community_token_holding_address = get_governing_token_holding_address( + &self.program_id, + &realm_address, + &realm_cookie.account.community_mint, + ); + + let council_token_holding_address = + get_governing_token_holding_address(&self.program_id, &realm_address, &council_mint); + + let realm_config_cookie = RealmConfigCookie { + address: get_realm_config_address(&self.program_id, &realm_address), + account: RealmConfigAccount { + account_type: GovernanceAccountType::RealmConfig, + realm: realm_address, + council_token_config: GoverningTokenConfig::default(), + reserved: Reserved110::default(), + community_token_config: GoverningTokenConfig::default(), + }, + }; + + RealmCookie { + address: realm_address, + account, + + community_mint_authority: clone_keypair(&realm_cookie.community_mint_authority), + community_token_holding_account: community_token_holding_address, + + council_token_holding_account: Some(council_token_holding_address), + council_mint_authority: Some(clone_keypair( + realm_cookie.council_mint_authority.as_ref().unwrap(), + )), + realm_authority: Some(realm_authority), + realm_config: realm_config_cookie, + } + } + + // Creates TokenOwner which owns 100 community tokens and deposits them into the + // given Realm + #[allow(dead_code)] + pub async fn with_community_token_deposit( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + self.with_initial_governing_token_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + 100, + None, + ) + .await + } + + // Creates TokenOwner which owns 100 community tokens and deposits them into the + // given Realm + #[allow(dead_code)] + pub async fn with_community_2022_token_deposit( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + self.with_initial_governing_token_2022_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + 100, + None, + ) + .await + } + + // Creates TokenOwner which owns 100 community tokens and deposits them into the + // given Realm with TransferFees + #[allow(dead_code)] + pub async fn with_community_2022_token_deposit_with_transfer_fees( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + self.with_initial_governing_token_2022_deposit_with_transfer_fees( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + 100, + None, + ) + .await + } + + // Creates TokenOwner which owns 100 community tokens and deposits them into the + // given Realm with TransferHook + #[allow(dead_code)] + pub async fn with_community_2022_token_deposit_with_transfer_hook( + &mut self, + realm_cookie: &RealmCookie, + transfer_hook_program_id: &Pubkey, + writable_pubkey: &Pubkey, + ) -> Result { + self.with_initial_governing_token_2022_deposit_with_transfer_hook( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + 100, + None, + transfer_hook_program_id, + writable_pubkey, + &realm_cookie.community_token_holding_account, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_community_token_owner_record( + &mut self, + realm_cookie: &RealmCookie, + ) -> TokenOwnerRecordCookie { + self.with_token_owner_record(realm_cookie, &realm_cookie.account.community_mint) + .await + } + + #[allow(dead_code)] + pub async fn with_council_token_owner_record( + &mut self, + realm_cookie: &RealmCookie, + ) -> TokenOwnerRecordCookie { + self.with_token_owner_record( + realm_cookie, + &realm_cookie.account.config.council_mint.unwrap(), + ) + .await + } + + #[allow(dead_code)] + pub async fn with_token_owner_record( + &mut self, + realm_cookie: &RealmCookie, + governing_token_mint: &Pubkey, + ) -> TokenOwnerRecordCookie { + let token_owner = Keypair::new(); + + let create_token_owner_record_ix = create_token_owner_record( + &self.program_id, + &realm_cookie.address, + &token_owner.pubkey(), + governing_token_mint, + &self.bench.payer.pubkey(), + ); + + self.bench + .process_transaction(&[create_token_owner_record_ix], None) + .await + .unwrap(); + + let account = TokenOwnerRecordV2 { account_type: GovernanceAccountType::TokenOwnerRecordV2, realm: realm_cookie.address, governing_token_mint: *governing_token_mint, @@ -655,23 +1432,57 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn with_community_token_deposit_amount( + pub async fn with_community_token_deposit_amount( + &mut self, + realm_cookie: &RealmCookie, + amount: u64, + ) -> Result { + self.with_initial_governing_token_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + amount, + None, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_subsequent_community_token_deposit( + &mut self, + realm_cookie: &RealmCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + amount: u64, + ) { + self.with_subsequent_governing_token_deposit( + &realm_cookie.address, + &realm_cookie.account.community_mint, + &realm_cookie.community_mint_authority, + token_owner_record_cookie, + amount, + ) + .await; + } + + #[allow(dead_code)] + pub async fn with_subsequent_community_token_2022_deposit( &mut self, realm_cookie: &RealmCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, amount: u64, - ) -> Result { - self.with_initial_governing_token_deposit( + ) { + self.with_subsequent_governing_token_2022_deposit( &realm_cookie.address, &realm_cookie.account.community_mint, &realm_cookie.community_mint_authority, + token_owner_record_cookie, amount, - None, ) - .await + .await; } #[allow(dead_code)] - pub async fn with_subsequent_community_token_deposit( + pub async fn with_subsequent_council_token_deposit( &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, @@ -679,8 +1490,8 @@ impl GovernanceProgramTest { ) { self.with_subsequent_governing_token_deposit( &realm_cookie.address, - &realm_cookie.account.community_mint, - &realm_cookie.community_mint_authority, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), token_owner_record_cookie, amount, ) @@ -688,13 +1499,13 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn with_subsequent_council_token_deposit( + pub async fn with_subsequent_council_token_deposit_2022( &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, amount: u64, ) { - self.with_subsequent_governing_token_deposit( + self.with_subsequent_governing_token_2022_deposit( &realm_cookie.address, &realm_cookie.account.config.council_mint.unwrap(), realm_cookie.council_mint_authority.as_ref().unwrap(), @@ -735,6 +1546,21 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn with_council_token_deposit_2022( + &mut self, + realm_cookie: &RealmCookie, + ) -> Result { + self.with_initial_governing_token_2022_deposit( + &realm_cookie.address, + &realm_cookie.account.config.council_mint.unwrap(), + realm_cookie.council_mint_authority.as_ref().unwrap(), + 100, + None, + ) + .await + } + #[allow(dead_code)] pub async fn with_community_token_deposit_by_owner( &mut self, @@ -753,31 +1579,282 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn with_initial_governing_token_deposit( + pub async fn with_initial_governing_token_deposit( + &mut self, + realm_address: &Pubkey, + governing_mint: &Pubkey, + governing_mint_authority: &Keypair, + amount: u64, + token_owner: Option, + ) -> Result { + let token_owner = token_owner.unwrap_or_else(Keypair::new); + let token_source = Keypair::new(); + + let transfer_authority = Keypair::new(); + + self.bench + .create_token_account_with_transfer_authority( + &token_source, + governing_mint, + governing_mint_authority, + amount, + &token_owner, + &transfer_authority.pubkey(), + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.program_id, + realm_address, + &token_source.pubkey(), + &token_owner.pubkey(), + &token_owner.pubkey(), + &self.bench.payer.pubkey(), + amount, + governing_mint, + false, // is_token_2022 + ); + + self.bench + .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner])) + .await?; + + let token_owner_record_address = get_token_owner_record_address( + &self.program_id, + realm_address, + governing_mint, + &token_owner.pubkey(), + ); + + let account = TokenOwnerRecordV2 { + account_type: GovernanceAccountType::TokenOwnerRecordV2, + realm: *realm_address, + governing_token_mint: *governing_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: amount, + governance_delegate: None, + unrelinquished_votes_count: 0, + outstanding_proposal_count: 0, + version: TOKEN_OWNER_RECORD_LAYOUT_VERSION, + reserved: [0; 6], + reserved_v2: [0; 128], + }; + + let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); + + Ok(TokenOwnerRecordCookie { + address: token_owner_record_address, + account, + + token_source_amount: amount, + token_source: token_source.pubkey(), + token_owner, + governance_authority: None, + governance_delegate, + voter_weight_record: None, + max_voter_weight_record: None, + }) + } + + #[allow(dead_code)] + pub async fn with_initial_governing_token_2022_deposit( + &mut self, + realm_address: &Pubkey, + governing_mint: &Pubkey, + governing_mint_authority: &Keypair, + amount: u64, + token_owner: Option, + ) -> Result { + let token_owner = token_owner.unwrap_or_else(Keypair::new); + let token_source = Keypair::new(); + + let transfer_authority = Keypair::new(); + + self.bench + .create_token_2022_account_with_transfer_authority( + &token_source, + governing_mint, + governing_mint_authority, + amount, + &token_owner, + &transfer_authority.pubkey(), + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.program_id, + realm_address, + &token_source.pubkey(), + &token_owner.pubkey(), + &token_owner.pubkey(), + &self.bench.payer.pubkey(), + amount, + governing_mint, + true, // is_token_2022 + ); + + self.bench + .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner])) + .await?; + + let token_owner_record_address = get_token_owner_record_address( + &self.program_id, + realm_address, + governing_mint, + &token_owner.pubkey(), + ); + + let account = TokenOwnerRecordV2 { + account_type: GovernanceAccountType::TokenOwnerRecordV2, + realm: *realm_address, + governing_token_mint: *governing_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: amount, + governance_delegate: None, + unrelinquished_votes_count: 0, + outstanding_proposal_count: 0, + version: TOKEN_OWNER_RECORD_LAYOUT_VERSION, + reserved: [0; 6], + reserved_v2: [0; 128], + }; + + let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); + + Ok(TokenOwnerRecordCookie { + address: token_owner_record_address, + account, + + token_source_amount: amount, + token_source: token_source.pubkey(), + token_owner, + governance_authority: None, + governance_delegate, + voter_weight_record: None, + max_voter_weight_record: None, + }) + } + + #[allow(dead_code)] + pub async fn with_initial_governing_token_2022_deposit_with_transfer_fees( + &mut self, + realm_address: &Pubkey, + governing_mint: &Pubkey, + governing_mint_authority: &Keypair, + amount: u64, + token_owner: Option, + ) -> Result { + let token_owner = token_owner.unwrap_or_else(Keypair::new); + let token_source = Keypair::new(); + + let transfer_authority = Keypair::new(); + + self.bench + .create_token_2022_account_with_transfer_authority_with_transfer_fees( + &token_source, + governing_mint, + governing_mint_authority, + amount, + &token_owner, + &transfer_authority.pubkey(), + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.program_id, + realm_address, + &token_source.pubkey(), + &token_owner.pubkey(), + &token_owner.pubkey(), + &self.bench.payer.pubkey(), + amount, + governing_mint, + true, // is_token_2022 + ); + + self.bench + .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner])) + .await?; + + let token_owner_record_address = get_token_owner_record_address( + &self.program_id, + realm_address, + governing_mint, + &token_owner.pubkey(), + ); + // expected transfer_fee + let transfer_fee = 3; + let account = TokenOwnerRecordV2 { + account_type: GovernanceAccountType::TokenOwnerRecordV2, + realm: *realm_address, + governing_token_mint: *governing_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: amount - transfer_fee, + governance_delegate: None, + unrelinquished_votes_count: 0, + outstanding_proposal_count: 0, + version: TOKEN_OWNER_RECORD_LAYOUT_VERSION, + reserved: [0; 6], + reserved_v2: [0; 128], + }; + + let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); + + Ok(TokenOwnerRecordCookie { + address: token_owner_record_address, + account, + + token_source_amount: amount, + token_source: token_source.pubkey(), + token_owner, + governance_authority: None, + governance_delegate, + voter_weight_record: None, + max_voter_weight_record: None, + }) + } + + #[allow(dead_code)] + pub async fn with_initial_governing_token_2022_deposit_with_transfer_hook( &mut self, realm_address: &Pubkey, governing_mint: &Pubkey, governing_mint_authority: &Keypair, amount: u64, token_owner: Option, + transfer_hook_program_id: &Pubkey, + writable_pubkey: &Pubkey, + governing_token_holding_address: &Pubkey, ) -> Result { let token_owner = token_owner.unwrap_or_else(Keypair::new); let token_source = Keypair::new(); let transfer_authority = Keypair::new(); - self.bench - .create_token_account_with_transfer_authority( + .create_token_2022_account_with_transfer_authority_with_transfer_hooks( &token_source, governing_mint, governing_mint_authority, amount, &token_owner, &transfer_authority.pubkey(), + transfer_hook_program_id, ) .await; - let deposit_governing_tokens_ix = deposit_governing_tokens( + let extra_account_metas = self + .bench + .initialize_transfer_hook_account_metas( + governing_mint, + governing_mint_authority, + transfer_hook_program_id, + &token_source.pubkey(), + governing_token_holding_address, + writable_pubkey, + amount, + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens_with_extra_account_metas( &self.program_id, realm_address, &token_source.pubkey(), @@ -786,6 +1863,7 @@ impl GovernanceProgramTest { &self.bench.payer.pubkey(), amount, governing_mint, + extra_account_metas, ); self.bench @@ -850,6 +1928,75 @@ impl GovernanceProgramTest { &self.bench.payer.pubkey(), amount, governing_mint, + false, // is_token_2022 + ); + + self.bench + .process_transaction( + &[deposit_governing_tokens_ix], + Some(&[&token_owner, governing_mint_authority]), + ) + .await?; + + let token_owner_record_address = get_token_owner_record_address( + &self.program_id, + realm_address, + governing_mint, + &token_owner.pubkey(), + ); + + let account = TokenOwnerRecordV2 { + account_type: GovernanceAccountType::TokenOwnerRecordV2, + realm: *realm_address, + governing_token_mint: *governing_mint, + governing_token_owner: token_owner.pubkey(), + governing_token_deposit_amount: amount, + governance_delegate: None, + unrelinquished_votes_count: 0, + outstanding_proposal_count: 0, + version: TOKEN_OWNER_RECORD_LAYOUT_VERSION, + reserved: [0; 6], + reserved_v2: [0; 128], + }; + + let governance_delegate = Keypair::from_base58_string(&token_owner.to_base58_string()); + + Ok(TokenOwnerRecordCookie { + address: token_owner_record_address, + account, + + token_source_amount: amount, + token_source: token_source.pubkey(), + token_owner, + governance_authority: None, + governance_delegate, + voter_weight_record: None, + max_voter_weight_record: None, + }) + } + + #[allow(dead_code)] + pub async fn with_initial_governing_token_deposit_using_mint_2022( + &mut self, + realm_address: &Pubkey, + governing_mint: &Pubkey, + governing_mint_authority: &Keypair, + amount: u64, + token_owner: Option, + ) -> Result { + let token_owner = token_owner.unwrap_or_else(Keypair::new); + let token_source = Keypair::new(); + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.program_id, + realm_address, + governing_mint, + &token_owner.pubkey(), + &governing_mint_authority.pubkey(), + &self.bench.payer.pubkey(), + amount, + governing_mint, + true, // is_token_2022 ); self.bench @@ -968,6 +2115,46 @@ impl GovernanceProgramTest { &self.bench.payer.pubkey(), amount, governing_token_mint, + false, // is_token_2022 + ); + + self.bench + .process_transaction( + &[deposit_governing_tokens_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await + .unwrap(); + } + + #[allow(dead_code)] + async fn with_subsequent_governing_token_2022_deposit( + &mut self, + realm: &Pubkey, + governing_token_mint: &Pubkey, + governing_token_mint_authority: &Keypair, + token_owner_record_cookie: &TokenOwnerRecordCookie, + amount: u64, + ) { + self.bench + .mint_2022_tokens( + governing_token_mint, + governing_token_mint_authority, + &token_owner_record_cookie.token_source, + amount, + ) + .await; + + let deposit_governing_tokens_ix = deposit_governing_tokens( + &self.program_id, + realm, + &token_owner_record_cookie.token_source, + &token_owner_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.token_owner.pubkey(), + &self.bench.payer.pubkey(), + amount, + governing_token_mint, + true, // is_token_2022 ); self.bench @@ -1220,12 +2407,31 @@ impl GovernanceProgramTest { &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, + is_token_2022: bool, ) -> Result<(), ProgramError> { self.withdraw_governing_tokens( realm_cookie, token_owner_record_cookie, &realm_cookie.account.community_mint, &token_owner_record_cookie.token_owner, + is_token_2022, + ) + .await + } + + #[allow(dead_code)] + pub async fn withdraw_community_2022_tokens_with_transfer_hook( + &mut self, + realm_cookie: &RealmCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + transfer_hook_program_id: &Pubkey, + ) -> Result<(), ProgramError> { + self.withdraw_governing_2022_tokens_with_transfer_hook( + realm_cookie, + token_owner_record_cookie, + &realm_cookie.account.community_mint, + &token_owner_record_cookie.token_owner, + transfer_hook_program_id, ) .await } @@ -1235,12 +2441,14 @@ impl GovernanceProgramTest { &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, + is_token_2022: bool, ) -> Result<(), ProgramError> { self.withdraw_governing_tokens( realm_cookie, token_owner_record_cookie, &realm_cookie.account.config.council_mint.unwrap(), &token_owner_record_cookie.token_owner, + is_token_2022, ) .await } @@ -1251,8 +2459,8 @@ impl GovernanceProgramTest { realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, governing_token_mint: &Pubkey, - governing_token_owner: &Keypair, + is_token_2022: bool, ) -> Result<(), ProgramError> { let deposit_governing_tokens_ix = withdraw_governing_tokens( &self.program_id, @@ -1260,6 +2468,7 @@ impl GovernanceProgramTest { &token_owner_record_cookie.token_source, &governing_token_owner.pubkey(), governing_token_mint, + is_token_2022, ); self.bench @@ -1270,11 +2479,53 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + async fn withdraw_governing_2022_tokens_with_transfer_hook( + &mut self, + realm_cookie: &RealmCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + governing_token_mint: &Pubkey, + governing_token_owner: &Keypair, + transfer_hook_program_id: &Pubkey, + ) -> Result<(), ProgramError> { + let writable_pubkey = Pubkey::new_unique(); + let mint_authority_key = &realm_cookie.community_mint_authority; + let extra_account_metas = self + .bench + .update_transfer_hook_account_metas( + governing_token_mint, + mint_authority_key, + transfer_hook_program_id, + &realm_cookie.community_token_holding_account, + &token_owner_record_cookie.token_source, + &writable_pubkey, + 100, + ) + .await; + + let withdraw_governing_tokens_ix = withdraw_governing_tokens_with_extra_account_metas( + &self.program_id, + &realm_cookie.address, + &token_owner_record_cookie.token_source, + &governing_token_owner.pubkey(), + governing_token_mint, + extra_account_metas, + ); + + self.bench + .process_transaction( + &[withdraw_governing_tokens_ix], + Some(&[governing_token_owner]), + ) + .await + } + #[allow(dead_code)] pub async fn revoke_community_tokens( &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, + is_token_2022: bool, ) -> Result<(), ProgramError> { self.revoke_governing_tokens_using_instruction( realm_cookie, @@ -1286,6 +2537,7 @@ impl GovernanceProgramTest { .governing_token_deposit_amount, NopOverride, None, + is_token_2022, ) .await } @@ -1295,6 +2547,7 @@ impl GovernanceProgramTest { &mut self, realm_cookie: &RealmCookie, token_owner_record_cookie: &TokenOwnerRecordCookie, + is_token_2022: bool, ) -> Result<(), ProgramError> { self.revoke_governing_tokens_using_instruction( realm_cookie, @@ -1306,6 +2559,7 @@ impl GovernanceProgramTest { .governing_token_deposit_amount, NopOverride, None, + is_token_2022, ) .await } @@ -1321,6 +2575,7 @@ impl GovernanceProgramTest { amount: u64, instruction_override: F, signers_override: Option<&[&Keypair]>, + is_token_2022: bool, ) -> Result<(), ProgramError> { let mut revoke_governing_tokens_ix = revoke_governing_tokens( &self.program_id, @@ -1329,6 +2584,7 @@ impl GovernanceProgramTest { governing_token_mint, &revoke_authority.pubkey(), amount, + is_token_2022, ); instruction_override(&mut revoke_governing_tokens_ix); @@ -1355,6 +2611,14 @@ impl GovernanceProgramTest { self.with_governed_mint_impl(&mint_authority, None).await } + #[allow(dead_code)] + pub async fn with_governed_mint_token_2022(&mut self) -> GovernedMintCookie { + let mint_authority = Keypair::new(); + + self.with_governed_mint_token_2022_impl(&mint_authority, None) + .await + } + #[allow(dead_code)] pub async fn with_freezable_governed_mint(&mut self) -> GovernedMintCookie { let mint_authority = Keypair::new(); @@ -1363,6 +2627,59 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn with_governed_mint_governed_authority( + &mut self, + governance_cookie: &GovernanceCookie, + ) -> GovernedMintCookie { + let mint_keypair = Keypair::new(); + + self.bench + .create_mint(&mint_keypair, &governance_cookie.address, None) + .await; + + GovernedMintCookie { + address: mint_keypair.pubkey(), + mint_authority: Keypair::new(), + transfer_mint_authority: true, + } + } + #[allow(dead_code)] + pub async fn with_governed_mint_governed_authority_token_2022( + &mut self, + governance_cookie: &GovernanceCookie, + ) -> GovernedMintCookie { + let mint_keypair = Keypair::new(); + + self.bench + .create_mint_2022(&mint_keypair, &governance_cookie.address, None) + .await; + + GovernedMintCookie { + address: mint_keypair.pubkey(), + mint_authority: Keypair::new(), + transfer_mint_authority: true, + } + } + + #[allow(dead_code)] + pub async fn with_governed_mint_governed_authority_token_2022_with_extensions( + &mut self, + governance_cookie: &GovernanceCookie, + ) -> GovernedMintCookie { + let mint_keypair = Keypair::new(); + + self.bench + .create_mint_2022_with_extensions(&mint_keypair, &governance_cookie.address, None) + .await; + + GovernedMintCookie { + address: mint_keypair.pubkey(), + mint_authority: Keypair::new(), + transfer_mint_authority: true, + } + } + #[allow(dead_code)] pub async fn with_governed_mint_impl( &mut self, @@ -1372,18 +2689,77 @@ impl GovernanceProgramTest { let mint_keypair = Keypair::new(); self.bench - .create_mint(&mint_keypair, &mint_authority.pubkey(), freeze_authority) + .create_mint(&mint_keypair, &mint_authority.pubkey(), freeze_authority) + .await; + + GovernedMintCookie { + address: mint_keypair.pubkey(), + mint_authority: clone_keypair(mint_authority), + transfer_mint_authority: true, + } + } + + #[allow(dead_code)] + pub async fn with_governed_mint_token_2022_impl( + &mut self, + mint_authority: &Keypair, + freeze_authority: Option<&Pubkey>, + ) -> GovernedMintCookie { + let mint_keypair = Keypair::new(); + + self.bench + .create_mint_2022(&mint_keypair, &mint_authority.pubkey(), freeze_authority) + .await; + + GovernedMintCookie { + address: mint_keypair.pubkey(), + mint_authority: clone_keypair(mint_authority), + transfer_mint_authority: true, + } + } + + #[allow(dead_code)] + pub async fn with_governed_token(&mut self) -> GovernedTokenCookie { + let mint_keypair = Keypair::new(); + let mint_authority = Keypair::new(); + + self.bench + .create_mint(&mint_keypair, &mint_authority.pubkey(), None) + .await; + + let token_keypair = Keypair::new(); + let token_owner = Keypair::new(); + + self.bench + .create_empty_token_account( + &token_keypair, + &mint_keypair.pubkey(), + &token_owner.pubkey(), + ) + .await; + + self.bench + .mint_tokens( + &mint_keypair.pubkey(), + &mint_authority, + &token_keypair.pubkey(), + 100, + ) .await; - GovernedMintCookie { - address: mint_keypair.pubkey(), - mint_authority: clone_keypair(mint_authority), - transfer_mint_authority: true, + GovernedTokenCookie { + address: token_keypair.pubkey(), + token_owner, + transfer_token_owner: true, + token_mint: mint_keypair.pubkey(), } } #[allow(dead_code)] - pub async fn with_governed_token(&mut self) -> GovernedTokenCookie { + pub async fn with_governed_token_governed_authority( + &mut self, + governance_cookie: &GovernanceCookie, + ) -> GovernedTokenCookie { let mint_keypair = Keypair::new(); let mint_authority = Keypair::new(); @@ -1392,14 +2768,10 @@ impl GovernanceProgramTest { .await; let token_keypair = Keypair::new(); - let token_owner = Keypair::new(); + let token_owner = governance_cookie.address; self.bench - .create_empty_token_account( - &token_keypair, - &mint_keypair.pubkey(), - &token_owner.pubkey(), - ) + .create_empty_token_account(&token_keypair, &mint_keypair.pubkey(), &token_owner) .await; self.bench @@ -1413,7 +2785,7 @@ impl GovernanceProgramTest { GovernedTokenCookie { address: token_keypair.pubkey(), - token_owner, + token_owner: Keypair::new(), transfer_token_owner: true, token_mint: mint_keypair.pubkey(), } @@ -1593,311 +2965,25 @@ impl GovernanceProgramTest { &program_keypair.pubkey(), &program_buffer_keypair.pubkey(), &program_upgrade_authority_keypair.pubkey(), - program_account_rent, - program_data.len(), - ) - .unwrap(); - - self.bench - .process_transaction( - &deploy_ixs, - Some(&[&program_upgrade_authority_keypair, &program_keypair]), - ) - .await - .unwrap(); - - GovernedProgramCookie { - address: program_keypair.pubkey(), - upgrade_authority: program_upgrade_authority_keypair, - data_address: program_data_address, - transfer_upgrade_authority: true, - } - } - - #[allow(dead_code)] - pub async fn with_program_governance( - &mut self, - realm_cookie: &RealmCookie, - governed_program_cookie: &GovernedProgramCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - ) -> Result { - self.with_program_governance_using_instruction( - realm_cookie, - governed_program_cookie, - token_owner_record_cookie, - NopOverride, - None, - ) - .await - } - - #[allow(dead_code)] - pub async fn with_program_governance_using_instruction( - &mut self, - realm_cookie: &RealmCookie, - governed_program_cookie: &GovernedProgramCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - instruction_override: F, - signers_override: Option<&[&Keypair]>, - ) -> Result { - let config = self.get_default_governance_config(); - - let voter_weight_record = token_owner_record_cookie - .voter_weight_record - .as_ref() - .map(|voter_weight_record| voter_weight_record.address); - - let mut create_program_governance_ix = create_program_governance( - &self.program_id, - &realm_cookie.address, - &governed_program_cookie.address, - &governed_program_cookie.upgrade_authority.pubkey(), - &token_owner_record_cookie.address, - &self.bench.payer.pubkey(), - &token_owner_record_cookie.token_owner.pubkey(), - voter_weight_record, - config.clone(), - governed_program_cookie.transfer_upgrade_authority, - ); - - instruction_override(&mut create_program_governance_ix); - - let default_signers = &[ - &governed_program_cookie.upgrade_authority, - &token_owner_record_cookie.token_owner, - ]; - let signers = signers_override.unwrap_or(default_signers); - - self.bench - .process_transaction(&[create_program_governance_ix], Some(signers)) - .await?; - - let account = GovernanceV2 { - account_type: GovernanceAccountType::ProgramGovernanceV2, - realm: realm_cookie.address, - governed_account: governed_program_cookie.address, - config, - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; - - let program_governance_address = get_program_governance_address( - &self.program_id, - &realm_cookie.address, - &governed_program_cookie.address, - ); - - Ok(GovernanceCookie { - address: program_governance_address, - account, - next_proposal_index: 0, - }) - } - - #[allow(dead_code)] - pub async fn with_mint_governance( - &mut self, - realm_cookie: &RealmCookie, - governed_mint_cookie: &GovernedMintCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - ) -> Result { - self.with_mint_governance_using_instruction( - realm_cookie, - governed_mint_cookie, - token_owner_record_cookie, - NopOverride, - None, - ) - .await - } - - #[allow(dead_code)] - pub async fn with_mint_governance_using_config( - &mut self, - realm_cookie: &RealmCookie, - governed_mint_cookie: &GovernedMintCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - governance_config: &GovernanceConfig, - ) -> Result { - self.with_mint_governance_using_config_and_instruction( - realm_cookie, - governed_mint_cookie, - token_owner_record_cookie, - governance_config, - NopOverride, - None, - ) - .await - } - - #[allow(dead_code)] - pub async fn with_mint_governance_using_instruction( - &mut self, - realm_cookie: &RealmCookie, - governed_mint_cookie: &GovernedMintCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - instruction_override: F, - signers_override: Option<&[&Keypair]>, - ) -> Result { - let governance_config = self.get_default_governance_config(); - - self.with_mint_governance_using_config_and_instruction( - realm_cookie, - governed_mint_cookie, - token_owner_record_cookie, - &governance_config, - instruction_override, - signers_override, - ) - .await - } - - #[allow(dead_code)] - pub async fn with_mint_governance_using_config_and_instruction( - &mut self, - realm_cookie: &RealmCookie, - governed_mint_cookie: &GovernedMintCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - governance_config: &GovernanceConfig, - instruction_override: F, - signers_override: Option<&[&Keypair]>, - ) -> Result { - let voter_weight_record = token_owner_record_cookie - .voter_weight_record - .as_ref() - .map(|voter_weight_record| voter_weight_record.address); - - let mut create_mint_governance_ix = create_mint_governance( - &self.program_id, - &realm_cookie.address, - &governed_mint_cookie.address, - &governed_mint_cookie.mint_authority.pubkey(), - &token_owner_record_cookie.address, - &self.bench.payer.pubkey(), - &token_owner_record_cookie.token_owner.pubkey(), - voter_weight_record, - governance_config.clone(), - governed_mint_cookie.transfer_mint_authority, - ); - - instruction_override(&mut create_mint_governance_ix); - - let default_signers = &[ - &governed_mint_cookie.mint_authority, - &token_owner_record_cookie.token_owner, - ]; - let signers = signers_override.unwrap_or(default_signers); - - self.bench - .process_transaction(&[create_mint_governance_ix], Some(signers)) - .await?; - - let account = GovernanceV2 { - account_type: GovernanceAccountType::MintGovernanceV2, - realm: realm_cookie.address, - governed_account: governed_mint_cookie.address, - config: governance_config.clone(), - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; - - let mint_governance_address = get_mint_governance_address( - &self.program_id, - &realm_cookie.address, - &governed_mint_cookie.address, - ); - - Ok(GovernanceCookie { - address: mint_governance_address, - account, - next_proposal_index: 0, - }) - } - - #[allow(dead_code)] - pub async fn with_token_governance( - &mut self, - realm_cookie: &RealmCookie, - governed_token_cookie: &GovernedTokenCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - ) -> Result { - self.with_token_governance_using_instruction( - realm_cookie, - governed_token_cookie, - token_owner_record_cookie, - NopOverride, - None, - ) - .await - } - - #[allow(dead_code)] - pub async fn with_token_governance_using_instruction( - &mut self, - realm_cookie: &RealmCookie, - governed_token_cookie: &GovernedTokenCookie, - token_owner_record_cookie: &TokenOwnerRecordCookie, - instruction_override: F, - signers_override: Option<&[&Keypair]>, - ) -> Result { - let config = self.get_default_governance_config(); - - let voter_weight_record = token_owner_record_cookie - .voter_weight_record - .as_ref() - .map(|voter_weight_record| voter_weight_record.address); - - let mut create_token_governance_ix = create_token_governance( - &self.program_id, - &realm_cookie.address, - &governed_token_cookie.address, - &governed_token_cookie.token_owner.pubkey(), - &token_owner_record_cookie.address, - &self.bench.payer.pubkey(), - &token_owner_record_cookie.token_owner.pubkey(), - voter_weight_record, - config.clone(), - governed_token_cookie.transfer_token_owner, - ); - - instruction_override(&mut create_token_governance_ix); - - let default_signers = &[ - &governed_token_cookie.token_owner, - &token_owner_record_cookie.token_owner, - ]; - let signers = signers_override.unwrap_or(default_signers); - - self.bench - .process_transaction(&[create_token_governance_ix], Some(signers)) - .await?; - - let account = GovernanceV2 { - account_type: GovernanceAccountType::TokenGovernanceV2, - realm: realm_cookie.address, - governed_account: governed_token_cookie.address, - config, - reserved1: 0, - reserved_v2: Reserved119::default(), - required_signatories_count: 0, - active_proposal_count: 0, - }; + program_account_rent, + program_data.len(), + ) + .unwrap(); - let token_governance_address = get_token_governance_address( - &self.program_id, - &realm_cookie.address, - &governed_token_cookie.address, - ); + self.bench + .process_transaction( + &deploy_ixs, + Some(&[&program_upgrade_authority_keypair, &program_keypair]), + ) + .await + .unwrap(); - Ok(GovernanceCookie { - address: token_governance_address, - account, - next_proposal_index: 0, - }) + GovernedProgramCookie { + address: program_keypair.pubkey(), + upgrade_authority: program_upgrade_authority_keypair, + data_address: program_data_address, + transfer_upgrade_authority: true, + } } #[allow(dead_code)] @@ -2530,8 +3616,48 @@ impl GovernanceProgramTest { ) .await; - let mut instruction = spl_token::instruction::mint_to( - &spl_token::id(), + let mut instruction = spl_token_2022::instruction::mint_to( + &inline_spl_token::id(), + &governed_mint_cookie.address, + &token_account_keypair.pubkey(), + &proposal_cookie.account.governance, + &[], + 10, + ) + .unwrap(); + + self.with_proposal_transaction( + proposal_cookie, + token_owner_record_cookie, + option_index, + index, + &mut instruction, + hold_up_time, + ) + .await + } + + #[allow(dead_code)] + pub async fn with_mint_tokens_token_2022_transaction( + &mut self, + governed_mint_cookie: &GovernedMintCookie, + proposal_cookie: &mut ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + option_index: u8, + index: Option, + hold_up_time: Option, + ) -> Result { + let token_account_keypair = Keypair::new(); + self.bench + .create_empty_token_2022_account( + &token_account_keypair, + &governed_mint_cookie.address, + &self.bench.payer.pubkey(), + ) + .await; + + let mut instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), &governed_mint_cookie.address, &token_account_keypair.pubkey(), &proposal_cookie.account.governance, @@ -2568,8 +3694,9 @@ impl GovernanceProgramTest { ) .await; - let mut instruction = spl_token::instruction::transfer( - &spl_token::id(), + #[allow(deprecated)] + let mut instruction = spl_token_2022::instruction::transfer( + &inline_spl_token::id(), &governed_token_cookie.address, &token_account_keypair.pubkey(), &proposal_cookie.account.governance, @@ -3134,6 +4261,18 @@ impl GovernanceProgramTest { .await } + #[allow(dead_code)] + pub async fn get_proposal_versioned_transaction_account( + &mut self, + proposal_versioned_transaction_address: &Pubkey, + ) -> ProposalVersionedTransaction { + self.bench + .get_borsh_account::( + proposal_versioned_transaction_address, + ) + .await + } + #[allow(dead_code)] pub async fn get_required_signatory_account( &mut self, @@ -3220,12 +4359,12 @@ impl GovernanceProgramTest { } #[allow(dead_code)] - pub async fn get_token_account(&mut self, address: &Pubkey) -> spl_token::state::Account { + pub async fn get_token_account(&mut self, address: &Pubkey) -> spl_token_2022::state::Account { self.get_packed_account(address).await } #[allow(dead_code)] - pub async fn get_mint_account(&mut self, address: &Pubkey) -> spl_token::state::Mint { + pub async fn get_mint_account(&mut self, address: &Pubkey) -> spl_token_2022::state::Mint { self.get_packed_account(address).await } @@ -3368,4 +4507,297 @@ impl GovernanceProgramTest { Ok(()) } + + #[allow(dead_code)] + pub async fn with_create_transaction_buffer( + &mut self, + proposal_cookie: &mut ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + buffer_index: u8, + final_buffer_hash: [u8; 32], + final_buffer_size: u16, + buffer: Vec, + ) -> Result { + let create_buffer_ix = create_transaction_buffer( + &self.program_id, + &proposal_cookie.account.governance, + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &self.bench.payer.pubkey(), + buffer_index, + final_buffer_hash, + final_buffer_size, + buffer.clone(), + ); + + self.bench + .process_transaction( + &[create_buffer_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + let buffer_address = get_proposal_transaction_buffer_address( + &self.program_id, + &proposal_cookie.address, + &self.bench.payer.pubkey(), + &buffer_index.to_le_bytes(), + ); + + let buffer_cookie = ProposalTransactionBufferCookie { + address: buffer_address, + buffer_index, + buffer, + }; + + Ok(buffer_cookie) + } + + #[allow(dead_code)] + pub async fn with_extend_transaction_buffer( + &mut self, + proposal_cookie: &ProposalCookie, + governance: &GovernanceCookie, + buffer_index: u8, + buffer: Vec, + ) -> Result<(), ProgramError> { + let extend_buffer_ix = extend_transaction_buffer( + &self.program_id, + &governance.address, + &proposal_cookie.address, + &self.bench.payer.pubkey(), + buffer_index, + buffer, + ); + + self.bench + .process_transaction(&[extend_buffer_ix], None) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn with_close_transaction_buffer( + &mut self, + governance: &GovernanceCookie, + proposal_cookie: &ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + buffer_index: u8, + ) -> Result<(), ProgramError> { + let close_buffer_ix = close_transaction_buffer( + &self.program_id, + &governance.address, + &proposal_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &token_owner_record_cookie.address, + &self.bench.payer.pubkey(), + buffer_index, + ); + + self.bench + .process_transaction( + &[close_buffer_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn with_insert_versioned_transaction_from_buffer( + &mut self, + proposal_cookie: &mut ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + option_index: u8, + ephemeral_signers: u8, + transaction_index: Option, + buffer_index: u8, + ) -> Result { + let yes_option = &mut proposal_cookie.account.options[0]; + let tx_index = transaction_index.unwrap_or(yes_option.transactions_next_index); + yes_option.transactions_next_index += 1; + + let insert_tx_ix = insert_versioned_transaction_from_buffer( + &self.program_id, + &proposal_cookie.account.governance, + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &self.bench.payer.pubkey(), + option_index, + ephemeral_signers, + tx_index, + buffer_index, + ); + let compute_heap_ix = ComputeBudgetInstruction::request_heap_frame(8 * 32 * 1024); + let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + + self.bench + .process_transaction( + &[compute_heap_ix, compute_unit_ix, insert_tx_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + let versioned_tx_address = get_proposal_versioned_transaction_address( + &self.program_id, + &proposal_cookie.address, + &option_index.to_le_bytes(), + &tx_index.to_le_bytes(), + ); + + let versioned_tx_cookie = ProposalVersionedTransactionCookie { + address: versioned_tx_address, + option_index, + transaction_index: tx_index, + }; + + Ok(versioned_tx_cookie) + } + + #[allow(dead_code)] + pub async fn with_insert_versioned_transaction( + &mut self, + proposal_cookie: &mut ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + option_index: u8, + ephemeral_signers: u8, + transaction_index: Option, + transaction_message: Vec, + ) -> Result { + let yes_option = &mut proposal_cookie.account.options[0]; + let tx_index = transaction_index.unwrap_or(yes_option.transactions_next_index); + yes_option.transactions_next_index += 1; + + let insert_tx_ix = insert_versioned_transaction( + &self.program_id, + &proposal_cookie.account.governance, + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &self.bench.payer.pubkey(), + option_index, + ephemeral_signers, + tx_index, + transaction_message, + ); + + self.bench + .process_transaction( + &[insert_tx_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + let versioned_tx_address = get_proposal_versioned_transaction_address( + &self.program_id, + &proposal_cookie.address, + &option_index.to_le_bytes(), + &tx_index.to_le_bytes(), + ); + + let versioned_tx_cookie = ProposalVersionedTransactionCookie { + address: versioned_tx_address, + option_index, + transaction_index: tx_index, + }; + + Ok(versioned_tx_cookie) + } + + #[allow(dead_code)] + pub async fn with_execute_versioned_transaction( + &mut self, + proposal_cookie: &ProposalCookie, + versioned_transaction_cookie: &ProposalVersionedTransactionCookie, + proposal_transaction_message: TransactionMessage, + ephemeral_signers: u8, + transaction_index: u16, + native_treasury_pubkey: &Pubkey, + governance_pubkey: &Pubkey, + address_lookup_table_accounts: &[AddressLookupTableAccount], + ) -> Result<(), ProgramError> { + let mut execute_tx_ix = execute_versioned_transaction( + &self.program_id, + &proposal_cookie.account.governance, + &proposal_cookie.address, + &versioned_transaction_cookie.address, + ); + + let accounts_for_execute = proposal_transaction_message + .get_accounts_for_execute( + native_treasury_pubkey, + governance_pubkey, + &versioned_transaction_cookie.address, + &transaction_index, + &address_lookup_table_accounts, + ephemeral_signers, + &self.program_id, + ) + .unwrap(); + + execute_tx_ix + .accounts + .extend(accounts_for_execute.into_iter()); + let compute_heap_ix = ComputeBudgetInstruction::request_heap_frame(8 * 32 * 1024); + let compute_unit_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + self.bench + .process_transaction(&[compute_heap_ix, compute_unit_ix, execute_tx_ix], None) + .await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn process_buffer_in_chunks( + &mut self, + proposal_cookie: &mut ProposalCookie, + governance: &GovernanceCookie, + buffer: Vec, + chunk_size: usize, + buffer_index: u8, + ) -> Result<(), ProgramError> { + let mut start_index = 0; + + while start_index < buffer.len() { + let end_index = std::cmp::min(start_index + chunk_size, buffer.len()); + let chunk = buffer[start_index..end_index].to_vec(); + + self.with_extend_transaction_buffer(proposal_cookie, governance, buffer_index, chunk) + .await?; + + start_index = end_index; + } + + Ok(()) + } + + #[allow(dead_code)] + pub async fn remove_versioned_transaction( + &mut self, + proposal_cookie: &ProposalCookie, + token_owner_record_cookie: &TokenOwnerRecordCookie, + proposal_vtransaction_cookie: &ProposalVersionedTransactionCookie, + ) -> Result<(), ProgramError> { + let remove_transaction_ix = remove_versioned_transaction( + &self.program_id, + &proposal_cookie.address, + &token_owner_record_cookie.address, + &token_owner_record_cookie.token_owner.pubkey(), + &proposal_vtransaction_cookie.address, + &self.bench.payer.pubkey(), + ); + + self.bench + .process_transaction( + &[remove_transaction_ix], + Some(&[&token_owner_record_cookie.token_owner]), + ) + .await?; + + Ok(()) + } } diff --git a/governance/program/tests/program_test/versioned_transaction_ext.rs b/governance/program/tests/program_test/versioned_transaction_ext.rs new file mode 100644 index 000000000..0fd69f8f2 --- /dev/null +++ b/governance/program/tests/program_test/versioned_transaction_ext.rs @@ -0,0 +1,206 @@ +use solana_program::instruction::{AccountMeta, Instruction}; +use solana_program::message::{AccountKeys, CompileError}; +use solana_program::pubkey::Pubkey; +use solana_sdk::address_lookup_table::AddressLookupTableAccount; +use spl_governance::state::proposal_versioned_transaction::ProposalTransactionMessage; +use spl_governance::tools::transaction_message::{ + CompiledInstruction, MessageAddressTableLookup, TransactionMessage, +}; +use spl_governance_test_sdk::versioned_transaction::{ + compiled_keys::CompiledKeys, pda::get_ephemeral_signer_pda, +}; +use std::collections::HashMap; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Invalid AddressLookupTableAccount")] + InvalidAddressLookupTableAccount, + #[error("Invalid TransactionMessage")] + InvalidTransactionMessage, +} + +pub trait VaultTransactionMessageExt { + fn as_transaction_message(&self) -> &TransactionMessage; + + /// This implementation is mostly a copy-paste from `solana_program::message::v0::Message::try_compile()`, + /// but it constructs a `TransactionMessage` meant to be passed to `transaction_create`. + #[allow(dead_code)] + fn try_compile( + payer: &Pubkey, + instructions: &[Instruction], + address_lookup_table_accounts: &[AddressLookupTableAccount], + ) -> Result { + let mut compiled_keys = CompiledKeys::compile(instructions, Some(*payer)); + + let mut address_table_lookups = Vec::with_capacity(address_lookup_table_accounts.len()); + let mut loaded_addresses_list = Vec::with_capacity(address_lookup_table_accounts.len()); + for lookup_table_account in address_lookup_table_accounts { + if let Some((lookup, loaded_addresses)) = + compiled_keys.try_extract_table_lookup(lookup_table_account)? + { + address_table_lookups.push(lookup); + loaded_addresses_list.push(loaded_addresses); + } + } + + let (header, static_keys) = compiled_keys.try_into_message_components()?; + let dynamic_keys = loaded_addresses_list.into_iter().collect(); + let account_keys = AccountKeys::new(&static_keys, Some(&dynamic_keys)); + let instructions = account_keys.try_compile_instructions(instructions)?; + + let num_static_keys: u8 = static_keys + .len() + .try_into() + .map_err(|_| CompileError::AccountIndexOverflow)?; + + Ok(TransactionMessage { + num_signers: header.num_required_signatures, + num_writable_signers: header.num_required_signatures + - header.num_readonly_signed_accounts, + num_writable_non_signers: num_static_keys + - header.num_required_signatures + - header.num_readonly_unsigned_accounts, + account_keys: static_keys.into(), + instructions: instructions + .into_iter() + .map(|ix| CompiledInstruction { + program_id_index: ix.program_id_index, + account_indexes: ix.accounts.into(), + data: ix.data.into(), + }) + .collect::>() + .into(), + address_table_lookups: address_table_lookups + .into_iter() + .map(|lookup| MessageAddressTableLookup { + account_key: lookup.account_key, + writable_indexes: lookup.writable_indexes.into(), + readonly_indexes: lookup.readonly_indexes.into(), + }) + .collect::>() + .into(), + }) + } + + fn get_accounts_for_execute( + &self, + native_treasury_pubkey: &Pubkey, + governance_pubkey: &Pubkey, + transaction_proposal_pda: &Pubkey, + transaction_index: &u16, + address_lookup_table_accounts: &[AddressLookupTableAccount], + num_ephemeral_signers: u8, + program_id: &Pubkey, + ) -> Result, Error> { + let message = + ProposalTransactionMessage::try_from(self.as_transaction_message().to_owned()) + .map_err(|_| Error::InvalidTransactionMessage)?; + + let ephemeral_signer_pdas: Vec = (0..num_ephemeral_signers) + .into_iter() + .map(|ephemeral_signer_index| { + get_ephemeral_signer_pda( + transaction_proposal_pda, + ephemeral_signer_index, + program_id, + *transaction_index, + ) + .0 + }) + .collect(); + + let address_lookup_tables = address_lookup_table_accounts + .into_iter() + .map(|alt| (alt.key, alt)) + .collect::>(); + + // First go the lookup table accounts used by the transaction. They are needed for on-chain validation. + let lookup_table_account_metas = address_lookup_table_accounts + .iter() + .map(|alt| AccountMeta { + pubkey: alt.key, + is_writable: false, + is_signer: false, + }) + .collect::>(); + + // Then come static account keys included into the message. + let static_account_metas = message + .account_keys + .iter() + .enumerate() + .map(|(index, &pubkey)| { + AccountMeta { + pubkey, + is_writable: message.is_static_writable_index(index), + // NOTE: proposal_pda and ephemeral_signer_pdas cannot be marked as signers, + // because they are PDAs and hence won't have their signatures on the transaction. + is_signer: message.is_signer_index(index) + // native_treasury, governance_pubkey, and ephermal_signers can only sign the final transaction + && &pubkey != native_treasury_pubkey + && &pubkey != governance_pubkey + && !ephemeral_signer_pdas.contains(&pubkey), + } + }) + .collect::>(); + + // And the last go the accounts that will be loaded with address lookup tables. + let loaded_account_metas = message + .address_table_lookups + .iter() + .map(|lookup| { + let lookup_table_account = address_lookup_tables + .get(&lookup.account_key) + .ok_or(Error::InvalidAddressLookupTableAccount)?; + + // For each lookup, fist list writable, then readonly account metas. + lookup + .writable_indexes + .iter() + .map(|&index| { + let pubkey = lookup_table_account + .addresses + .get(index as usize) + .ok_or(Error::InvalidAddressLookupTableAccount)? + .to_owned(); + + Ok(AccountMeta { + pubkey, + is_writable: true, + is_signer: false, + }) + }) + .chain(lookup.readonly_indexes.iter().map(|&index| { + let pubkey = lookup_table_account + .addresses + .get(index as usize) + .ok_or(Error::InvalidAddressLookupTableAccount)? + .to_owned(); + + Ok(AccountMeta { + pubkey, + is_writable: false, + is_signer: false, + }) + })) + .collect::, Error>>() + }) + .collect::, Error>>()? + .into_iter() + .flatten() + .collect::>(); + + Ok([ + lookup_table_account_metas, + static_account_metas, + loaded_account_metas, + ] + .concat()) + } +} + +impl VaultTransactionMessageExt for TransactionMessage { + fn as_transaction_message(&self) -> &TransactionMessage { + self + } +} diff --git a/governance/program/tests/use_proposals_with_multiple_options.rs b/governance/program/tests/use_proposals_with_multiple_options.rs index a7190ce8c..4e58083f2 100644 --- a/governance/program/tests/use_proposals_with_multiple_options.rs +++ b/governance/program/tests/use_proposals_with_multiple_options.rs @@ -626,7 +626,7 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; // 100 tokens let token_owner_record_cookie1 = governance_test @@ -651,14 +651,17 @@ async fn test_execute_proposal_with_multiple_options_and_partial_success() { governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(30); let mut governance_cookie = governance_test - .with_mint_governance_using_config( + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie1, &governance_config, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test .with_multi_option_proposal( @@ -845,7 +848,7 @@ async fn test_try_execute_proposal_with_multiple_options_and_full_deny() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; // 100 tokens let token_owner_record_cookie1 = governance_test @@ -862,11 +865,12 @@ async fn test_try_execute_proposal_with_multiple_options_and_full_deny() { // 100 tokes approval quorum let mut governance_config = governance_test.get_default_governance_config(); governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(30); + let governed_mint_cookie = governance_test.with_governed_mint().await; let mut governance_cookie = governance_test - .with_mint_governance_using_config( + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie1, &governance_config, ) @@ -1251,7 +1255,7 @@ async fn test_vote_multi_weighted_choice_proposal_with_partial_success() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; // 100 tokens each, sum 300 tokens let token_owner_record_cookie1 = governance_test @@ -1272,15 +1276,17 @@ async fn test_vote_multi_weighted_choice_proposal_with_partial_success() { governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(20); let mut governance_cookie = governance_test - .with_mint_governance_using_config( + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie1, &governance_config, ) .await .unwrap(); - + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test .with_multi_option_proposal( &token_owner_record_cookie1, @@ -1477,7 +1483,7 @@ async fn test_vote_multi_weighted_choice_proposal_with_multi_success() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; // 100 tokens each, sum 300 tokens let token_owner_record_cookie1 = governance_test @@ -1494,14 +1500,17 @@ async fn test_vote_multi_weighted_choice_proposal_with_multi_success() { governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(30); let mut governance_cookie = governance_test - .with_mint_governance_using_config( + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie1, &governance_config, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test .with_multi_option_proposal( @@ -1660,7 +1669,7 @@ async fn test_vote_multi_weighted_choice_proposal_executable_with_full_deny() { let mut governance_test = GovernanceProgramTest::start_new().await; let realm_cookie = governance_test.with_realm().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; + let governed_account_cookie = governance_test.with_governed_account().await; // 100 tokens let token_owner_record_cookie1 = governance_test @@ -1678,14 +1687,17 @@ async fn test_vote_multi_weighted_choice_proposal_executable_with_full_deny() { governance_config.community_vote_threshold = VoteThreshold::YesVotePercentage(3); let mut governance_cookie = governance_test - .with_mint_governance_using_config( + .with_governance_using_config( &realm_cookie, - &governed_mint_cookie, + &governed_account_cookie, &token_owner_record_cookie1, &governance_config, ) .await .unwrap(); + let governed_mint_cookie = governance_test + .with_governed_mint_governed_authority(&governance_cookie) + .await; let mut proposal_cookie = governance_test .with_multi_option_proposal( diff --git a/governance/program/tests/use_realm_with_voter_weight_addin.rs b/governance/program/tests/use_realm_with_voter_weight_addin.rs index 342078cc5..c6d003505 100644 --- a/governance/program/tests/use_realm_with_voter_weight_addin.rs +++ b/governance/program/tests/use_realm_with_voter_weight_addin.rs @@ -194,7 +194,7 @@ async fn test_cast_vote_with_voter_weight_addin() { async fn test_create_token_governance_with_voter_weight_addin() { // Arrange let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; - let governed_token_cookie = governance_test.with_governed_token().await; + let governed_account_cookie = governance_test.with_governed_account().await; let realm_cookie = governance_test .with_realm_using_addins(PluginSetupArgs::COMMUNITY_VOTER_WEIGHT) @@ -210,10 +210,10 @@ async fn test_create_token_governance_with_voter_weight_addin() { .unwrap(); // Act - let token_governance_cookie = governance_test - .with_token_governance( + let governance_cookie = governance_test + .with_governance( &realm_cookie, - &governed_token_cookie, + &governed_account_cookie, &token_owner_record_cookie, ) .await @@ -221,87 +221,10 @@ async fn test_create_token_governance_with_voter_weight_addin() { // // Assert let token_governance_account = governance_test - .get_governance_account(&token_governance_cookie.address) - .await; - - assert_eq!(token_governance_cookie.account, token_governance_account); -} - -#[tokio::test] -async fn test_create_mint_governance_with_voter_weight_addin() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; - let governed_mint_cookie = governance_test.with_governed_mint().await; - - let realm_cookie = governance_test - .with_realm_using_addins(PluginSetupArgs::COMMUNITY_VOTER_WEIGHT) - .await; - - let mut token_owner_record_cookie = governance_test - .with_community_token_owner_record(&realm_cookie) - .await; - - governance_test - .with_voter_weight_addin_record(&mut token_owner_record_cookie) - .await - .unwrap(); - - // Act - let mint_governance_cookie = governance_test - .with_mint_governance( - &realm_cookie, - &governed_mint_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // // Assert - let mint_governance_account = governance_test - .get_governance_account(&mint_governance_cookie.address) - .await; - - assert_eq!(mint_governance_cookie.account, mint_governance_account); -} - -#[tokio::test] -async fn test_create_program_governance_with_voter_weight_addin() { - // Arrange - let mut governance_test = GovernanceProgramTest::start_with_voter_weight_addin().await; - let governed_program_cookie = governance_test.with_governed_program().await; - - let realm_cookie = governance_test - .with_realm_using_addins(PluginSetupArgs::COMMUNITY_VOTER_WEIGHT) - .await; - - let mut token_owner_record_cookie = governance_test - .with_community_token_owner_record(&realm_cookie) - .await; - - governance_test - .with_voter_weight_addin_record(&mut token_owner_record_cookie) - .await - .unwrap(); - - // Act - let program_governance_cookie = governance_test - .with_program_governance( - &realm_cookie, - &governed_program_cookie, - &token_owner_record_cookie, - ) - .await - .unwrap(); - - // Assert - let program_governance_account = governance_test - .get_governance_account(&program_governance_cookie.address) + .get_governance_account(&governance_cookie.address) .await; - assert_eq!( - program_governance_cookie.account, - program_governance_account - ); + assert_eq!(governance_cookie.account, token_governance_account); } #[tokio::test] diff --git a/governance/test-sdk/Cargo.toml b/governance/test-sdk/Cargo.toml index c7fb2d333..8624ceb0f 100644 --- a/governance/test-sdk/Cargo.toml +++ b/governance/test-sdk/Cargo.toml @@ -19,5 +19,16 @@ serde_derive = "1.0.103" solana-program = { workspace = true } solana-program-test = { workspace = true } solana-sdk = { workspace = true } -spl-token = { version = "4.0", path = "../../token/program", features = [ "no-entrypoint" ] } +spl-token = { version = "4.0.0", path = "../../token/program", features = [ + "no-entrypoint", +] } +spl-token-2022 = { workspace = true } thiserror = "1.0" +spl-tlv-account-resolution = { workspace = true } +spl-transfer-hook-interface = { workspace = true } +spl-transfer-hook-example = { version = "0.4", path = "../../token/transfer-hook/example", features = [ "no-entrypoint" ] } +spl-token-client = { workspace = true } +mpl-core = "0.8.1-beta.1" + +[lints] +workspace = true \ No newline at end of file diff --git a/governance/test-sdk/src/addins.rs b/governance/test-sdk/src/addins.rs index 101f1b3a9..35c9d1e7b 100644 --- a/governance/test-sdk/src/addins.rs +++ b/governance/test-sdk/src/addins.rs @@ -7,6 +7,9 @@ use { lazy_static! { pub static ref VOTER_WEIGHT_ADDIN_BUILD_GUARD: Mutex:: = Mutex::new(0); } +lazy_static! { + pub static ref SPL_TRANSFER_HOOK_EXAMPLE_BUILD: Mutex:: = Mutex::new(0); +} pub fn ensure_addin_mock_is_built() { if find_file("spl_governance_voter_weight_addin_mock.so").is_none() { @@ -24,3 +27,21 @@ pub fn ensure_addin_mock_is_built() { } } } + +pub fn ensure_transfer_hook_example_is_built() { + if find_file("spl-transfer-hook-example.so").is_none() { + let _spl_transfer_hook_example = SPL_TRANSFER_HOOK_EXAMPLE_BUILD.lock().unwrap(); + if find_file("spl-transfer-hook-example.so").is_none() { + assert!(Command::new("cargo") + .args([ + "build-sbf", + "--no-default-features", + "--manifest-path", + "../../token/transfer-hook/example/Cargo.toml", + ]) + .status() + .expect("Failed to build spl-transfer-hook-example program") + .success()); + } + } +} \ No newline at end of file diff --git a/governance/test-sdk/src/lib.rs b/governance/test-sdk/src/lib.rs index f4717a45d..b1eeb78be 100644 --- a/governance/test-sdk/src/lib.rs +++ b/governance/test-sdk/src/lib.rs @@ -12,18 +12,32 @@ use { solana_program_test::{ProgramTest, ProgramTestContext}, solana_sdk::{ account::{Account, AccountSharedData, WritableAccount}, + instruction::AccountMeta, signature::Keypair, signer::Signer, transaction::Transaction, }, - spl_token::instruction::{set_authority, AuthorityType}, + spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, + }, + spl_token_2022::instruction::{set_authority, AuthorityType}, + spl_token_2022::{extension::ExtensionType, state::Mint}, + spl_token_client::token::ExtensionInitializationParams, + spl_transfer_hook_interface::{ + get_extra_account_metas_address, + instruction::{initialize_extra_account_meta_list, update_extra_account_meta_list}, + }, std::borrow::Borrow, + token_2022::{test_transfer_fee_config_with_keypairs, TransferFeeConfigWithKeypairs}, tools::clone_keypair, }; pub mod addins; pub mod cookies; +pub mod mpl_core_tools; +pub mod token_2022; pub mod tools; +pub mod versioned_transaction; /// Program's test bench which captures test context, rent and payer and common /// utility functions @@ -38,7 +52,7 @@ impl ProgramTestBench { /// Create new bench given a ProgramTest instance populated with all of the /// desired programs. pub async fn start_new(program_test: ProgramTest) -> Self { - let mut context = program_test.start_with_context().await; + let context = program_test.start_with_context().await; let rent = context.banks_client.get_rent().await.unwrap(); let payer = clone_keypair(&context.payer); @@ -149,6 +163,405 @@ impl ProgramTestBench { .unwrap(); } + pub async fn create_mint_2022( + &mut self, + mint_keypair: &Keypair, + mint_authority: &Pubkey, + freeze_authority: Option<&Pubkey>, + ) { + let mint_rent = self.rent.minimum_balance(spl_token_2022::state::Mint::LEN); + + let instructions = [ + system_instruction::create_account( + &self.context.payer.pubkey(), + &mint_keypair.pubkey(), + mint_rent, + spl_token_2022::state::Mint::LEN as u64, + &spl_token_2022::id(), + ), + spl_token_2022::instruction::initialize_mint( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + mint_authority, + freeze_authority, + 0, + ) + .unwrap(), + ]; + + self.process_transaction(&instructions, Some(&[mint_keypair])) + .await + .unwrap(); + } + + pub async fn create_mint_2022_with_extensions( + &mut self, + mint_keypair: &Keypair, + mint_authority: &Pubkey, + freeze_authority: Option<&Pubkey>, + ) { + let extension_initialization_params = vec![ + ExtensionInitializationParams::MintCloseAuthority { + close_authority: Some(*mint_authority), + }, + ExtensionInitializationParams::PermanentDelegate { + delegate: *mint_authority, + }, + ]; + let extension_types = extension_initialization_params + .iter() + .map(|e| e.extension()) + .collect::>(); + let space = ExtensionType::try_calculate_account_len::(&extension_types).unwrap(); + let mint_rent = self.rent.minimum_balance(space); + + let mut instructions = vec![system_instruction::create_account( + &self.context.payer.pubkey(), + &mint_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + )]; + + for params in extension_initialization_params { + instructions.push( + params + .instruction(&spl_token_2022::id(), &mint_keypair.pubkey()) + .unwrap(), + ); + } + + instructions.push( + spl_token_2022::instruction::initialize_mint( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + mint_authority, + freeze_authority, + 0, + ) + .unwrap(), + ); + + self.process_transaction(&instructions, Some(&[mint_keypair])) + .await + .unwrap(); + } + + pub async fn create_mint_2022_transfer_fee( + &mut self, + mint_keypair: &Keypair, + mint_authority: &Pubkey, + freeze_authority: Option<&Pubkey>, + ) { + let TransferFeeConfigWithKeypairs { + transfer_fee_config_authority, + withdraw_withheld_authority, + transfer_fee_config, + .. + } = test_transfer_fee_config_with_keypairs(); + let transfer_fee_basis_points = u16::from( + transfer_fee_config + .newer_transfer_fee + .transfer_fee_basis_points, + ); + let maximum_fee = u64::from(transfer_fee_config.newer_transfer_fee.maximum_fee); + let extension_initialization_params = + vec![ExtensionInitializationParams::TransferFeeConfig { + transfer_fee_config_authority: transfer_fee_config_authority.pubkey().into(), + withdraw_withheld_authority: withdraw_withheld_authority.pubkey().into(), + transfer_fee_basis_points, + maximum_fee, + }]; + let extension_types = extension_initialization_params + .iter() + .map(|e| e.extension()) + .collect::>(); + let space = ExtensionType::try_calculate_account_len::(&extension_types).unwrap(); + let mint_rent = self.rent.minimum_balance(space); + + let mut instructions = vec![system_instruction::create_account( + &self.context.payer.pubkey(), + &mint_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + )]; + + for params in extension_initialization_params { + instructions.push( + params + .instruction(&spl_token_2022::id(), &mint_keypair.pubkey()) + .unwrap(), + ); + } + instructions.push( + spl_token_2022::instruction::initialize_mint( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + mint_authority, + freeze_authority, + 0, + ) + .unwrap(), + ); + self.process_transaction(&instructions, Some(&[mint_keypair])) + .await + .unwrap(); + } + + pub async fn create_mint_2022_transfer_hook( + &mut self, + mint_keypair: &Keypair, + mint_authority: &Pubkey, + program_id: &Pubkey, + freeze_authority: Option<&Pubkey>, + ) { + let extension_initialization_params = vec![ExtensionInitializationParams::TransferHook { + authority: Some(*mint_authority), + program_id: Some(*program_id), + }]; + + let extension_types = extension_initialization_params + .iter() + .map(|e| e.extension()) + .collect::>(); + let space = ExtensionType::try_calculate_account_len::(&extension_types).unwrap(); + let mint_rent = self.rent.minimum_balance(space); + + let mut instructions = vec![system_instruction::create_account( + &self.context.payer.pubkey(), + &mint_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + )]; + + for params in extension_initialization_params { + instructions.push( + params + .instruction(&spl_token_2022::id(), &mint_keypair.pubkey()) + .unwrap(), + ); + } + instructions.push( + spl_token_2022::instruction::initialize_mint( + &spl_token_2022::id(), + &mint_keypair.pubkey(), + mint_authority, + freeze_authority, + 0, + ) + .unwrap(), + ); + self.process_transaction(&instructions, Some(&[mint_keypair])) + .await + .unwrap(); + } + + pub async fn initialize_transfer_hook_account_metas( + &mut self, + mint_address: &Pubkey, + mint_authority: &Keypair, + program_id: &Pubkey, + source: &Pubkey, + destination: &Pubkey, + writable_pubkey: &Pubkey, + amount: u64, + ) -> Vec { + let extra_account_metas_address = + get_extra_account_metas_address(&mint_address, &program_id); + + let init_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority.pubkey(), false, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let extra_pda_1 = Pubkey::find_program_address( + &[ + b"seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &program_id, + ) + .0; + + let extra_account_metas = [ + AccountMeta::new(extra_account_metas_address, false), + AccountMeta::new(*program_id, false), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority.pubkey(), false), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(*writable_pubkey, false), + ]; + + let rent = self.context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent.minimum_balance( + ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap(), + ); + + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &self.context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority.pubkey(), + &init_extra_account_metas, + ), + ], + Some(&self.context.payer.pubkey()), + &[&self.context.payer, &mint_authority], + self.context.last_blockhash, + ); + + self.context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + extra_account_metas.to_vec() + } + + pub async fn update_transfer_hook_account_metas( + &mut self, + mint_address: &Pubkey, + mint_authority: &Keypair, + program_id: &Pubkey, + source: &Pubkey, + destination: &Pubkey, + updated_writable_pubkey: &Pubkey, + amount: u64, + ) -> Vec { + let extra_account_metas_address = + get_extra_account_metas_address(&mint_address, &program_id); + + let updated_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority.pubkey(), false, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"updated-seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&updated_writable_pubkey, false, true).unwrap(), + ]; + + let extra_pda_1 = Pubkey::find_program_address( + &[ + b"updated-seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &program_id, + ) + .0; + + let extra_account_metas = [ + AccountMeta::new(extra_account_metas_address, false), + AccountMeta::new(*program_id, false), + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority.pubkey(), false), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(*updated_writable_pubkey, false), + ]; + + let rent = self.context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent.minimum_balance( + ExtraAccountMetaList::size_of(updated_extra_account_metas.len()).unwrap(), + ); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &self.context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + update_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority.pubkey(), + &updated_extra_account_metas, + ), + ], + Some(&self.context.payer.pubkey()), + &[&self.context.payer, &mint_authority], + self.context.last_blockhash, + ); + + self.context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + extra_account_metas.to_vec() + } /// Sets spl-token program account (Mint or TokenAccount) authority pub async fn set_spl_token_account_authority( &mut self, @@ -172,6 +585,29 @@ impl ProgramTestBench { .unwrap(); } + /// Sets spl-token-2022 program account (Mint or TokenAccount) authority + pub async fn set_spl_token_2022_account_authority( + &mut self, + account: &Pubkey, + account_authority: &Keypair, + new_authority: Option<&Pubkey>, + authority_type: spl_token_2022::instruction::AuthorityType, + ) { + let set_authority_ix = spl_token_2022::instruction::set_authority( + &spl_token_2022::id(), + account, + new_authority, + authority_type, + &account_authority.pubkey(), + &[], + ) + .unwrap(); + + self.process_transaction(&[set_authority_ix], Some(&[account_authority])) + .await + .unwrap(); + } + #[allow(dead_code)] pub async fn create_empty_token_account( &mut self, @@ -204,6 +640,38 @@ impl ProgramTestBench { .unwrap(); } + #[allow(dead_code)] + pub async fn create_empty_token_2022_account( + &mut self, + token_account_keypair: &Keypair, + token_mint: &Pubkey, + owner: &Pubkey, + ) { + let create_account_instruction = system_instruction::create_account( + &self.context.payer.pubkey(), + &token_account_keypair.pubkey(), + self.rent + .minimum_balance(spl_token_2022::state::Account::get_packed_len()), + spl_token_2022::state::Account::get_packed_len() as u64, + &spl_token_2022::id(), + ); + + let initialize_account_instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + token_mint, + owner, + ) + .unwrap(); + + self.process_transaction( + &[create_account_instruction, initialize_account_instruction], + Some(&[token_account_keypair]), + ) + .await + .unwrap(); + } + #[allow(dead_code)] pub async fn with_token_account( &mut self, @@ -230,6 +698,32 @@ impl ProgramTestBench { } } + #[allow(dead_code)] + pub async fn with_token_2022_account( + &mut self, + token_mint: &Pubkey, + owner: &Pubkey, + token_mint_authority: &Keypair, + amount: u64, + ) -> TokenAccountCookie { + let token_account_keypair = Keypair::new(); + + self.create_empty_token_2022_account(&token_account_keypair, token_mint, owner) + .await; + + self.mint_2022_tokens( + token_mint, + token_mint_authority, + &token_account_keypair.pubkey(), + amount, + ) + .await; + + TokenAccountCookie { + address: token_account_keypair.pubkey(), + } + } + pub async fn transfer_sol(&mut self, to_account: &Pubkey, lamports: u64) { let transfer_ix = system_instruction::transfer(&self.payer.pubkey(), to_account, lamports); @@ -260,6 +754,28 @@ impl ProgramTestBench { .unwrap(); } + pub async fn mint_2022_tokens( + &mut self, + token_mint: &Pubkey, + token_mint_authority: &Keypair, + token_account: &Pubkey, + amount: u64, + ) { + let mint_instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), + token_mint, + token_account, + &token_mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + self.process_transaction(&[mint_instruction], Some(&[token_mint_authority])) + .await + .unwrap(); + } + #[allow(dead_code)] pub async fn create_token_account_with_transfer_authority( &mut self, @@ -320,6 +836,203 @@ impl ProgramTestBench { .unwrap(); } + #[allow(dead_code)] + pub async fn create_token_2022_account_with_transfer_authority( + &mut self, + token_account_keypair: &Keypair, + token_mint: &Pubkey, + token_mint_authority: &Keypair, + amount: u64, + owner: &Keypair, + transfer_authority: &Pubkey, + ) { + let create_account_instruction = system_instruction::create_account( + &self.context.payer.pubkey(), + &token_account_keypair.pubkey(), + self.rent + .minimum_balance(spl_token_2022::state::Account::get_packed_len()), + spl_token_2022::state::Account::get_packed_len() as u64, + &spl_token_2022::id(), + ); + + let initialize_account_instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + token_mint, + &owner.pubkey(), + ) + .unwrap(); + + let mint_instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), + token_mint, + &token_account_keypair.pubkey(), + &token_mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let approve_instruction = spl_token_2022::instruction::approve( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + transfer_authority, + &owner.pubkey(), + &[], + amount, + ) + .unwrap(); + + self.process_transaction( + &[ + create_account_instruction, + initialize_account_instruction, + mint_instruction, + approve_instruction, + ], + Some(&[token_account_keypair, token_mint_authority, owner]), + ) + .await + .unwrap(); + } + + #[allow(dead_code)] + pub async fn create_token_2022_account_with_transfer_authority_with_transfer_fees( + &mut self, + token_account_keypair: &Keypair, + token_mint: &Pubkey, + token_mint_authority: &Keypair, + amount: u64, + owner: &Keypair, + transfer_authority: &Pubkey, + ) { + let space = ExtensionType::try_calculate_account_len::(&[ + spl_token_2022::extension::ExtensionType::TransferFeeConfig, + ]) + .unwrap(); + let mint_rent = self.rent.minimum_balance(space); + + let create_account_instruction = system_instruction::create_account( + &self.context.payer.pubkey(), + &token_account_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + ); + + let initialize_account_instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + token_mint, + &owner.pubkey(), + ) + .unwrap(); + + let mint_instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), + token_mint, + &token_account_keypair.pubkey(), + &token_mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let approve_instruction = spl_token_2022::instruction::approve( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + transfer_authority, + &owner.pubkey(), + &[], + amount, + ) + .unwrap(); + + self.process_transaction( + &[ + create_account_instruction, + initialize_account_instruction, + mint_instruction, + approve_instruction, + ], + Some(&[token_account_keypair, token_mint_authority, owner]), + ) + .await + .unwrap(); + } + + #[allow(dead_code)] + pub async fn create_token_2022_account_with_transfer_authority_with_transfer_hooks( + &mut self, + token_account_keypair: &Keypair, + token_mint: &Pubkey, + token_mint_authority: &Keypair, + amount: u64, + owner: &Keypair, + transfer_authority: &Pubkey, + program_id: &Pubkey, + ) { + let extension_initialization_params = vec![ExtensionInitializationParams::TransferHook { + authority: Some(token_mint_authority.pubkey()), + program_id: Some(*program_id), + }]; + + let extension_types = extension_initialization_params + .iter() + .map(|e| e.extension()) + .collect::>(); + let space = ExtensionType::try_calculate_account_len::(&extension_types).unwrap(); + let mint_rent = self.rent.minimum_balance(space); + + let create_account_instruction = system_instruction::create_account( + &self.context.payer.pubkey(), + &token_account_keypair.pubkey(), + mint_rent, + space as u64, + &spl_token_2022::id(), + ); + + let initialize_account_instruction = spl_token_2022::instruction::initialize_account( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + token_mint, + &owner.pubkey(), + ) + .unwrap(); + + let mint_instruction = spl_token_2022::instruction::mint_to( + &spl_token_2022::id(), + token_mint, + &token_account_keypair.pubkey(), + &token_mint_authority.pubkey(), + &[], + amount, + ) + .unwrap(); + + let approve_instruction = spl_token_2022::instruction::approve( + &spl_token_2022::id(), + &token_account_keypair.pubkey(), + transfer_authority, + &owner.pubkey(), + &[], + amount, + ) + .unwrap(); + + self.process_transaction( + &[ + create_account_instruction, + initialize_account_instruction, + mint_instruction, + approve_instruction, + ], + Some(&[token_account_keypair, token_mint_authority, owner]), + ) + .await + .unwrap(); + } + #[allow(dead_code)] pub async fn get_clock(&mut self) -> Clock { self.get_bincode_account::(&sysvar::clock::id()) @@ -370,6 +1083,15 @@ impl ProgramTestBench { self.context.set_account(address, &data); } + /// Removes an account by setting its data to empty and owner to system + /// subverting normal runtime checks + pub fn remove_account(&mut self, address: &Pubkey) { + let data = + AccountSharedData::create(0, vec![], system_program::id(), false, Epoch::default()); + + self.context.set_account(address, &data); + } + #[allow(dead_code)] pub async fn get_account(&mut self, address: &Pubkey) -> Option { self.context diff --git a/governance/test-sdk/src/mpl_core_tools.rs b/governance/test-sdk/src/mpl_core_tools.rs new file mode 100644 index 000000000..1e1e025e2 --- /dev/null +++ b/governance/test-sdk/src/mpl_core_tools.rs @@ -0,0 +1,353 @@ +pub use mpl_core::types::UpdateAuthority; +use mpl_core::{ + instructions::{CreateCollectionV2Builder, CreateV2Builder}, + types::{ + DataState, ExternalPluginAdapter, ExternalPluginAdapterInitInfo, Key, Plugin, + PluginAuthorityPair, + }, + Asset, Collection, +}; +use solana_program_test::{BanksClientError, ProgramTestContext}; +use solana_sdk::{ + instruction::Instruction, pubkey::Pubkey, signer::Signer, system_instruction, system_program, + transaction::Transaction, +}; + +pub fn program_id() -> Pubkey { + mpl_core::ID +} + +const DEFAULT_ASSET_NAME: &str = "Test Asset"; +const DEFAULT_ASSET_URI: &str = "https://example.com/asset"; +const DEFAULT_COLLECTION_NAME: &str = "Test Collection"; +const DEFAULT_COLLECTION_URI: &str = "https://example.com/collection"; + +#[derive(Debug)] +pub struct CreateAssetHelperArgs<'a> { + pub owner: Option, + pub payer: Option<&'a Pubkey>, + pub asset: &'a Pubkey, + pub data_state: Option, + pub name: Option, + pub uri: Option, + pub authority: Option, + pub update_authority: Option, + pub collection: Option, + // TODO use PluginList type here + pub plugins: Vec, + pub external_plugin_adapters: Vec, +} + +pub fn create_asset<'a>(input: CreateAssetHelperArgs<'a>, payer: Pubkey) -> Instruction { + CreateV2Builder::new() + .asset(*input.asset) + .collection(input.collection) + .authority(input.authority) + .payer(payer) + .owner(Some(input.owner.unwrap_or(payer))) + .update_authority(input.update_authority) + .system_program(system_program::ID) + .data_state(input.data_state.unwrap_or(DataState::AccountState)) + .name(input.name.unwrap_or(DEFAULT_ASSET_NAME.to_owned())) + .uri(input.uri.unwrap_or(DEFAULT_ASSET_URI.to_owned())) + .plugins(input.plugins) + .external_plugin_adapters(input.external_plugin_adapters) + .instruction() +} + +pub struct AssertAssetHelperArgs { + pub asset: Pubkey, + pub owner: Pubkey, + pub update_authority: Option, + pub name: Option, + pub uri: Option, + pub plugins: Vec, + pub external_plugin_adapters: Vec, +} + +pub async fn assert_asset(context: &mut ProgramTestContext, input: AssertAssetHelperArgs) { + let asset_account = context + .banks_client + .get_account(input.asset) + .await + .expect("get_account") + .expect("asset account not found"); + + let asset = Asset::from_bytes(&asset_account.data).unwrap(); + assert_eq!(asset.base.key, Key::AssetV1); + assert_eq!(asset.base.owner, input.owner); + if let Some(update_authority) = input.update_authority { + assert_eq!(asset.base.update_authority, update_authority); + } + assert_eq!( + asset.base.name, + input.name.unwrap_or(DEFAULT_ASSET_NAME.to_owned()) + ); + assert_eq!( + asset.base.uri, + input.uri.unwrap_or(DEFAULT_ASSET_URI.to_owned()) + ); + + for plugin in input.plugins { + match plugin { + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(freeze), + authority, + } => { + let plugin = asset.plugin_list.freeze_delegate.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.freeze_delegate, freeze); + } + PluginAuthorityPair { + plugin: Plugin::Royalties(royalties), + authority, + } => { + let plugin = asset.plugin_list.royalties.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.royalties, royalties); + } + _ => panic!("unsupported plugin type"), + } + } + + assert_eq!( + input.external_plugin_adapters.len(), + asset.external_plugin_adapter_list.lifecycle_hooks.len() + + asset.external_plugin_adapter_list.oracles.len() + + asset.external_plugin_adapter_list.app_data.len() + ); + for plugin in input.external_plugin_adapters { + match plugin { + ExternalPluginAdapter::LifecycleHook(hook) => { + assert!(asset + .external_plugin_adapter_list + .lifecycle_hooks + .iter() + .any(|lifecyle_hook_with_data| lifecyle_hook_with_data.base == hook)) + } + ExternalPluginAdapter::Oracle(oracle) => { + assert!(asset.external_plugin_adapter_list.oracles.contains(&oracle)) + } + ExternalPluginAdapter::AppData(app_data) => { + assert!(asset + .external_plugin_adapter_list + .app_data + .iter() + .any(|app_data_with_data| app_data_with_data.base == app_data)) + } + ExternalPluginAdapter::LinkedLifecycleHook(hook) => { + assert!(asset + .external_plugin_adapter_list + .linked_lifecycle_hooks + .contains(&hook)) + } + ExternalPluginAdapter::LinkedAppData(app_data) => { + assert!(asset + .external_plugin_adapter_list + .linked_app_data + .contains(&app_data)) + } + ExternalPluginAdapter::DataSection(data) => { + assert!(asset + .external_plugin_adapter_list + .data_sections + .iter() + .any(|data_sections_with_data| data_sections_with_data.base == data)) + } + } + } +} + +#[derive(Debug)] +pub struct CreateCollectionHelperArgs<'a> { + pub collection: &'a Pubkey, + pub update_authority: Option, + pub payer: Option<&'a Pubkey>, + pub name: Option, + pub uri: Option, + pub plugins: Vec, + pub external_plugin_adapters: Vec, +} + +pub fn create_collection<'a>(input: CreateCollectionHelperArgs<'a>, payer: Pubkey) -> Instruction { + CreateCollectionV2Builder::new() + .collection(*input.collection) + .update_authority(input.update_authority) + .payer(payer) + .system_program(system_program::ID) + .name(input.name.unwrap_or(DEFAULT_COLLECTION_NAME.to_owned())) + .uri(input.uri.unwrap_or(DEFAULT_COLLECTION_URI.to_owned())) + .plugins(input.plugins) + .external_plugin_adapters(input.external_plugin_adapters) + .instruction() +} + +pub struct AssertCollectionHelperArgs { + pub collection: Pubkey, + pub update_authority: Pubkey, + pub name: Option, + pub uri: Option, + pub num_minted: u32, + pub current_size: u32, + // TODO use PluginList type here + pub plugins: Vec, + pub external_plugin_adapters: Vec, +} + +pub async fn assert_collection( + context: &mut ProgramTestContext, + input: AssertCollectionHelperArgs, +) { + let collection_account = context + .banks_client + .get_account(input.collection) + .await + .expect("get_account") + .expect("collection account not found"); + + let collection = Collection::from_bytes(&collection_account.data).unwrap(); + assert_eq!(collection.base.key, Key::CollectionV1); + assert_eq!(collection.base.update_authority, input.update_authority); + assert_eq!( + collection.base.name, + input.name.unwrap_or(DEFAULT_COLLECTION_NAME.to_owned()) + ); + assert_eq!( + collection.base.uri, + input.uri.unwrap_or(DEFAULT_COLLECTION_URI.to_owned()) + ); + assert_eq!(collection.base.num_minted, input.num_minted); + assert_eq!(collection.base.current_size, input.current_size); + + for plugin in input.plugins { + match plugin { + PluginAuthorityPair { + plugin: Plugin::FreezeDelegate(freeze), + authority, + } => { + let plugin = collection.plugin_list.freeze_delegate.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.freeze_delegate, freeze); + } + PluginAuthorityPair { + plugin: Plugin::Royalties(royalties), + authority, + } => { + let plugin = collection.plugin_list.royalties.clone().unwrap(); + if let Some(authority) = authority { + assert_eq!(plugin.base.authority, authority.into()); + } + assert_eq!(plugin.royalties, royalties); + } + _ => panic!("unsupported plugin type"), + } + } + + assert_eq!( + input.external_plugin_adapters.len(), + collection + .external_plugin_adapter_list + .lifecycle_hooks + .len() + + collection.external_plugin_adapter_list.oracles.len() + + collection.external_plugin_adapter_list.app_data.len() + ); + for plugin in input.external_plugin_adapters { + match plugin { + ExternalPluginAdapter::LifecycleHook(hook) => { + assert!(collection + .external_plugin_adapter_list + .lifecycle_hooks + .iter() + .any(|lifecyle_hook_with_data| lifecyle_hook_with_data.base == hook)) + } + ExternalPluginAdapter::Oracle(oracle) => { + assert!(collection + .external_plugin_adapter_list + .oracles + .contains(&oracle)) + } + ExternalPluginAdapter::AppData(app_data) => { + assert!(collection + .external_plugin_adapter_list + .app_data + .iter() + .any(|app_data_with_data| app_data_with_data.base == app_data)) + } + ExternalPluginAdapter::LinkedLifecycleHook(hook) => { + assert!(collection + .external_plugin_adapter_list + .linked_lifecycle_hooks + .contains(&hook)) + } + ExternalPluginAdapter::LinkedAppData(app_data) => { + assert!(collection + .external_plugin_adapter_list + .linked_app_data + .contains(&app_data)) + } + ExternalPluginAdapter::DataSection(data) => { + assert!(collection + .external_plugin_adapter_list + .data_sections + .iter() + .any(|data_sections_with_data| data_sections_with_data.base == data)) + } + } + } +} + +pub async fn airdrop( + context: &mut ProgramTestContext, + receiver: &Pubkey, + amount: u64, +) -> Result<(), BanksClientError> { + let tx = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &context.payer.pubkey(), + receiver, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + Ok(()) +} + +#[macro_export] +macro_rules! assert_custom_instruction_error { + ($ix:expr, $error:expr, $matcher:pat) => { + match $error { + solana_program_test::BanksClientError::TransactionError( + solana_sdk::transaction::TransactionError::InstructionError( + $ix, + solana_sdk::instruction::InstructionError::Custom(x), + ), + ) => match num_traits::FromPrimitive::from_i32(x as i32) { + Some($matcher) => assert!(true), + Some(other) => { + assert!( + false, + "Expected another custom instruction error than '{:#?}'", + other + ) + } + None => assert!(false, "Expected custom instruction error"), + }, + err => assert!( + false, + "Expected custom instruction error but got '{:#?}'", + err + ), + }; + }; +} diff --git a/governance/test-sdk/src/token_2022.rs b/governance/test-sdk/src/token_2022.rs new file mode 100644 index 000000000..70b20e01d --- /dev/null +++ b/governance/test-sdk/src/token_2022.rs @@ -0,0 +1,46 @@ +use { + solana_sdk::{ + program_option::COption, + signature::{Keypair, Signer}, + }, + spl_token_2022::extension::transfer_fee::{TransferFee, TransferFeeConfig}, +}; + +pub struct TransferFeeConfigWithKeypairs { + pub transfer_fee_config: TransferFeeConfig, + pub transfer_fee_config_authority: Keypair, + pub withdraw_withheld_authority: Keypair, +} + +const TEST_MAXIMUM_FEE: u64 = 10_000_000; +const TEST_FEE_BASIS_POINTS: u16 = 250; + +fn test_transfer_fee() -> TransferFee { + TransferFee { + epoch: 0.into(), + transfer_fee_basis_points: TEST_FEE_BASIS_POINTS.into(), + maximum_fee: TEST_MAXIMUM_FEE.into(), + } +} + +pub fn test_transfer_fee_config_with_keypairs() -> TransferFeeConfigWithKeypairs { + let transfer_fee = test_transfer_fee(); + let transfer_fee_config_authority = Keypair::new(); + let withdraw_withheld_authority = Keypair::new(); + let transfer_fee_config = TransferFeeConfig { + transfer_fee_config_authority: COption::Some(transfer_fee_config_authority.pubkey()) + .try_into() + .unwrap(), + withdraw_withheld_authority: COption::Some(withdraw_withheld_authority.pubkey()) + .try_into() + .unwrap(), + withheld_amount: 0.into(), + older_transfer_fee: transfer_fee, + newer_transfer_fee: transfer_fee, + }; + TransferFeeConfigWithKeypairs { + transfer_fee_config, + transfer_fee_config_authority, + withdraw_withheld_authority, + } +} \ No newline at end of file diff --git a/governance/test-sdk/src/tools.rs b/governance/test-sdk/src/tools.rs index 8705f8fb9..a1acb30a6 100644 --- a/governance/test-sdk/src/tools.rs +++ b/governance/test-sdk/src/tools.rs @@ -29,20 +29,30 @@ pub fn map_transaction_error(transport_error: TransportError) -> ProgramError { TransportError::TransactionError(TransactionError::InstructionError( _, instruction_error, - )) => ProgramError::try_from(instruction_error).unwrap_or_else(|ie| match ie { + )) => match instruction_error { + // In solana-sdk v1.19.0, there is a ProgramError for + // InstructionError::IncorrectAuthority. This results in the error mapping + // returning two different values: one for sdk < v1.19 and another for sdk >= v1.19.0. + // To avoid this situation, handle InstructionError::IncorrectAuthority earlier. + // Can be removed when Solana v1.19.0 becomes a stable channel (also need to update the + // test assert for + // `test_create_program_governance_with_incorrect_upgrade_authority_error`) InstructionError::IncorrectAuthority => { ProgramInstructionError::IncorrectAuthority.into() } - InstructionError::PrivilegeEscalation => { - ProgramInstructionError::PrivilegeEscalation.into() - } - _ => panic!("TEST-INSTRUCTION-ERROR {:?}", ie), - }), - + _ => ProgramError::try_from(instruction_error).unwrap_or_else(|ie| match ie { + InstructionError::IncorrectAuthority => unreachable!(), + InstructionError::PrivilegeEscalation => { + ProgramInstructionError::PrivilegeEscalation.into() + } + _ => panic!("TEST-INSTRUCTION-ERROR {:?}", ie), + }), + }, _ => panic!("TEST-TRANSPORT-ERROR: {:?}", transport_error), } } + pub fn clone_keypair(source: &Keypair) -> Keypair { Keypair::from_bytes(&source.to_bytes()).unwrap() } diff --git a/governance/test-sdk/src/versioned_transaction/compiled_keys.rs b/governance/test-sdk/src/versioned_transaction/compiled_keys.rs new file mode 100644 index 000000000..10e643a12 --- /dev/null +++ b/governance/test-sdk/src/versioned_transaction/compiled_keys.rs @@ -0,0 +1,174 @@ +use std::collections::BTreeMap; + +use solana_sdk::address_lookup_table::AddressLookupTableAccount; +use solana_program::instruction::Instruction; +use solana_program::message::v0::{LoadedAddresses, MessageAddressTableLookup}; +use solana_program::message::{CompileError, MessageHeader}; + +use solana_program::pubkey::Pubkey; + +/// A helper struct to collect pubkeys compiled for a set of instructions +/// +/// NOTE: The only difference between this and the original implementation from `solana_program` is that we don't mark the instruction programIds as invoked. +/// It makes sense to do because the instructions will be called via CPI, so the programIds can come from Address Lookup Tables. +/// This allows to compress the message size and avoid hitting the tx size limit during `process_insert_proposal_versioned_transaction_from_buffer` instruction calls. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct CompiledKeys { + payer: Option, + key_meta_map: BTreeMap, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +struct CompiledKeyMeta { + is_signer: bool, + is_writable: bool, + is_invoked: bool, +} + +impl CompiledKeys { + /// Compiles the pubkeys referenced by a list of instructions and organizes by + /// signer/non-signer and writable/readonly. + pub fn compile(instructions: &[Instruction], payer: Option) -> Self { + let mut key_meta_map = BTreeMap::::new(); + for ix in instructions { + let meta = key_meta_map.entry(ix.program_id).or_default(); + // NOTE: This is the only difference from the original. + // meta.is_invoked = true; + meta.is_invoked = false; + for account_meta in &ix.accounts { + let meta = key_meta_map.entry(account_meta.pubkey).or_default(); + meta.is_signer |= account_meta.is_signer; + meta.is_writable |= account_meta.is_writable; + } + } + if let Some(payer) = &payer { + let meta = key_meta_map.entry(*payer).or_default(); + meta.is_signer = true; + meta.is_writable = true; + } + Self { + payer, + key_meta_map, + } + } + + pub fn try_into_message_components( + self, + ) -> Result<(MessageHeader, Vec), CompileError> { + let try_into_u8 = |num: usize| -> Result { + u8::try_from(num).map_err(|_| CompileError::AccountIndexOverflow) + }; + + let Self { + payer, + mut key_meta_map, + } = self; + + if let Some(payer) = &payer { + key_meta_map.remove_entry(payer); + } + + let writable_signer_keys: Vec = payer + .into_iter() + .chain( + key_meta_map + .iter() + .filter_map(|(key, meta)| (meta.is_signer && meta.is_writable).then_some(*key)), + ) + .collect(); + let readonly_signer_keys: Vec = key_meta_map + .iter() + .filter_map(|(key, meta)| (meta.is_signer && !meta.is_writable).then_some(*key)) + .collect(); + let writable_non_signer_keys: Vec = key_meta_map + .iter() + .filter_map(|(key, meta)| (!meta.is_signer && meta.is_writable).then_some(*key)) + .collect(); + let readonly_non_signer_keys: Vec = key_meta_map + .iter() + .filter_map(|(key, meta)| (!meta.is_signer && !meta.is_writable).then_some(*key)) + .collect(); + + let signers_len = writable_signer_keys + .len() + .saturating_add(readonly_signer_keys.len()); + + let header = MessageHeader { + num_required_signatures: try_into_u8(signers_len)?, + num_readonly_signed_accounts: try_into_u8(readonly_signer_keys.len())?, + num_readonly_unsigned_accounts: try_into_u8(readonly_non_signer_keys.len())?, + }; + + let static_account_keys = std::iter::empty() + .chain(writable_signer_keys) + .chain(readonly_signer_keys) + .chain(writable_non_signer_keys) + .chain(readonly_non_signer_keys) + .collect(); + + Ok((header, static_account_keys)) + } + + pub fn try_extract_table_lookup( + &mut self, + lookup_table_account: &AddressLookupTableAccount, + ) -> Result, CompileError> { + let (writable_indexes, drained_writable_keys) = self + .try_drain_keys_found_in_lookup_table(&lookup_table_account.addresses, |meta| { + !meta.is_signer && !meta.is_invoked && meta.is_writable + })?; + let (readonly_indexes, drained_readonly_keys) = self + .try_drain_keys_found_in_lookup_table(&lookup_table_account.addresses, |meta| { + !meta.is_signer && !meta.is_invoked && !meta.is_writable + })?; + + // Don't extract lookup if no keys were found + if writable_indexes.is_empty() && readonly_indexes.is_empty() { + return Ok(None); + } + + Ok(Some(( + MessageAddressTableLookup { + account_key: lookup_table_account.key, + writable_indexes, + readonly_indexes, + }, + LoadedAddresses { + writable: drained_writable_keys, + readonly: drained_readonly_keys, + }, + ))) + } + + fn try_drain_keys_found_in_lookup_table( + &mut self, + lookup_table_addresses: &[Pubkey], + key_meta_filter: impl Fn(&CompiledKeyMeta) -> bool, + ) -> Result<(Vec, Vec), CompileError> { + let mut lookup_table_indexes = Vec::new(); + let mut drained_keys = Vec::new(); + + for search_key in self + .key_meta_map + .iter() + .filter_map(|(key, meta)| key_meta_filter(meta).then_some(key)) + { + for (key_index, key) in lookup_table_addresses.iter().enumerate() { + if key == search_key { + let lookup_table_index = u8::try_from(key_index) + .map_err(|_| CompileError::AddressTableLookupIndexOverflow)?; + + lookup_table_indexes.push(lookup_table_index); + drained_keys.push(*search_key); + break; + } + } + } + + for key in &drained_keys { + self.key_meta_map.remove_entry(key); + } + + Ok((lookup_table_indexes, drained_keys)) + } +} diff --git a/governance/test-sdk/src/versioned_transaction/mod.rs b/governance/test-sdk/src/versioned_transaction/mod.rs new file mode 100644 index 000000000..10b43a436 --- /dev/null +++ b/governance/test-sdk/src/versioned_transaction/mod.rs @@ -0,0 +1,8 @@ + +pub mod pda; +pub mod compiled_keys; + +pub use { + pda::*, + compiled_keys::* +}; \ No newline at end of file diff --git a/governance/test-sdk/src/versioned_transaction/pda.rs b/governance/test-sdk/src/versioned_transaction/pda.rs new file mode 100644 index 000000000..e8f2cef95 --- /dev/null +++ b/governance/test-sdk/src/versioned_transaction/pda.rs @@ -0,0 +1,23 @@ +use solana_sdk::pubkey::Pubkey; + + +pub const EPHERMAL_SIGNER_SEED: &[u8] = b"ephemeral_signer"; +pub const VERSIONED_TRANSACTION_BUFFER_SEED: &[u8] = b"version_transaction"; + +pub fn get_ephemeral_signer_pda( + transaction_proposal: &Pubkey, + ephemeral_signer_index: u8, + program_id: &Pubkey, + transaction_index: u16, +) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[ + VERSIONED_TRANSACTION_BUFFER_SEED, + &transaction_proposal.to_bytes(), + EPHERMAL_SIGNER_SEED, + &transaction_index.to_le_bytes(), + &ephemeral_signer_index.to_le_bytes(), + ], + program_id, + ) +} diff --git a/governance/tools/Cargo.toml b/governance/tools/Cargo.toml index f53e2939e..aa9112ea7 100644 --- a/governance/tools/Cargo.toml +++ b/governance/tools/Cargo.toml @@ -16,5 +16,4 @@ num-traits = "0.2" serde = "1.0.195" serde_derive = "1.0.103" solana-program = { workspace = true } -spl-token = { version = "4.0", path = "../../token/program", features = [ "no-entrypoint" ] } thiserror = "1.0" diff --git a/governance/tools/src/account.rs b/governance/tools/src/account.rs index 321785130..d6c466451 100644 --- a/governance/tools/src/account.rs +++ b/governance/tools/src/account.rs @@ -143,7 +143,6 @@ pub fn create_and_serialize_account_with_owner_signed<'a, T: BorshSerialize + Ac let account_size = serialized_data.len(); (Some(serialized_data), account_size) }; - let mut signers_seeds = account_address_seeds.to_vec(); let bump = &[bump_seed]; signers_seeds.push(bump); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0193dee36..0d943e1a6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.83.0" +channel = "nightly-2024-11-19" diff --git a/token/program/Cargo.toml b/token/program/Cargo.toml index bd8a911a7..3951e29d9 100644 --- a/token/program/Cargo.toml +++ b/token/program/Cargo.toml @@ -33,3 +33,6 @@ crate-type = ["cdylib", "lib"] [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] + +[lints] +workspace = true \ No newline at end of file diff --git a/token/transfer-hook/example-old/Cargo.toml b/token/transfer-hook/example-old/Cargo.toml new file mode 100644 index 000000000..341bc89be --- /dev/null +++ b/token/transfer-hook/example-old/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "spl-transfer-hook-example" +version = "0.4.0" +description = "Solana Program Library Transfer Hook Example Program" +authors = ["Solana Labs Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2021" + +[features] +no-entrypoint = [] +test-sbf = [] + +[dependencies] +arrayref = "0.3.7" +solana-program = { workspace = true } +spl-tlv-account-resolution = { version = "0.5" , path = "../../../libraries/tlv-account-resolution" } +spl-token-2022 = { version = "1.0", path = "../../program-2022", features = ["no-entrypoint"] } +spl-transfer-hook-interface = { version = "0.4" , path = "../interface" } +spl-type-length-value = { version = "0.3" , path = "../../../libraries/type-length-value" } + +[dev-dependencies] +solana-program-test = { workspace = true } +solana-sdk = { workspace = true } + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/token/transfer-hook/example-old/README.md b/token/transfer-hook/example-old/README.md new file mode 100644 index 000000000..f3d2aef4a --- /dev/null +++ b/token/transfer-hook/example-old/README.md @@ -0,0 +1,66 @@ +## Transfer-Hook Example + +Full example program and tests implementing the `spl-transfer-hook-interface`, +to be used for testing a program that calls into the `spl-transfer-hook-interface`. + +See the +[SPL Transfer Hook Interface](https://github.com/solana-labs/solana-program-library/tree/master/token/transfer-hook/interface) +code for more information. + +### Example usage of example + +When testing your program that uses `spl-transfer-hook-interface`, you can also +import this crate, and then use it with `solana-program-test`, ie: + +```rust +use { + solana_program_test::{processor, ProgramTest}, + solana_sdk::{account::Account, instruction::AccountMeta}, + spl_transfer_hook_example::state::example_data, + spl_transfer_hook_interface::get_extra_account_metas_address, +}; + +#[test] +fn my_program_test() { + let mut program_test = ProgramTest::new( + "my_program", + my_program_id, + processor!(my_program_processor), + ); + + let transfer_hook_program_id = Pubkey::new_unique(); + program_test.prefer_bpf(false); // BPF won't work, unless you've built this from scratch! + program_test.add_program( + "spl_transfer_hook_example", + transfer_hook_program_id, + processor!(spl_transfer_hook_example::processor::process), + ); + + let mint = Pubkey::new_unique(); + let extra_accounts_address = get_extra_account_metas_address(&mint, &transfer_hook_program_id); + let account_metas = vec![ + AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: false, + is_writable: false, + }, + ]; + let data = example_data(&account_metas); + program_test.add_account( + extra_accounts_address, + Account { + lamports: 1_000_000_000, // a lot, just to be safe + data, + owner: transfer_hook_program_id, + ..Account::default() + }, + ); + + // run your test logic! +} +``` diff --git a/token/transfer-hook/example-old/src/entrypoint.rs b/token/transfer-hook/example-old/src/entrypoint.rs new file mode 100644 index 000000000..b20460372 --- /dev/null +++ b/token/transfer-hook/example-old/src/entrypoint.rs @@ -0,0 +1,24 @@ +//! Program entrypoint + +use { + crate::processor, + solana_program::{ + account_info::AccountInfo, entrypoint::ProgramResult, program_error::PrintProgramError, + pubkey::Pubkey, + }, + spl_transfer_hook_interface::error::TransferHookError, +}; + +solana_program::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) { + // catch the error so we can print it + error.print::(); + return Err(error); + } + Ok(()) +} diff --git a/token/transfer-hook/example-old/src/lib.rs b/token/transfer-hook/example-old/src/lib.rs new file mode 100644 index 000000000..9db06d9c6 --- /dev/null +++ b/token/transfer-hook/example-old/src/lib.rs @@ -0,0 +1,18 @@ +//! Crate defining an example program for performing a hook on transfer, where +//! the token program calls into a separate program with additional accounts +//! after all other logic, to be sure that a transfer has accomplished all +//! required preconditions. + +#![allow(clippy::arithmetic_side_effects)] +#![deny(missing_docs)] +#![cfg_attr(not(test), forbid(unsafe_code))] + +pub mod processor; +pub mod state; + +#[cfg(not(feature = "no-entrypoint"))] +mod entrypoint; + +// Export current sdk types for downstream users building with a different sdk +// version +pub use solana_program; diff --git a/token/transfer-hook/example-old/src/processor.rs b/token/transfer-hook/example-old/src/processor.rs new file mode 100644 index 000000000..e398b2078 --- /dev/null +++ b/token/transfer-hook/example-old/src/processor.rs @@ -0,0 +1,221 @@ +//! Program state processor + +use { + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program::invoke_signed, + program_error::ProgramError, + pubkey::Pubkey, + system_instruction, + }, + spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}, + spl_token_2022::{ + extension::{ + transfer_hook::TransferHookAccount, BaseStateWithExtensions, StateWithExtensions, + }, + state::{Account, Mint}, + }, + spl_transfer_hook_interface::{ + collect_extra_account_metas_signer_seeds, + error::TransferHookError, + get_extra_account_metas_address, get_extra_account_metas_address_and_bump_seed, + instruction::{ExecuteInstruction, TransferHookInstruction}, + }, +}; + +fn check_token_account_is_transferring(account_info: &AccountInfo) -> Result<(), ProgramError> { + let account_data = account_info.try_borrow_data()?; + let token_account = StateWithExtensions::::unpack(&account_data)?; + let extension = token_account.get_extension::()?; + if bool::from(extension.transferring) { + Ok(()) + } else { + Err(TransferHookError::ProgramCalledOutsideOfTransfer.into()) + } +} + +/// Processes an [Execute](enum.TransferHookInstruction.html) instruction. +pub fn process_execute( + program_id: &Pubkey, + accounts: &[AccountInfo], + amount: u64, +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let source_account_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let destination_account_info = next_account_info(account_info_iter)?; + let _authority_info = next_account_info(account_info_iter)?; + let extra_account_metas_info = next_account_info(account_info_iter)?; + + // Check that the accounts are properly in "transferring" mode + check_token_account_is_transferring(source_account_info)?; + check_token_account_is_transferring(destination_account_info)?; + + // For the example program, we just check that the correct pda and validation + // pubkeys are provided + let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id); + if expected_validation_address != *extra_account_metas_info.key { + return Err(ProgramError::InvalidSeeds); + } + + let data = extra_account_metas_info.try_borrow_data()?; + + ExtraAccountMetaList::check_account_infos::( + accounts, + &TransferHookInstruction::Execute { amount }.pack(), + program_id, + &data, + )?; + + Ok(()) +} + +/// Processes a +/// [InitializeExtraAccountMetaList](enum.TransferHookInstruction.html) +/// instruction. +pub fn process_initialize_extra_account_meta_list( + program_id: &Pubkey, + accounts: &[AccountInfo], + extra_account_metas: &[ExtraAccountMeta], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let extra_account_metas_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let _system_program_info = next_account_info(account_info_iter)?; + + // check that the mint authority is valid without fully deserializing + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + let mint_authority = mint + .base + .mint_authority + .ok_or(TransferHookError::MintHasNoMintAuthority)?; + + // Check signers + if !authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if *authority_info.key != mint_authority { + return Err(TransferHookError::IncorrectMintAuthority.into()); + } + + // Check validation account + let (expected_validation_address, bump_seed) = + get_extra_account_metas_address_and_bump_seed(mint_info.key, program_id); + if expected_validation_address != *extra_account_metas_info.key { + return Err(ProgramError::InvalidSeeds); + } + + // Create the account + let bump_seed = [bump_seed]; + let signer_seeds = collect_extra_account_metas_signer_seeds(mint_info.key, &bump_seed); + let length = extra_account_metas.len(); + let account_size = ExtraAccountMetaList::size_of(length)?; + invoke_signed( + &system_instruction::allocate(extra_account_metas_info.key, account_size as u64), + &[extra_account_metas_info.clone()], + &[&signer_seeds], + )?; + invoke_signed( + &system_instruction::assign(extra_account_metas_info.key, program_id), + &[extra_account_metas_info.clone()], + &[&signer_seeds], + )?; + + // Write the data + let mut data = extra_account_metas_info.try_borrow_mut_data()?; + ExtraAccountMetaList::init::(&mut data, extra_account_metas)?; + + Ok(()) +} + +/// Processes a +/// [UpdateExtraAccountMetaList](enum.TransferHookInstruction.html) +/// instruction. +pub fn process_update_extra_account_meta_list( + program_id: &Pubkey, + accounts: &[AccountInfo], + extra_account_metas: &[ExtraAccountMeta], +) -> ProgramResult { + let account_info_iter = &mut accounts.iter(); + + let extra_account_metas_info = next_account_info(account_info_iter)?; + let mint_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + + // check that the mint authority is valid without fully deserializing + let mint_data = mint_info.try_borrow_data()?; + let mint = StateWithExtensions::::unpack(&mint_data)?; + let mint_authority = mint + .base + .mint_authority + .ok_or(TransferHookError::MintHasNoMintAuthority)?; + + // Check signers + if !authority_info.is_signer { + return Err(ProgramError::MissingRequiredSignature); + } + if *authority_info.key != mint_authority { + return Err(TransferHookError::IncorrectMintAuthority.into()); + } + + // Check validation account + let expected_validation_address = get_extra_account_metas_address(mint_info.key, program_id); + if expected_validation_address != *extra_account_metas_info.key { + return Err(ProgramError::InvalidSeeds); + } + + // Check if the extra metas have been initialized + let min_account_size = ExtraAccountMetaList::size_of(0)?; + let original_account_size = extra_account_metas_info.data_len(); + if program_id != extra_account_metas_info.owner || original_account_size < min_account_size { + return Err(ProgramError::UninitializedAccount); + } + + // If the new extra_account_metas length is different, resize the account and + // update + let length = extra_account_metas.len(); + let account_size = ExtraAccountMetaList::size_of(length)?; + if account_size >= original_account_size { + extra_account_metas_info.realloc(account_size, false)?; + let mut data = extra_account_metas_info.try_borrow_mut_data()?; + ExtraAccountMetaList::update::(&mut data, extra_account_metas)?; + } else { + { + let mut data = extra_account_metas_info.try_borrow_mut_data()?; + ExtraAccountMetaList::update::(&mut data, extra_account_metas)?; + } + extra_account_metas_info.realloc(account_size, false)?; + } + + Ok(()) +} + +/// Processes an [Instruction](enum.Instruction.html). +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { + let instruction = TransferHookInstruction::unpack(input)?; + + match instruction { + TransferHookInstruction::Execute { amount } => { + msg!("Instruction: Execute"); + process_execute(program_id, accounts, amount) + } + TransferHookInstruction::InitializeExtraAccountMetaList { + extra_account_metas, + } => { + msg!("Instruction: InitializeExtraAccountMetaList"); + process_initialize_extra_account_meta_list(program_id, accounts, &extra_account_metas) + } + TransferHookInstruction::UpdateExtraAccountMetaList { + extra_account_metas, + } => { + msg!("Instruction: UpdateExtraAccountMetaList"); + process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas) + } + } +} diff --git a/token/transfer-hook/example-old/src/state.rs b/token/transfer-hook/example-old/src/state.rs new file mode 100644 index 000000000..b2c962c50 --- /dev/null +++ b/token/transfer-hook/example-old/src/state.rs @@ -0,0 +1,15 @@ +//! State helpers for working with the example program + +use { + solana_program::program_error::ProgramError, + spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList}, + spl_transfer_hook_interface::instruction::ExecuteInstruction, +}; + +/// Generate example data to be used directly in an account for testing +pub fn example_data(account_metas: &[ExtraAccountMeta]) -> Result, ProgramError> { + let account_size = ExtraAccountMetaList::size_of(account_metas.len())?; + let mut data = vec![0; account_size]; + ExtraAccountMetaList::init::(&mut data, account_metas)?; + Ok(data) +} diff --git a/token/transfer-hook/example-old/tests/functional.rs b/token/transfer-hook/example-old/tests/functional.rs new file mode 100644 index 000000000..4e078c1fd --- /dev/null +++ b/token/transfer-hook/example-old/tests/functional.rs @@ -0,0 +1,1398 @@ +// Mark this test as SBF-only due to current `ProgramTest` limitations when +// CPIing into the system program +#![cfg(feature = "test-sbf")] + +use { + solana_program_test::{processor, tokio, ProgramTest}, + solana_sdk::{ + account::Account as SolanaAccount, + account_info::AccountInfo, + entrypoint::ProgramResult, + instruction::{AccountMeta, InstructionError}, + program_error::ProgramError, + program_option::COption, + pubkey::Pubkey, + signature::Signer, + signer::keypair::Keypair, + system_instruction, sysvar, + transaction::{Transaction, TransactionError}, + }, + spl_tlv_account_resolution::{ + account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed, + state::ExtraAccountMetaList, + }, + spl_token_2022::{ + extension::{transfer_hook::TransferHookAccount, ExtensionType, StateWithExtensionsMut}, + state::{Account, AccountState, Mint}, + }, + spl_transfer_hook_interface::{ + error::TransferHookError, + get_extra_account_metas_address, + instruction::{ + execute_with_extra_account_metas, initialize_extra_account_meta_list, + update_extra_account_meta_list, + }, + onchain, + }, +}; + +fn setup(program_id: &Pubkey) -> ProgramTest { + let mut program_test = ProgramTest::new( + "spl_transfer_hook_example", + *program_id, + processor!(spl_transfer_hook_example::processor::process), + ); + + program_test.prefer_bpf(false); // simplicity in the build + + program_test.add_program( + "spl_token_2022", + spl_token_2022::id(), + processor!(spl_token_2022::processor::Processor::process), + ); + + program_test +} + +#[allow(clippy::too_many_arguments)] +fn setup_token_accounts( + program_test: &mut ProgramTest, + program_id: &Pubkey, + mint_address: &Pubkey, + mint_authority: &Pubkey, + source: &Pubkey, + destination: &Pubkey, + owner: &Pubkey, + decimals: u8, + transferring: bool, +) { + // add mint, source, and destination accounts by hand to always force + // the "transferring" flag to true + let mint_size = ExtensionType::try_calculate_account_len::(&[]).unwrap(); + let mut mint_data = vec![0; mint_size]; + let mut state = StateWithExtensionsMut::::unpack_uninitialized(&mut mint_data).unwrap(); + let token_amount = 1_000_000_000_000; + state.base = Mint { + mint_authority: COption::Some(*mint_authority), + supply: token_amount, + decimals, + is_initialized: true, + freeze_authority: COption::None, + }; + state.pack_base(); + program_test.add_account( + *mint_address, + SolanaAccount { + lamports: 1_000_000_000, + data: mint_data, + owner: *program_id, + ..SolanaAccount::default() + }, + ); + + let account_size = + ExtensionType::try_calculate_account_len::(&[ExtensionType::TransferHookAccount]) + .unwrap(); + let mut account_data = vec![0; account_size]; + let mut state = + StateWithExtensionsMut::::unpack_uninitialized(&mut account_data).unwrap(); + let extension = state.init_extension::(true).unwrap(); + extension.transferring = transferring.into(); + let token_amount = 1_000_000_000_000; + state.base = Account { + mint: *mint_address, + owner: *owner, + amount: token_amount, + delegate: COption::None, + state: AccountState::Initialized, + is_native: COption::None, + delegated_amount: 0, + close_authority: COption::None, + }; + state.pack_base(); + state.init_account_type().unwrap(); + + program_test.add_account( + *source, + SolanaAccount { + lamports: 1_000_000_000, + data: account_data.clone(), + owner: *program_id, + ..SolanaAccount::default() + }, + ); + program_test.add_account( + *destination, + SolanaAccount { + lamports: 1_000_000_000, + data: account_data, + owner: *program_id, + ..SolanaAccount::default() + }, + ); +} + +#[tokio::test] +async fn success_execute() { + let program_id = Pubkey::new_unique(); + let mut program_test = setup(&program_id); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + let amount = 0u64; + + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); + + let writable_pubkey = Pubkey::new_unique(); + + let init_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let extra_pda_1 = Pubkey::find_program_address( + &[ + b"seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &program_id, + ) + .0; + + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &init_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // fail with missing account + { + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas[..2], + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with wrong account + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(Pubkey::new_unique(), false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with wrong PDA + let wrong_pda_2 = Pubkey::find_program_address( + &[ + &99u64.to_le_bytes(), // Wrong data + destination.as_ref(), + ], + &program_id, + ) + .0; + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(wrong_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with not signer + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, false), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // success with correct params + { + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + } +} + +#[tokio::test] +async fn fail_incorrect_derivation() { + let program_id = Pubkey::new_unique(); + let mut program_test = setup(&program_id); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + // wrong derivation + let extra_account_metas = get_extra_account_metas_address(&program_id, &mint_address); + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent.minimum_balance(ExtraAccountMetaList::size_of(0).unwrap()); + + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas, + &mint_address, + &mint_authority_pubkey, + &[], + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::InvalidSeeds) + ); +} + +/// Test program to CPI into default transfer-hook-interface program +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let amount = input + .get(8..16) + .and_then(|slice| slice.try_into().ok()) + .map(u64::from_le_bytes) + .ok_or(ProgramError::InvalidInstructionData)?; + onchain::invoke_execute( + accounts[0].key, + accounts[1].clone(), + accounts[2].clone(), + accounts[3].clone(), + accounts[4].clone(), + &accounts[5..], + amount, + ) +} + +#[tokio::test] +async fn success_on_chain_invoke() { + let hook_program_id = Pubkey::new_unique(); + let mut program_test = setup(&hook_program_id); + let program_id = Pubkey::new_unique(); + program_test.add_program( + "test_cpi_program", + program_id, + processor!(process_instruction), + ); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + let amount = 0u64; + + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + let extra_account_metas_address = + get_extra_account_metas_address(&mint_address, &hook_program_id); + let writable_pubkey = Pubkey::new_unique(); + + let init_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let extra_pda_1 = Pubkey::find_program_address( + &[ + b"seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &hook_program_id, + ) + .0; + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &hook_program_id, + ) + .0; + + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &hook_program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &init_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + // easier to hack this up! + let mut test_instruction = execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + amount, + ); + test_instruction + .accounts + .insert(0, AccountMeta::new_readonly(hook_program_id, false)); + let transaction = Transaction::new_signed_with_payer( + &[test_instruction], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn fail_without_transferring_flag() { + let program_id = Pubkey::new_unique(); + let mut program_test = setup(&program_id); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + false, + ); + + let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); + let extra_account_metas = []; + let init_extra_account_metas = []; + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &init_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + 0, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(TransferHookError::ProgramCalledOutsideOfTransfer as u32) + ) + ); +} + +#[tokio::test] +async fn success_on_chain_invoke_with_updated_extra_account_metas() { + let hook_program_id = Pubkey::new_unique(); + let mut program_test = setup(&hook_program_id); + let program_id = Pubkey::new_unique(); + program_test.add_program( + "test_cpi_program", + program_id, + processor!(process_instruction), + ); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + let amount = 0u64; + + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + let extra_account_metas_address = + get_extra_account_metas_address(&mint_address, &hook_program_id); + let writable_pubkey = Pubkey::new_unique(); + + // Create an initial account metas list + let init_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"init-seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); + let init_transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &hook_program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &init_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(init_transaction) + .await + .unwrap(); + + // Create an updated account metas list + let updated_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"updated-seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(updated_extra_account_metas.len()).unwrap()); + let update_transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + update_extra_account_meta_list( + &hook_program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &updated_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(update_transaction) + .await + .unwrap(); + + let updated_extra_pda_1 = Pubkey::find_program_address( + &[ + b"updated-seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &hook_program_id, + ) + .0; + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &hook_program_id, + ) + .0; + + let test_updated_extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(updated_extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + + // Use updated account metas list + let mut test_instruction = execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &test_updated_extra_account_metas, + amount, + ); + test_instruction + .accounts + .insert(0, AccountMeta::new_readonly(hook_program_id, false)); + let transaction = Transaction::new_signed_with_payer( + &[test_instruction], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); +} + +#[tokio::test] +async fn success_execute_with_updated_extra_account_metas() { + let program_id = Pubkey::new_unique(); + let mut program_test = setup(&program_id); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + let amount = 0u64; + + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + let extra_account_metas_address = get_extra_account_metas_address(&mint_address, &program_id); + + let writable_pubkey = Pubkey::new_unique(); + + let init_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&writable_pubkey, false, true).unwrap(), + ]; + + let extra_pda_1 = Pubkey::find_program_address( + &[ + b"seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + + let extra_pda_2 = Pubkey::find_program_address( + &[ + &amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &program_id, + ) + .0; + + let init_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(extra_pda_1, false), + AccountMeta::new(extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent + .minimum_balance(ExtraAccountMetaList::size_of(init_extra_account_metas.len()).unwrap()); + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &init_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let updated_amount = 1u64; + let updated_writable_pubkey = Pubkey::new_unique(); + + // Create updated extra account metas + let updated_extra_account_metas = [ + ExtraAccountMeta::new_with_pubkey(&sysvar::instructions::id(), false, false).unwrap(), + ExtraAccountMeta::new_with_pubkey(&mint_authority_pubkey, true, false).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"updated-seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::InstructionData { + index: 8, // After instruction discriminator + length: 8, // `u64` (amount) + }, + Seed::AccountKey { index: 2 }, + ], + false, + true, + ) + .unwrap(), + ExtraAccountMeta::new_with_pubkey(&updated_writable_pubkey, false, true).unwrap(), + ExtraAccountMeta::new_with_seeds( + &[ + Seed::Literal { + bytes: b"new-seed-prefix".to_vec(), + }, + Seed::AccountKey { index: 0 }, + ], + false, + true, + ) + .unwrap(), + ]; + + let updated_extra_pda_1 = Pubkey::find_program_address( + &[ + b"updated-seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + + let updated_extra_pda_2 = Pubkey::find_program_address( + &[ + &updated_amount.to_le_bytes(), // Instruction data bytes 8 to 16 + destination.as_ref(), // Account at index 2 + ], + &program_id, + ) + .0; + + // add another PDA + let new_extra_pda = Pubkey::find_program_address( + &[ + b"new-seed-prefix", // Literal prefix + source.as_ref(), // Account at index 0 + ], + &program_id, + ) + .0; + + let updated_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(updated_extra_pda_1, false), + AccountMeta::new(updated_extra_pda_2, false), + AccountMeta::new(updated_writable_pubkey, false), + AccountMeta::new(new_extra_pda, false), + ]; + + let update_transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas_address, + rent_lamports, + ), + update_extra_account_meta_list( + &program_id, + &extra_account_metas_address, + &mint_address, + &mint_authority_pubkey, + &updated_extra_account_metas, + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction(update_transaction) + .await + .unwrap(); + + // fail with initial account metas list + { + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &init_account_metas, + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with missing account + { + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &updated_account_metas[..2], + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with wrong account + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(updated_extra_pda_1, false), + AccountMeta::new(updated_extra_pda_2, false), + AccountMeta::new(Pubkey::new_unique(), false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with wrong PDA + let wrong_pda_2 = Pubkey::find_program_address( + &[ + &99u64.to_le_bytes(), // Wrong data + destination.as_ref(), + ], + &program_id, + ) + .0; + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, true), + AccountMeta::new(updated_extra_pda_1, false), + AccountMeta::new(wrong_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // fail with not signer + { + let extra_account_metas = [ + AccountMeta::new_readonly(sysvar::instructions::id(), false), + AccountMeta::new_readonly(mint_authority_pubkey, false), + AccountMeta::new(updated_extra_pda_1, false), + AccountMeta::new(updated_extra_pda_2, false), + AccountMeta::new(writable_pubkey, false), + ]; + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &extra_account_metas, + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(AccountResolutionError::IncorrectAccount as u32), + ) + ); + } + + // success with correct params + { + let transaction = Transaction::new_signed_with_payer( + &[execute_with_extra_account_metas( + &program_id, + &source, + &mint_address, + &destination, + &wallet.pubkey(), + &extra_account_metas_address, + &updated_account_metas, + updated_amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + } +} diff --git a/token/transfer-hook/example/Cargo.toml b/token/transfer-hook/example/Cargo.toml index 341bc89be..44a08f9e2 100644 --- a/token/transfer-hook/example/Cargo.toml +++ b/token/transfer-hook/example/Cargo.toml @@ -8,16 +8,18 @@ license = "Apache-2.0" edition = "2021" [features] +default = ["forbid-additional-mints"] no-entrypoint = [] test-sbf = [] +forbid-additional-mints = [] [dependencies] -arrayref = "0.3.7" +arrayref = "0.3.9" solana-program = { workspace = true } -spl-tlv-account-resolution = { version = "0.5" , path = "../../../libraries/tlv-account-resolution" } -spl-token-2022 = { version = "1.0", path = "../../program-2022", features = ["no-entrypoint"] } -spl-transfer-hook-interface = { version = "0.4" , path = "../interface" } -spl-type-length-value = { version = "0.3" , path = "../../../libraries/type-length-value" } +spl-tlv-account-resolution = { workspace = true } +spl-token-2022 = { workspace = true } +spl-transfer-hook-interface = { workspace = true } +spl-type-length-value = "0.7.0" [dev-dependencies] solana-program-test = { workspace = true } diff --git a/token/transfer-hook/example/src/lib.rs b/token/transfer-hook/example/src/lib.rs index 9db06d9c6..aa49d80c4 100644 --- a/token/transfer-hook/example/src/lib.rs +++ b/token/transfer-hook/example/src/lib.rs @@ -16,3 +16,15 @@ mod entrypoint; // Export current sdk types for downstream users building with a different sdk // version pub use solana_program; + +/// Place the mint id that you want to target with your transfer hook program. +/// Any other mint will fail to initialize, protecting the transfer hook program +/// from rogue mints trying to get access to accounts. +/// +/// There are many situations where it's reasonable to support multiple mints +/// with one transfer-hook program, but because it's easy to make something +/// unsafe, this simple example implementation only allows for one mint. +#[cfg(feature = "forbid-additional-mints")] +pub mod mint { + solana_program::declare_id!("Mint111111111111111111111111111111111111111"); +} diff --git a/token/transfer-hook/example/src/processor.rs b/token/transfer-hook/example/src/processor.rs index e398b2078..9c5eccde9 100644 --- a/token/transfer-hook/example/src/processor.rs +++ b/token/transfer-hook/example/src/processor.rs @@ -74,7 +74,7 @@ pub fn process_execute( } /// Processes a -/// [InitializeExtraAccountMetaList](enum.TransferHookInstruction.html) +/// [`InitializeExtraAccountMetaList`](enum.TransferHookInstruction.html) /// instruction. pub fn process_initialize_extra_account_meta_list( program_id: &Pubkey, @@ -88,6 +88,13 @@ pub fn process_initialize_extra_account_meta_list( let authority_info = next_account_info(account_info_iter)?; let _system_program_info = next_account_info(account_info_iter)?; + // check that the one mint we want to target is trying to create extra + // account metas + #[cfg(feature = "forbid-additional-mints")] + if *mint_info.key != crate::mint::id() { + return Err(ProgramError::InvalidArgument); + } + // check that the mint authority is valid without fully deserializing let mint_data = mint_info.try_borrow_data()?; let mint = StateWithExtensions::::unpack(&mint_data)?; @@ -135,7 +142,7 @@ pub fn process_initialize_extra_account_meta_list( } /// Processes a -/// [UpdateExtraAccountMetaList](enum.TransferHookInstruction.html) +/// [`UpdateExtraAccountMetaList`](enum.TransferHookInstruction.html) /// instruction. pub fn process_update_extra_account_meta_list( program_id: &Pubkey, @@ -218,4 +225,4 @@ pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> P process_update_extra_account_meta_list(program_id, accounts, &extra_account_metas) } } -} +} \ No newline at end of file diff --git a/token/transfer-hook/example/src/state.rs b/token/transfer-hook/example/src/state.rs index b2c962c50..f3b6bdd51 100644 --- a/token/transfer-hook/example/src/state.rs +++ b/token/transfer-hook/example/src/state.rs @@ -12,4 +12,4 @@ pub fn example_data(account_metas: &[ExtraAccountMeta]) -> Result, Progr let mut data = vec![0; account_size]; ExtraAccountMetaList::init::(&mut data, account_metas)?; Ok(data) -} +} \ No newline at end of file diff --git a/token/transfer-hook/example/tests/functional.rs b/token/transfer-hook/example/tests/functional.rs index 4e078c1fd..0d64d1abf 100644 --- a/token/transfer-hook/example/tests/functional.rs +++ b/token/transfer-hook/example/tests/functional.rs @@ -22,7 +22,10 @@ use { state::ExtraAccountMetaList, }, spl_token_2022::{ - extension::{transfer_hook::TransferHookAccount, ExtensionType, StateWithExtensionsMut}, + extension::{ + transfer_hook::TransferHookAccount, BaseStateWithExtensionsMut, ExtensionType, + StateWithExtensionsMut, + }, state::{Account, AccountState, Mint}, }, spl_transfer_hook_interface::{ @@ -139,7 +142,7 @@ async fn success_execute() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique(); @@ -436,7 +439,7 @@ async fn fail_incorrect_derivation() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique(); @@ -492,6 +495,69 @@ async fn fail_incorrect_derivation() { ); } +#[tokio::test] +async fn fail_incorrect_mint() { + let program_id = Pubkey::new_unique(); + let mut program_test = setup(&program_id); + + let token_program_id = spl_token_2022::id(); + let wallet = Keypair::new(); + // wrong mint, only `spl_transfer_hook_example::mint::id()` allowed + let mint_address = Pubkey::new_unique(); + let mint_authority = Keypair::new(); + let mint_authority_pubkey = mint_authority.pubkey(); + let source = Pubkey::new_unique(); + let destination = Pubkey::new_unique(); + let decimals = 2; + setup_token_accounts( + &mut program_test, + &token_program_id, + &mint_address, + &mint_authority_pubkey, + &source, + &destination, + &wallet.pubkey(), + decimals, + true, + ); + + let extra_account_metas = get_extra_account_metas_address(&mint_address, &program_id); + + let mut context = program_test.start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let rent_lamports = rent.minimum_balance(ExtraAccountMetaList::size_of(0).unwrap()); + + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::transfer( + &context.payer.pubkey(), + &extra_account_metas, + rent_lamports, + ), + initialize_extra_account_meta_list( + &program_id, + &extra_account_metas, + &mint_address, + &mint_authority_pubkey, + &[], + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &mint_authority], + context.last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(1, InstructionError::InvalidArgument) + ); +} + /// Test program to CPI into default transfer-hook-interface program pub fn process_instruction( _program_id: &Pubkey, @@ -527,7 +593,7 @@ async fn success_on_chain_invoke() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique(); @@ -670,7 +736,7 @@ async fn fail_without_transferring_flag() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique(); @@ -764,7 +830,7 @@ async fn success_on_chain_invoke_with_updated_extra_account_metas() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique(); @@ -967,7 +1033,7 @@ async fn success_execute_with_updated_extra_account_metas() { let token_program_id = spl_token_2022::id(); let wallet = Keypair::new(); - let mint_address = Pubkey::new_unique(); + let mint_address = spl_transfer_hook_example::mint::id(); let mint_authority = Keypair::new(); let mint_authority_pubkey = mint_authority.pubkey(); let source = Pubkey::new_unique();