diff --git a/Cargo.lock b/Cargo.lock index 569b1ff..9db9a75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,26 +15,40 @@ dependencies = [ [[package]] name = "ap-valkyrie" -version = "1.0.0" +version = "1.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw20 1.1.0", ] [[package]] name = "astroport" -version = "2.5.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=main#65ce7d1879cc5d95b09fa14202f0423bba52ae0e" +version = "3.3.2" +source = "git+https://github.com/astroport-fi/astroport-core#52af83eab04c620ac40019f7cc9cee433d0c601e" dependencies = [ + "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", + "cw-utils 1.0.1", "cw20 0.15.1", + "cw3", "itertools", "uint", ] +[[package]] +name = "astroport-circular-buffer" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/astroport-core#52af83eab04c620ac40019f7cc9cee433d0c601e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "thiserror", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -53,16 +67,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bigint" -version = "4.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0e8c8a600052b52482eff2cf4d810e462fdff1f656ac1ecb6232132a1ed7def" -dependencies = [ - "byteorder", - "crunchy 0.1.6", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -81,6 +85,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" + [[package]] name = "byteorder" version = "1.4.3" @@ -95,17 +105,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "cosmwasm-crypto" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22add0f9b2a5416df98c1d0248a8d8eedb882c38fbf0c5052b64eebe865df6d" +checksum = "51dd316b3061747d6f57c1c4a131a5ba2f9446601a9276d05a4d25ab2ce0a7e0" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "ed25519-zebra", "k256", "rand_core 0.6.4", @@ -114,18 +124,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e64f710a18ef90d0a632cf27842e98ffc2d005a38a6f76c12fd0bc03bc1a2d" +checksum = "03b14230c6942a301afb96f601af97ae09966601bd1007067a2c7fe8ffcfe303" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5ad2e23a971b9e4cd57b20cee3e2e79c33799bed4b128e473aca3702bfe5dd" +checksum = "1027bdd5941b7d4b45bd773b6d88818dcc043e8db68916bfbd5caf971024dbea" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -136,9 +146,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2926d159a9bb1a716a592b40280f1663f2491a9de3b6da77c0933cee2a2655b8" +checksum = "b6e069f6e65a9a1f55f8d7423703bed35e9311d029d91b357b17a07010d95cd7" dependencies = [ "proc-macro2", "quote", @@ -147,11 +157,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.3" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fee88ff5bf7bef55bd37ac0619974701b99bf6bd4b16cf56ee8810718abd71" +checksum = "c27a06f0f6c35b178563c6b1044245b3f750c4a66d9f6d2b942a6b29ad77d3ae" dependencies = [ "base64", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -160,36 +171,19 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", - "uint", -] - -[[package]] -name = "cosmwasm-storage" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639bc36408bc1ac45e3323166ceeb8f0b91b55a941c4ad59d389829002fbbd94" -dependencies = [ - "cosmwasm-std", - "serde", ] [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] -[[package]] -name = "crunchy" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f4a431c5c9f662e1200b7c7f02c34e91361150e382089a8f2dec3ba680cbda" - [[package]] name = "crunchy" version = "0.2.2" @@ -233,9 +227,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.11.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" +checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" dependencies = [ "cosmwasm-std", "schemars", @@ -244,9 +238,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.15.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" +checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" dependencies = [ "cosmwasm-std", "schemars", @@ -255,25 +249,28 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.11.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef842a1792e4285beff7b3b518705f760fa4111dc1e296e53f3e92d1ef7f6220" +checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" dependencies = [ + "cosmwasm-schema", "cosmwasm-std", + "cw2 0.15.1", "schemars", + "semver", "serde", "thiserror", ] [[package]] name = "cw-utils" -version = "0.15.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2", + "cw2 1.1.0", "schemars", "semver", "serde", @@ -294,15 +291,17 @@ dependencies = [ ] [[package]] -name = "cw20" -version = "0.11.1" +name = "cw2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9671d7edef5608acaf5b2f1e473ee3f501eced2cd4f7392e2106c8cf02ba0720" +checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" dependencies = [ + "cosmwasm-schema", "cosmwasm-std", - "cw-utils 0.11.1", + "cw-storage-plus 1.1.0", "schemars", "serde", + "thiserror", ] [[package]] @@ -318,6 +317,34 @@ dependencies = [ "serde", ] +[[package]] +name = "cw20" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171af3d9127de6805a7dd819fb070c7d2f6c3ea85f4193f42cef259f0a7f33d5" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "cw20 1.1.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "der" version = "0.6.1" @@ -350,9 +377,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", @@ -361,9 +388,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "ecdsa" @@ -394,9 +421,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -407,7 +434,7 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -443,10 +470,9 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw2", + "cw2 0.15.1", "cw20 0.15.1", "thiserror", - "valkyrie", ] [[package]] @@ -461,9 +487,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -502,7 +528,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -516,9 +542,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "k256" @@ -529,20 +555,20 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] name = "libc" -version = "0.2.140" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -562,18 +588,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.54" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -606,9 +632,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" @@ -650,37 +676,37 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.159" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde-json-wasm" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15bee9b04dd165c3f4e142628982ddde884c2022a89e8ddf99c4829bf2c3a58" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.29", ] [[package]] @@ -696,9 +722,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -720,13 +746,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -735,7 +761,7 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -757,9 +783,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -774,9 +800,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.11" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -785,22 +811,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.29", ] [[package]] @@ -816,29 +842,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", - "crunchy 0.2.2", + "crunchy", "hex", "static_assertions", ] [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] -name = "valkyrie" -version = "1.0.8-beta.1" -source = "git+https://github.com/astroport-fi/valkyrieprotocol?rev=b5fcb666f17d7e291f40365756e50fc0d7b9bf54#b5fcb666f17d7e291f40365756e50fc0d7b9bf54" +name = "valkyrie-lp-staking" +version = "1.1.0" dependencies = [ - "bigint", + "ap-valkyrie", + "astroport", + "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-storage-plus 0.11.1", - "cw20 0.11.1", - "schemars", - "serde", + "cw-storage-plus 1.1.0", + "cw20 1.1.0", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 2489720..25c0999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [workspace] +resolver = "2" members = [ - "contracts/*", + "contracts/proxies/proxy_to_vkr", + "contracts/stakings/staking_vkr", "packages/*" ] [profile.release] -opt-level = 3 +opt-level = "z" debug = false rpath = false lto = true diff --git a/contracts/proxy_to_vkr/.cargo/config b/contracts/proxies/proxy_to_vkr/.cargo/config similarity index 76% rename from contracts/proxy_to_vkr/.cargo/config rename to contracts/proxies/proxy_to_vkr/.cargo/config index 7d1a066..cc4f901 100644 --- a/contracts/proxy_to_vkr/.cargo/config +++ b/contracts/proxies/proxy_to_vkr/.cargo/config @@ -2,4 +2,4 @@ wasm = "build --release --target wasm32-unknown-unknown" wasm-debug = "build --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example schema" +schema = "run --example proxy_to_vkr_schema" diff --git a/contracts/proxy_to_vkr/Cargo.toml b/contracts/proxies/proxy_to_vkr/Cargo.toml similarity index 79% rename from contracts/proxy_to_vkr/Cargo.toml rename to contracts/proxies/proxy_to_vkr/Cargo.toml index 44e5fe6..7ef423b 100644 --- a/contracts/proxy_to_vkr/Cargo.toml +++ b/contracts/proxies/proxy_to_vkr/Cargo.toml @@ -25,7 +25,6 @@ cw-storage-plus = "0.15" thiserror = { version = "1.0" } cw2 = "0.15" cw20 = "0.15" -valkyrie = { git = "https://github.com/astroport-fi/valkyrieprotocol", rev = "b5fcb666f17d7e291f40365756e50fc0d7b9bf54" } -astroport = { git = "https://github.com/astroport-fi/astroport-core", branch = "main" } -ap-valkyrie = { path = "../../packages/valkyrie" } +astroport = { git = "https://github.com/astroport-fi/astroport-core" } +ap-valkyrie = { path = "../../../packages/valkyrie" } cosmwasm-schema = "1.1" diff --git a/contracts/proxy_to_vkr/README.md b/contracts/proxies/proxy_to_vkr/README.md similarity index 100% rename from contracts/proxy_to_vkr/README.md rename to contracts/proxies/proxy_to_vkr/README.md diff --git a/contracts/proxy_to_vkr/examples/schema.rs b/contracts/proxies/proxy_to_vkr/examples/schema.rs similarity index 65% rename from contracts/proxy_to_vkr/examples/schema.rs rename to contracts/proxies/proxy_to_vkr/examples/schema.rs index 273fafa..38c3e43 100644 --- a/contracts/proxy_to_vkr/examples/schema.rs +++ b/contracts/proxies/proxy_to_vkr/examples/schema.rs @@ -1,5 +1,4 @@ -use ap_valkyrie::MigrateMsg; -use astroport::generator_proxy::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use astroport::generator_proxy::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use cosmwasm_schema::write_api; fn main() { diff --git a/contracts/proxy_to_vkr/src/contract.rs b/contracts/proxies/proxy_to_vkr/src/contract.rs similarity index 97% rename from contracts/proxy_to_vkr/src/contract.rs rename to contracts/proxies/proxy_to_vkr/src/contract.rs index e5bd1d8..7cf20db 100644 --- a/contracts/proxy_to_vkr/src/contract.rs +++ b/contracts/proxies/proxy_to_vkr/src/contract.rs @@ -6,16 +6,15 @@ use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Cw20ReceiveMsg}; use crate::error::ContractError; use crate::state::{Config, CONFIG}; -use ap_valkyrie::MigrateMsg; use astroport::generator_proxy::{ CallbackMsg, ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, }; -use cw2::set_contract_version; -use valkyrie::lp_staking::execute_msgs::{ - Cw20HookMsg as VkrCw20HookMsg, ExecuteMsg as VkrExecuteMsg, +use ap_valkyrie::staking_vkr::{ + Cw20HookMsg as VkrCw20HookMsg, ExecuteMsg as VkrExecuteMsg, MigrateMsg, + QueryMsg as VkrQueryMsg, StakerInfoResponse, }; -use valkyrie::lp_staking::query_msgs::{QueryMsg as VkrQueryMsg, StakerInfoResponse}; +use cw2::set_contract_version; // version info for migration info const CONTRACT_NAME: &str = "astroport-generator-proxy-to-vkr"; @@ -102,7 +101,7 @@ fn receive_cw20( msg: to_binary(&Cw20ExecuteMsg::Send { contract: cfg.reward_contract_addr.to_string(), amount: cw20_msg.amount, - msg: to_binary(&VkrCw20HookMsg::Bond {})?, + msg: to_binary(&VkrCw20HookMsg::Bond { schedules: vec![] })?, // TODO: })?, }))); } else { diff --git a/contracts/proxy_to_vkr/src/error.rs b/contracts/proxies/proxy_to_vkr/src/error.rs similarity index 100% rename from contracts/proxy_to_vkr/src/error.rs rename to contracts/proxies/proxy_to_vkr/src/error.rs diff --git a/contracts/proxy_to_vkr/src/lib.rs b/contracts/proxies/proxy_to_vkr/src/lib.rs similarity index 100% rename from contracts/proxy_to_vkr/src/lib.rs rename to contracts/proxies/proxy_to_vkr/src/lib.rs diff --git a/contracts/proxy_to_vkr/src/state.rs b/contracts/proxies/proxy_to_vkr/src/state.rs similarity index 100% rename from contracts/proxy_to_vkr/src/state.rs rename to contracts/proxies/proxy_to_vkr/src/state.rs diff --git a/contracts/proxy_to_vkr/src/testing/mock_querier.rs b/contracts/proxies/proxy_to_vkr/src/testing/mock_querier.rs similarity index 99% rename from contracts/proxy_to_vkr/src/testing/mock_querier.rs rename to contracts/proxies/proxy_to_vkr/src/testing/mock_querier.rs index 93629da..fcc01cb 100644 --- a/contracts/proxy_to_vkr/src/testing/mock_querier.rs +++ b/contracts/proxies/proxy_to_vkr/src/testing/mock_querier.rs @@ -1,3 +1,4 @@ +use ap_valkyrie::staking_vkr::StakerInfoResponse; use cosmwasm_schema::cw_serde; use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{ @@ -6,7 +7,6 @@ use cosmwasm_std::{ }; use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg}; use std::collections::HashMap; -use valkyrie::lp_staking::query_msgs::StakerInfoResponse; /// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies /// this uses our CustomQuerier. diff --git a/contracts/proxy_to_vkr/src/testing/mod.rs b/contracts/proxies/proxy_to_vkr/src/testing/mod.rs similarity index 100% rename from contracts/proxy_to_vkr/src/testing/mod.rs rename to contracts/proxies/proxy_to_vkr/src/testing/mod.rs diff --git a/contracts/proxy_to_vkr/src/testing/tests.rs b/contracts/proxies/proxy_to_vkr/src/testing/tests.rs similarity index 98% rename from contracts/proxy_to_vkr/src/testing/tests.rs rename to contracts/proxies/proxy_to_vkr/src/testing/tests.rs index c6d9f30..2581303 100644 --- a/contracts/proxy_to_vkr/src/testing/tests.rs +++ b/contracts/proxies/proxy_to_vkr/src/testing/tests.rs @@ -2,13 +2,11 @@ use crate::contract::{execute, instantiate, query}; use crate::error::ContractError; use crate::state::{Config, CONFIG}; use crate::testing::mock_querier::mock_dependencies; +use ap_valkyrie::staking_vkr::{Cw20HookMsg as VkrCw20HookMsg, ExecuteMsg as VkrExecuteMsg}; use astroport::generator_proxy::{CallbackMsg, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{from_binary, to_binary, Addr, CosmosMsg, SubMsg, Uint128, WasmMsg}; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; -use valkyrie::lp_staking::execute_msgs::{ - Cw20HookMsg as VkrCw20HookMsg, ExecuteMsg as VkrExecuteMsg, -}; #[test] fn test_proper_initialization() { @@ -92,7 +90,7 @@ fn test_deposit() { msg: to_binary(&Cw20ExecuteMsg::Send { contract: "reward0000".to_string(), amount: Uint128::from(100u128), - msg: to_binary(&VkrCw20HookMsg::Bond {}).unwrap(), + msg: to_binary(&VkrCw20HookMsg::Bond { schedules: vec![] }).unwrap(), }) .unwrap(), }))] diff --git a/contracts/stakings/staking_vkr/.cargo/config b/contracts/stakings/staking_vkr/.cargo/config new file mode 100644 index 0000000..d3b249b --- /dev/null +++ b/contracts/stakings/staking_vkr/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example staking_vkr_schema" diff --git a/contracts/stakings/staking_vkr/Cargo.toml b/contracts/stakings/staking_vkr/Cargo.toml new file mode 100644 index 0000000..c616a65 --- /dev/null +++ b/contracts/stakings/staking_vkr/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "valkyrie-lp-staking" +version = "1.1.0" +authors = ["Valkyrie Protocol, Astroport"] +edition = "2021" +description = "A Staking contract for Valkyrie Protocol - distribute rewards to stakers" +license = "Apache-2.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = { version = "1.3" } +cw-storage-plus = { version = "1.1"} +cw20 = { version = "1.1" } +ap-valkyrie = { path = "../../../packages/valkyrie" } +cosmwasm-schema = { version = "1.1" } +thiserror = { version = "1.0" } + +[dev-dependencies] +astroport = { git = "https://github.com/astroport-fi/astroport-core" } diff --git a/contracts/stakings/staking_vkr/README.md b/contracts/stakings/staking_vkr/README.md new file mode 100644 index 0000000..c31163f --- /dev/null +++ b/contracts/stakings/staking_vkr/README.md @@ -0,0 +1,5 @@ +# Staking + +The Staking Contract contains the logic for LP Token staking and reward distribution. VKR tokens +allocated for as liquidity incentives are distributed pro-rata to stakers of the VKR-USDC +Astroport pair LP token. diff --git a/contracts/stakings/staking_vkr/examples/schema.rs b/contracts/stakings/staking_vkr/examples/schema.rs new file mode 100644 index 0000000..3cdaa1a --- /dev/null +++ b/contracts/stakings/staking_vkr/examples/schema.rs @@ -0,0 +1,12 @@ +use ap_valkyrie::staking_vkr::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/stakings/staking_vkr/src/entrypoints.rs b/contracts/stakings/staking_vkr/src/entrypoints.rs new file mode 100644 index 0000000..b91db8a --- /dev/null +++ b/contracts/stakings/staking_vkr/src/entrypoints.rs @@ -0,0 +1,118 @@ +use crate::errors::ContractError; +#[cfg(not(feature = "library"))] +use crate::executions::{bond, migrate_reward, unbond, update_config, withdraw}; +use crate::queries::{query_config, query_staker_info, query_state}; +use crate::states::{Config, State}; +use crate::ContractResult; +use ap_valkyrie::staking_vkr::{Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + from_binary, to_binary, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Response, Uint128, +}; +use cw20::Cw20ReceiveMsg; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> ContractResult { + Config { + admin: info.sender, + token: deps.api.addr_validate(&msg.token)?, + usdc_token: deps.api.addr_validate(&msg.token)?, + pair: deps.api.addr_validate(&msg.pair)?, + lp_token: deps.api.addr_validate(&msg.lp_token)?, + whitelisted_contracts: msg + .whitelisted_contracts + .iter() + .map(|item| deps.api.addr_validate(item).unwrap()) + .collect(), + distribution_schedule: vec![], + } + .save(deps.storage)?; + + State { + last_distributed: env.block.height, + total_bond_amount: Uint128::zero(), + global_reward_index: Decimal::zero(), + } + .save(deps.storage)?; + + Ok(Response::new().add_attribute("action", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::Unbond { amount } => unbond(deps, env, info, amount), + ExecuteMsg::Withdraw {} => withdraw(deps, env, info), + ExecuteMsg::UpdateConfig { + token, + pair, + lp_token, + admin, + whitelisted_contracts, + } => update_config( + deps, + info, + token, + pair, + lp_token, + admin, + whitelisted_contracts, + ), + ExecuteMsg::MigrateReward { recipient, amount } => { + migrate_reward(deps, env, info, recipient, amount) + } + ExecuteMsg::ApproveAdminNominee {} => crate::executions::approve_admin_nominee(deps, info), + } +} + +pub fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> ContractResult { + let mut config: Config = Config::load(deps.storage)?; + + // only staking token contract can execute this message + if config.lp_token != info.sender { + return Err(ContractError::Unauthorized {}); + } + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::Bond { schedules } => bond( + deps, + env, + &mut config, + &cw20_msg.sender, + schedules, + cw20_msg.amount, + ), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> ContractResult { + let result = match msg { + QueryMsg::Config {} => to_binary(&query_config(deps)?), + QueryMsg::State { block_height } => to_binary(&query_state(deps, block_height)?), + QueryMsg::StakerInfo { staker } => to_binary(&query_staker_info(deps, env, &staker)?), + }?; + + Ok(result) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> ContractResult { + Ok(Response::default()) +} diff --git a/contracts/stakings/staking_vkr/src/errors.rs b/contracts/stakings/staking_vkr/src/errors.rs new file mode 100644 index 0000000..4368c36 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/errors.rs @@ -0,0 +1,37 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid zero amount")] + InvalidZeroAmount {}, + + #[error("Asset mismatch")] + AssetMismatch {}, + + #[error("Not found")] + NotFound {}, + + #[error("Exceed limit")] + ExceedLimit {}, + + #[error("Already exists")] + AlreadyExists {}, + + #[error( + "Schedule error. Should satisfy: (start_block < end_block, end_block > current_block)" + )] + ScheduleError {}, + + #[error("Schedules amount error. The total amount should be equal to the received amount.")] + SchedulesAmountError {}, +} diff --git a/contracts/stakings/staking_vkr/src/executions.rs b/contracts/stakings/staking_vkr/src/executions.rs new file mode 100644 index 0000000..00e9002 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/executions.rs @@ -0,0 +1,229 @@ +use cosmwasm_std::{attr, wasm_execute, DepsMut, Env, MessageInfo, Response, StdError, Uint128}; + +use crate::states::{Config, StakerInfo, State}; + +use crate::errors::ContractError; +use crate::utils::register_schedules; +use crate::ContractResult; +use cw20::Cw20ExecuteMsg; + +pub fn bond( + deps: DepsMut, + env: Env, + config: &mut Config, + sender: &str, + schedules: Vec<(u64, u64, Uint128)>, + amount: Uint128, +) -> ContractResult { + let sender_addr = deps.api.addr_validate(sender)?; + + if !config.is_authorized(&sender_addr) { + return Err(ContractError::Std(StdError::generic_err( + "Can only called by wallet", + ))); + } + + register_schedules(env.block.height, config, schedules, amount)?; + + let mut state: State = State::load(deps.storage)?; + let mut staker_info: StakerInfo = StakerInfo::load_or_default(deps.storage, &sender_addr)?; + + // Compute global reward & staker reward + state.compute_reward(config, env.block.height); + staker_info.compute_staker_reward(&state)?; + + // Increase bond_amount + state.total_bond_amount += amount; + staker_info.bond_amount += amount; + staker_info.save(deps.storage)?; + state.save(deps.storage)?; + config.save(deps.storage)?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "bond"), + attr("amount", amount.to_string()), + ])) +} + +pub fn unbond( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> ContractResult { + let config: Config = Config::load(deps.storage)?; + let mut state: State = State::load(deps.storage)?; + let mut staker_info: StakerInfo = StakerInfo::load_or_default(deps.storage, &info.sender)?; + + if staker_info.bond_amount < amount { + return Err(ContractError::Std(StdError::generic_err( + "Cannot unbond more than bond amount", + ))); + } + + // Compute global reward & staker reward + state.compute_reward(&config, env.block.height); + staker_info.compute_staker_reward(&state)?; + + // Decrease bond_amount + state.total_bond_amount = (state.total_bond_amount.checked_sub(amount))?; + state.save(deps.storage)?; + // Store or remove updated rewards info + // depends on the left pending reward and bond amount + staker_info.bond_amount = (staker_info.bond_amount.checked_sub(amount))?; + if staker_info.pending_reward.is_zero() && staker_info.bond_amount.is_zero() { + //no bond, no reward. + staker_info.delete(deps.storage); + } else { + staker_info.save(deps.storage)?; + } + + Ok(Response::new() + .add_message(wasm_execute( + &config.lp_token, + &Cw20ExecuteMsg::Transfer { + recipient: info.sender.to_string(), + amount, + }, + vec![], + )?) + .add_attributes(vec![ + attr("action", "unbond"), + attr("amount", amount.to_string()), + ])) +} + +// withdraw rewards to executor +pub fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> ContractResult { + let config: Config = Config::load(deps.storage)?; + let mut state: State = State::load(deps.storage)?; + let mut staker_info = StakerInfo::load_or_default(deps.storage, &info.sender)?; + + // Compute global reward & staker reward + state.compute_reward(&config, env.block.height); + staker_info.compute_staker_reward(&state)?; + state.save(deps.storage)?; + + let amount = staker_info.pending_reward; + staker_info.pending_reward = Uint128::zero(); + + // Store or remove updated rewards info + // depends on the left pending reward and bond amount + if staker_info.bond_amount.is_zero() { + staker_info.delete(deps.storage); + } else { + staker_info.save(deps.storage)?; + } + + Ok(Response::new() + .add_message(wasm_execute( + &config.token, + &Cw20ExecuteMsg::Transfer { + recipient: info.sender.to_string(), + amount, + }, + vec![], + )?) + .add_attributes(vec![ + attr("action", "withdraw"), + attr("amount", amount.to_string()), + ])) +} + +pub fn update_config( + deps: DepsMut, + info: MessageInfo, + token: Option, + pair: Option, + lp_token: Option, + admin: Option, + whitelisted_contracts: Option>, +) -> ContractResult { + let mut attributes = vec![attr("action", "update_config")]; + + let mut config: Config = Config::load(deps.storage)?; + if config.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + + if let Some(token) = token { + config.token = deps.api.addr_validate(token.as_str())?; + attributes.push(attr("is_updated_token", "true")); + } + + if let Some(pair) = pair { + config.pair = deps.api.addr_validate(pair.as_str())?; + attributes.push(attr("is_updated_pair", "true")); + } + + if let Some(lp_token) = lp_token { + config.lp_token = deps.api.addr_validate(lp_token.as_str())?; + attributes.push(attr("is_updated_lp_token", "true")); + } + + if let Some(admin) = admin { + Config::save_admin_nominee(deps.storage, &deps.api.addr_validate(admin.as_str())?)?; + attributes.push(attr("is_updated_admin_nominee", "true")); + } + + if let Some(whitelisted_contracts) = whitelisted_contracts { + config.whitelisted_contracts = whitelisted_contracts + .iter() + .map(|item| deps.api.addr_validate(item.as_str()).unwrap()) + .collect(); + attributes.push(attr("is_updated_whitelisted_contracts", "true")); + } + + config.save(deps.storage)?; + + Ok(Response::new().add_attributes(attributes)) +} + +pub fn migrate_reward( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + amount: Uint128, +) -> ContractResult { + let config = Config::load(deps.storage)?; + if config.admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + + let message = wasm_execute( + &config.token, + &Cw20ExecuteMsg::Transfer { + recipient: deps.api.addr_validate(recipient.as_str())?.to_string(), + amount, + }, + vec![], + )?; + + Ok(Response::new().add_message(message).add_attributes(vec![ + attr("action", "migrate_reward"), + attr("recipient", recipient), + attr("amount", amount.to_string()), + ])) +} + +pub fn approve_admin_nominee(deps: DepsMut, info: MessageInfo) -> ContractResult { + if let Some(admin_nominee) = Config::may_load_admin_nominee(deps.storage)? { + if admin_nominee != info.sender { + return Err(ContractError::Std(StdError::generic_err( + "It is not admin nominee", + ))); + } + } else { + return Err(ContractError::Unauthorized {}); + } + + let mut config = Config::load(deps.storage)?; + config.admin = info.sender; + config.save(deps.storage)?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "approve_admin_nominee"), + attr("is_updated_admin", "true"), + ])) +} diff --git a/contracts/stakings/staking_vkr/src/lib.rs b/contracts/stakings/staking_vkr/src/lib.rs new file mode 100644 index 0000000..edd67f6 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/lib.rs @@ -0,0 +1,12 @@ +pub mod entrypoints; + +mod executions; +mod queries; +mod states; + +mod errors; +#[cfg(test)] +mod tests; +mod utils; + +pub type ContractResult = core::result::Result; diff --git a/contracts/stakings/staking_vkr/src/queries.rs b/contracts/stakings/staking_vkr/src/queries.rs new file mode 100644 index 0000000..f6327a5 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/queries.rs @@ -0,0 +1,61 @@ +use crate::ContractResult; +use cosmwasm_std::{Deps, Env}; + +use crate::states::{Config, StakerInfo, State}; +use ap_valkyrie::staking_vkr::{ConfigResponse, StakerInfoResponse, StateResponse}; + +pub fn query_config(deps: Deps) -> ContractResult { + let config: Config = Config::load(deps.storage)?; + let resp = ConfigResponse { + admin: config.admin.to_string(), + token: config.token.to_string(), + pair: config.pair.to_string(), + lp_token: config.lp_token.to_string(), + whitelisted_contracts: config + .whitelisted_contracts + .iter() + .map(|item| item.to_string()) + .collect(), + distribution_schedule: config.distribution_schedule, + }; + + Ok(resp) +} + +pub fn query_state(deps: Deps, block_height: Option) -> ContractResult { + let mut state: State = State::load(deps.storage)?; + if let Some(block_height) = block_height { + let config: Config = Config::load(deps.storage)?; + state.compute_reward(&config, block_height); + } + + Ok(StateResponse { + last_distributed: state.last_distributed, + total_bond_amount: state.total_bond_amount, + global_reward_index: state.global_reward_index, + }) +} + +pub fn query_staker_info( + deps: Deps, + env: Env, + staker: &String, +) -> ContractResult { + let block_height = env.block.height; + let staker_raw = deps.api.addr_validate(staker)?; + + let mut staker_info: StakerInfo = StakerInfo::load_or_default(deps.storage, &staker_raw)?; + + let config: Config = Config::load(deps.storage)?; + let mut state: State = State::load(deps.storage)?; + + state.compute_reward(&config, block_height); + staker_info.compute_staker_reward(&state)?; + + Ok(StakerInfoResponse { + staker: staker.to_string(), + reward_index: staker_info.reward_index, + bond_amount: staker_info.bond_amount, + pending_reward: staker_info.pending_reward, + }) +} diff --git a/contracts/stakings/staking_vkr/src/states.rs b/contracts/stakings/staking_vkr/src/states.rs new file mode 100644 index 0000000..c1d8ca9 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/states.rs @@ -0,0 +1,135 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Decimal, StdResult, Storage, Uint128}; +use cw_storage_plus::{Item, Map}; + +const CONFIG: Item = Item::new("config_v3"); +const ADMIN_NOMINEE: Item = Item::new("admin_nominee"); + +#[cw_serde] +pub struct Config { + pub admin: Addr, + pub token: Addr, + pub usdc_token: Addr, + pub lp_token: Addr, + pub pair: Addr, + pub whitelisted_contracts: Vec, + pub distribution_schedule: Vec<(u64, u64, Uint128)>, +} + +impl Config { + pub fn save(&self, storage: &mut dyn Storage) -> StdResult<()> { + CONFIG.save(storage, self)?; + Ok(()) + } + + pub fn load(storage: &dyn Storage) -> StdResult { + CONFIG.load(storage) + } + + pub fn is_authorized(&self, address: &Addr) -> bool { + self.whitelisted_contracts.contains(address) + } + + pub fn may_load_admin_nominee(storage: &dyn Storage) -> StdResult> { + ADMIN_NOMINEE.may_load(storage) + } + + pub fn save_admin_nominee(storage: &mut dyn Storage, address: &Addr) -> StdResult<()> { + ADMIN_NOMINEE.save(storage, address) + } +} + +const STATE: Item = Item::new("state_v2"); + +#[cw_serde] +pub struct State { + pub last_distributed: u64, + pub total_bond_amount: Uint128, + pub global_reward_index: Decimal, +} + +impl State { + pub fn save(&self, storage: &mut dyn Storage) -> StdResult<()> { + STATE.save(storage, self) + } + + pub fn load(storage: &dyn Storage) -> StdResult { + STATE.load(storage) + } + + // compute distributed rewards and update global reward index + pub fn compute_reward(&mut self, config: &Config, block_height: u64) { + if self.total_bond_amount.is_zero() { + self.last_distributed = block_height; + return; + } + + let mut distributed_amount: Uint128 = Uint128::zero(); + for s in config.distribution_schedule.iter() { + //s.0 = begin block height of this schedule + //s.1 = end block height of this schedule + if s.0 > block_height || s.1 < self.last_distributed { + continue; + } + + // min(s.1, block_height) - max(s.0, last_distributed) + let passed_blocks = + std::cmp::min(s.1, block_height) - std::cmp::max(s.0, self.last_distributed); + + let num_blocks = s.1 - s.0; + let distribution_amount_per_block: Decimal = Decimal::from_ratio(s.2, num_blocks); + // distribution_amount_per_block = distribution amount of this schedule / blocks count of this schedule. + distributed_amount += + distribution_amount_per_block * Uint128::new(passed_blocks as u128); + } + + self.last_distributed = block_height; + self.global_reward_index += Decimal::from_ratio(distributed_amount, self.total_bond_amount); + // state.global_reward_index = state.global_reward_index + (distributed_amount / state.total_bond_amount) + } +} + +const STAKER_INFO: Map<&str, StakerInfo> = Map::new("reward"); + +#[cw_serde] +pub struct StakerInfo { + pub owner: Addr, + pub reward_index: Decimal, + pub bond_amount: Uint128, + pub pending_reward: Uint128, +} + +impl StakerInfo { + pub fn default(owner: Addr) -> StakerInfo { + StakerInfo { + owner, + reward_index: Decimal::zero(), + bond_amount: Uint128::zero(), + pending_reward: Uint128::zero(), + } + } + + pub fn save(&self, storage: &mut dyn Storage) -> StdResult<()> { + STAKER_INFO.save(storage, self.owner.as_str(), self) + } + + pub fn load_or_default(storage: &dyn Storage, owner: &Addr) -> StdResult { + Ok(STAKER_INFO + .may_load(storage, owner.as_str())? + .unwrap_or_else(|| StakerInfo::default(owner.clone()))) + } + + pub fn delete(&self, storage: &mut dyn Storage) { + STAKER_INFO.remove(storage, self.owner.as_str()) + } + + // withdraw reward to pending reward + pub fn compute_staker_reward(&mut self, state: &State) -> StdResult<()> { + let pending_reward = (self.bond_amount * state.global_reward_index) + .checked_sub(self.bond_amount * self.reward_index)?; + + self.reward_index = state.global_reward_index; + self.pending_reward += pending_reward; + Ok(()) + } +} diff --git a/contracts/stakings/staking_vkr/src/tests/bond.rs b/contracts/stakings/staking_vkr/src/tests/bond.rs new file mode 100644 index 0000000..d0a3b2a --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/bond.rs @@ -0,0 +1,64 @@ +use crate::ContractResult; +use ap_valkyrie::staking_vkr::{Cw20HookMsg, ExecuteMsg}; +use cosmwasm_std::testing::mock_info; +use cosmwasm_std::{to_binary, Addr, Env, Response, Uint128}; +use cw20::Cw20ReceiveMsg; + +use crate::entrypoints::execute; +use crate::states::{StakerInfo, State}; +use crate::tests::instantiate::default; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::test_utils::{LP_LIQUIDITY_TOKEN, LP_WHITELISTED1, LP_WHITELISTED2}; + +pub fn exec_bond( + deps: &mut CustomDeps, + env: &Env, + sender: &Addr, + schedules: Vec<(u64, u64, Uint128)>, + amount: Uint128, +) -> ContractResult { + let info = mock_info(LP_LIQUIDITY_TOKEN, &[]); + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: sender.to_string(), + amount, + msg: to_binary(&Cw20HookMsg::Bond { schedules }).unwrap(), + }); + + execute(deps.as_mut(), env.clone(), info.clone(), msg) +} + +fn will_success(deps: &mut CustomDeps, env: Env, sender: &Addr) { + let amount = Uint128::new(100u128); + exec_bond( + deps, + &env, + sender, + vec![(0, 100, Uint128::new(100))], + amount, + ) + .unwrap(); +} + +#[test] +fn succeed() { + let sender1 = Addr::unchecked(LP_WHITELISTED1); + let sender2 = Addr::unchecked(LP_WHITELISTED2); + + let mut deps = custom_deps(); + let (env, _info, _response) = default(&mut deps, vec![], None); + will_success(&mut deps, env.clone(), &sender1); + will_success(&mut deps, env.clone(), &sender2); + + let state1 = State::load(deps.as_ref().storage).unwrap(); + let info1 = StakerInfo::load_or_default(deps.as_ref().storage, &sender1).unwrap(); + let info2 = StakerInfo::load_or_default(deps.as_ref().storage, &sender2).unwrap(); + + assert_eq!(state1.total_bond_amount, Uint128::new(200u128)); + assert_eq!(state1.last_distributed, 0); + + assert_eq!(info1.pending_reward, Uint128::zero()); + assert_eq!(info1.bond_amount, Uint128::new(100u128)); + + assert_eq!(info2.pending_reward, Uint128::zero()); + assert_eq!(info2.bond_amount, Uint128::new(100u128)); +} diff --git a/contracts/stakings/staking_vkr/src/tests/instantiate.rs b/contracts/stakings/staking_vkr/src/tests/instantiate.rs new file mode 100644 index 0000000..d906d85 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/instantiate.rs @@ -0,0 +1,106 @@ +use ap_valkyrie::staking_vkr::{Cw20HookMsg, ExecuteMsg, InstantiateMsg}; +use cosmwasm_std::{to_binary, Addr, Decimal, Env, MessageInfo, Response, Uint128}; +use cw20::Cw20ReceiveMsg; + +use crate::entrypoints::{execute, instantiate}; +use crate::states::{Config, State}; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::test_utils::{ + default_sender, lp_env, DEFAULT_SENDER, LP_DISTRIBUTION_SCHEDULE1, LP_DISTRIBUTION_SCHEDULE2, + LP_LIQUIDITY_TOKEN, LP_PAIR_TOKEN, LP_REWARD_TOKEN, LP_WHITELISTED1, LP_WHITELISTED2, +}; +use crate::ContractResult; + +pub fn exec( + deps: &mut CustomDeps, + env: Env, + info: MessageInfo, + token: String, + pair: String, + lp_token: String, + whitelisted_contracts: Vec, +) -> ContractResult { + let msg = InstantiateMsg { + token, + pair, + lp_token, + whitelisted_contracts, + }; + + instantiate(deps.as_mut(), env, info, msg) +} + +pub fn default( + deps: &mut CustomDeps, + schedules: Vec<(u64, u64, Uint128)>, + total_bonded: Option, +) -> (Env, MessageInfo, Response) { + let env = lp_env(); + let info = default_sender(); + + let response = exec( + deps, + env.clone(), + info.clone(), + LP_REWARD_TOKEN.to_string(), + LP_PAIR_TOKEN.to_string(), + LP_LIQUIDITY_TOKEN.to_string(), + vec![LP_WHITELISTED1.to_string(), LP_WHITELISTED2.to_string()], + ) + .unwrap(); + + if let Some(total_bonded) = total_bonded { + let msg = ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: info.sender.to_string(), + amount: total_bonded, + msg: to_binary(&Cw20HookMsg::Bond { schedules }).unwrap(), + }); + + let mut info = info.clone(); + info.sender = Addr::unchecked(LP_LIQUIDITY_TOKEN); + execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + } + + deps.querier.plus_token_balances(&[ + ( + LP_REWARD_TOKEN, + &[(DEFAULT_SENDER, &LP_DISTRIBUTION_SCHEDULE1.2)], + ), + ( + LP_REWARD_TOKEN, + &[(DEFAULT_SENDER, &LP_DISTRIBUTION_SCHEDULE2.2)], + ), + ]); + + (env, info, response) +} + +#[test] +fn succeed() { + let mut deps = custom_deps(); + + let (env, info, _response) = default( + &mut deps, + vec![(0, 10, Uint128::new(100))], + Some(Uint128::new(100u128)), + ); + + let config = Config::load(&deps.storage).unwrap(); + assert_eq!(config.token, LP_REWARD_TOKEN); + assert_eq!(config.pair, LP_PAIR_TOKEN); + assert_eq!(config.lp_token, LP_LIQUIDITY_TOKEN); + assert_eq!(config.admin, info.sender); + assert_eq!( + config.whitelisted_contracts, + vec![LP_WHITELISTED1.to_string(), LP_WHITELISTED2.to_string()] + ); + assert_eq!( + config.distribution_schedule, + vec![(0, 10, Uint128::new(100))] + ); + + let state = State::load(&deps.storage).unwrap(); + assert_eq!(state.global_reward_index, Decimal::zero()); + assert_eq!(state.last_distributed, env.block.height); + assert_eq!(state.total_bond_amount, Uint128::new(100)); +} diff --git a/contracts/stakings/staking_vkr/src/tests/mock_querier.rs b/contracts/stakings/staking_vkr/src/tests/mock_querier.rs new file mode 100644 index 0000000..e4760d8 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/mock_querier.rs @@ -0,0 +1,307 @@ +use astroport::router::QueryMsg::SimulateSwapOperations; +use astroport::router::{SimulateSwapOperationsResponse, SwapOperation}; +use std::collections::HashMap; +use std::marker::PhantomData; + +use crate::tests::test_utils::VALKYRIE_PROXY; +use cosmwasm_std::testing::{MockApi, MockQuerier, MockStorage, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + from_binary, from_slice, to_binary, Api, Binary, CanonicalAddr, ContractResult, Empty, + OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, Uint128, WasmQuery, +}; +use cw20::{Cw20QueryMsg, TokenInfoResponse}; + +pub type CustomDeps = OwnedDeps; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies +/// this uses our CustomQuerier. +pub fn custom_deps() -> CustomDeps { + let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, &[])])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, + token_querier: TokenQuerier, + astroport_router_querier: AstroportRouterQuerier, +} + +#[derive(Clone, Default)] +pub struct AstroportRouterQuerier { + prices: HashMap<(String, String), f64>, +} + +#[derive(Clone, Default)] +pub struct TokenQuerier { + // this lets us iterate over all pairs that match the first string + balances: HashMap>, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely here + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(_) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request"), + request: bin_request.into(), + }); + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Raw { contract_addr, key }) => { + self.handle_wasm_raw(contract_addr, key) + } + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + self.handle_wasm_smart(contract_addr, msg) + } + _ => self.base.handle_query(request), + } + } + + fn handle_wasm_raw(&self, contract_addr: &String, key: &Binary) -> QuerierResult { + let key: &[u8] = key.as_slice(); + + let mut result = self.query_token_info(contract_addr, key); + + if result.is_none() { + result = self.query_balance(contract_addr, key); + } + + if result.is_none() { + return QuerierResult::Err(SystemError::UnsupportedRequest { + kind: "handle_wasm_raw".to_string(), + }); + } + + result.unwrap() + } + + fn handle_wasm_smart(&self, contract_addr: &String, msg: &Binary) -> QuerierResult { + let mut result = self.handle_wasm_smart_astroport_router(contract_addr, msg); + + if result.is_none() { + result = self.handle_cw20(contract_addr, msg); + } + + if result.is_none() { + return QuerierResult::Err(SystemError::UnsupportedRequest { + kind: "handle_wasm_smart".to_string(), + }); + } + + result.unwrap() + } + + fn handle_cw20(&self, contract_addr: &String, msg: &Binary) -> Option { + match from_binary(msg) { + Ok(Cw20QueryMsg::Balance { address }) => { + let default = Uint128::zero(); + let balance = self.token_querier.balances[contract_addr] + .get(address.as_str()) + .unwrap_or(&default) + .clone(); + + Some(SystemResult::Ok(ContractResult::from(to_binary( + &cw20::BalanceResponse { balance }, + )))) + } + Ok(_) => Some(QuerierResult::Err(SystemError::UnsupportedRequest { + kind: "handle_wasm_smart:cw20".to_string(), + })), + Err(_) => None, + } + } + + fn handle_wasm_smart_astroport_router( + &self, + contract_addr: &String, + msg: &Binary, + ) -> Option { + if contract_addr != VALKYRIE_PROXY { + return None; + } + + match from_binary(msg) { + Ok(SimulateSwapOperations { + offer_amount, + operations, + }) => { + let mut amount = offer_amount.u128(); + for operation in operations.iter() { + let price = self + .astroport_router_querier + .prices + .get(&swap_operation_to_string(operation)) + .unwrap(); + + amount = (amount as f64 * *price) as u128; + } + + Some(SystemResult::Ok(ContractResult::from(to_binary( + &SimulateSwapOperationsResponse { + amount: Uint128::new(amount), + }, + )))) + } + Ok(_) => Some(QuerierResult::Err(SystemError::UnsupportedRequest { + kind: "handle_wasm_smart:valkyrie_proxy".to_string(), + })), + Err(_) => None, + } + } + + fn query_token_info(&self, contract_addr: &String, key: &[u8]) -> Option { + if key.to_vec() != to_length_prefixed(b"token_info").to_vec() { + return None; + } + + let balances = self.token_querier.balances.get(contract_addr); + + if balances.is_none() { + return Some(SystemResult::Err(SystemError::InvalidRequest { + request: key.into(), + error: format!("No balance info exists for the contract {}", contract_addr,), + })); + } + + let balances = balances.unwrap(); + + let mut total_supply = Uint128::zero(); + + for balance in balances { + total_supply += *balance.1; + } + + Some(SystemResult::Ok(ContractResult::Ok( + to_binary(&TokenInfoResponse { + name: format!("{}Token", contract_addr), + symbol: format!("TOK"), + decimals: 6, + total_supply, + }) + .unwrap(), + ))) + } + + fn query_balance(&self, contract_addr: &String, key: &[u8]) -> Option { + let prefix_balance = to_length_prefixed(b"balance").to_vec(); + if key[..prefix_balance.len()].to_vec() == prefix_balance {} + + let balances = self.token_querier.balances.get(contract_addr); + + if balances.is_none() { + return Some(SystemResult::Err(SystemError::InvalidRequest { + request: key.into(), + error: format!("No balance info exists for the contract {}", contract_addr,), + })); + } + + let balances = balances.unwrap(); + + let key_address: &[u8] = &key[prefix_balance.len()..]; + let address_raw: CanonicalAddr = CanonicalAddr::from(key_address); + let api = MockApi::default(); + let address = match api.addr_humanize(&address_raw) { + Ok(v) => v.to_string(), + Err(_) => { + return Some(SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request"), + request: key.into(), + })); + } + }; + let balance = match balances.get(&address) { + Some(v) => v, + None => { + return Some(SystemResult::Err(SystemError::InvalidRequest { + error: "Balance not found".to_string(), + request: key.into(), + })); + } + }; + + Some(SystemResult::Ok(ContractResult::Ok( + to_binary(&balance).unwrap(), + ))) + } +} + +const ZERO: Uint128 = Uint128::zero(); + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + token_querier: TokenQuerier::default(), + astroport_router_querier: AstroportRouterQuerier::default(), + } + } + + pub fn plus_token_balances(&mut self, balances: &[(&str, &[(&str, &Uint128)])]) { + for (token_contract, balances) in balances.iter() { + let token_contract = token_contract.to_string(); + + if !self.token_querier.balances.contains_key(&token_contract) { + self.token_querier + .balances + .insert(token_contract.clone(), HashMap::new()); + } + let token_balances = self + .token_querier + .balances + .get_mut(&token_contract) + .unwrap(); + + for (account, balance) in balances.iter() { + let account = account.to_string(); + let account_balance = token_balances.get(&account).unwrap_or(&ZERO); + let new_balance = *account_balance + *balance; + token_balances.insert(account, new_balance); + } + } + } +} + +// Copy from cosmwasm-storage v0.14.1 +fn to_length_prefixed(namespace: &[u8]) -> Vec { + let mut out = Vec::with_capacity(namespace.len() + 2); + out.extend_from_slice(&encode_length(namespace)); + out.extend_from_slice(namespace); + out +} + +// Copy from cosmwasm-storage v0.14.1 +fn encode_length(namespace: &[u8]) -> [u8; 2] { + if namespace.len() > 0xFFFF { + panic!("only supports namespaces up to length 0xFFFF") + } + let length_bytes = (namespace.len() as u32).to_be_bytes(); + [length_bytes[2], length_bytes[3]] +} + +fn swap_operation_to_string(operation: &SwapOperation) -> (String, String) { + match operation { + SwapOperation::NativeSwap { + offer_denom, + ask_denom, + } => (offer_denom.to_string(), ask_denom.to_string()), + SwapOperation::AstroSwap { + offer_asset_info, + ask_asset_info, + } => (offer_asset_info.to_string(), ask_asset_info.to_string()), + } +} diff --git a/contracts/stakings/staking_vkr/src/tests/mod.rs b/contracts/stakings/staking_vkr/src/tests/mod.rs new file mode 100644 index 0000000..04f60fa --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/mod.rs @@ -0,0 +1,8 @@ +pub mod bond; +pub mod instantiate; +mod mock_querier; +mod test_utils; +pub mod unbond; +pub mod update_config; +pub mod validate; +pub mod withdraw; diff --git a/contracts/stakings/staking_vkr/src/tests/test_utils.rs b/contracts/stakings/staking_vkr/src/tests/test_utils.rs new file mode 100644 index 0000000..b1e1edd --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/test_utils.rs @@ -0,0 +1,54 @@ +use cosmwasm_std::testing::{mock_env, mock_info}; +use cosmwasm_std::{Addr, Env, MessageInfo, Response, StdError, Uint128}; + +use crate::errors::ContractError; +use crate::ContractResult; + +pub const LP_REWARD_TOKEN: &str = VALKYRIE_TOKEN; +pub const LP_PAIR_TOKEN: &str = "terra17n5sunn88hpy965mzvt3079fqx3rttnplg779g"; +pub const LP_LIQUIDITY_TOKEN: &str = "terra1627ldjvxatt54ydd3ns6xaxtd68a2vtyu7kakj"; +pub const LP_WHITELISTED1: &str = "terra190fxpjfkp6cygr2k9unzjurq42dyehqd579h5j"; +pub const LP_WHITELISTED2: &str = "terra1c7m6j8ya58a2fkkptn8fgudx8sqjqvc8azq0ex"; +pub const LP_DISTRIBUTION_SCHEDULE1: (u64, u64, Uint128) = (0, 100, Uint128::new(1000000u128)); +pub const LP_DISTRIBUTION_SCHEDULE2: (u64, u64, Uint128) = (100, 200, Uint128::new(10000000u128)); + +pub const DEFAULT_SENDER: &str = "terra1sq9ppsvt4k378wwhvm2vyfg7kqrhtve8p0n3a6"; +pub const VALKYRIE_TOKEN: &str = "terra1xj49zyqrwpv5k928jwfpfy2ha668nwdgkwlrg3"; +pub const VALKYRIE_PROXY: &str = "terra1fnywlw4edny3vw44x04xd67uzkdqluymgreu7g"; +pub const LIQUIDITY: &str = "terra1l7xu2rl3c7qmtx3r5sd2tz25glf6jh8ul7aag7"; + +pub fn default_sender() -> MessageInfo { + mock_info(LP_WHITELISTED1, &[]) +} + +pub fn lp_env() -> Env { + let mut env = mock_env_contract(LIQUIDITY); + env.block.height = 0; + env +} + +pub fn mock_env_contract(contract: &str) -> Env { + let mut env = mock_env(); + + env.contract.address = Addr::unchecked(contract); + + env +} + +pub fn expect_generic_err(result: &ContractResult, expect_msg: &str) { + match result { + Ok(_) => panic!("Must return error"), + Err(ContractError::Std(StdError::GenericErr { msg, .. })) => assert_eq!(msg, expect_msg), + Err(e) => panic!("Unexpected error: {:?}", e), + } +} + +pub fn expect_unauthorized_err(result: &ContractResult) { + match result { + Ok(_) => panic!("Must return error"), + Err(ContractError::Unauthorized {}) => { + // do nothing + } + Err(e) => panic!("Unexpected error: {:?}", e), + } +} diff --git a/contracts/stakings/staking_vkr/src/tests/unbond.rs b/contracts/stakings/staking_vkr/src/tests/unbond.rs new file mode 100644 index 0000000..be291fe --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/unbond.rs @@ -0,0 +1,53 @@ +use crate::executions::unbond; +use crate::states::{StakerInfo, State}; +use crate::tests::instantiate::default; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::test_utils::expect_generic_err; +use crate::ContractResult; +use cosmwasm_std::{Env, MessageInfo, Response, Uint128}; +use std::ops::Add; + +pub fn exec_unbond( + deps: &mut CustomDeps, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> ContractResult { + unbond(deps.as_mut(), env, info, amount) +} + +pub fn will_success(deps: &mut CustomDeps, amount: Uint128) -> (Env, MessageInfo, Response) { + let (mut env, info, _response) = default(deps, vec![(0, 100, amount)], Some(amount)); + + env.block.height = 10; + + let response = exec_unbond(deps, env.clone(), info.clone(), amount).unwrap(); + (env, info, response) +} + +#[test] +fn succeed() { + let mut deps = custom_deps(); + let total_bonded = Uint128::new(200u128); + let (_env, info, _response) = will_success(&mut deps, total_bonded); + + let state1 = State::load(deps.as_ref().storage).unwrap(); + let info1 = StakerInfo::load_or_default(deps.as_ref().storage, &info.sender).unwrap(); + + assert_eq!(state1.total_bond_amount, Uint128::zero()); + assert_eq!(state1.last_distributed, 10); + + assert_eq!(info1.pending_reward, Uint128::new(20)); + assert_eq!(info1.bond_amount, Uint128::zero()); +} + +#[test] +fn failed_invalid_amount() { + let mut deps = custom_deps(); + let total_bonded = Uint128::new(200u128); + let (env, info, _response) = + default(&mut deps, vec![(0, 100, total_bonded)], Some(total_bonded)); + let result = exec_unbond(&mut deps, env, info, total_bonded.add(total_bonded)); + + expect_generic_err(&result, "Cannot unbond more than bond amount"); +} diff --git a/contracts/stakings/staking_vkr/src/tests/update_config.rs b/contracts/stakings/staking_vkr/src/tests/update_config.rs new file mode 100644 index 0000000..1216fc5 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/update_config.rs @@ -0,0 +1,113 @@ +use cosmwasm_std::testing::mock_info; +use cosmwasm_std::{Addr, Env, MessageInfo, Response}; + +use crate::executions::update_config; +use crate::states::Config; +use crate::ContractResult; + +use crate::tests::instantiate::default; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::test_utils::{expect_unauthorized_err, lp_env, LP_WHITELISTED1}; + +pub fn exec( + deps: &mut CustomDeps, + info: MessageInfo, + token: Option, + pair: Option, + lp_token: Option, + admin: Option, + whitelisted_contracts: Option>, +) -> ContractResult { + update_config( + deps.as_mut(), + info, + token, + pair, + lp_token, + admin, + whitelisted_contracts, + ) +} + +pub fn will_success( + deps: &mut CustomDeps, + token: Option, + pair: Option, + lp_token: Option, + admin: Option, + whitelisted_contracts: Option>, +) -> (Env, MessageInfo, Response) { + let env = lp_env(); + let info = mock_info(LP_WHITELISTED1, &[]); + + let response = exec( + deps, + info.clone(), + token, + pair, + lp_token, + admin, + whitelisted_contracts, + ) + .unwrap(); + + (env, info, response) +} + +#[test] +fn succeed() { + let mut deps = custom_deps(); + + let (_env, info, _response) = default(&mut deps, vec![], None); + + let whitelisted_contracts = vec![ + "terra1r4qtnusnk63wkg2y6sytwr37aymz0sfy3p2yc9".to_string(), + "terra14mtctaszgzm4gcedlfslds802fmklnp4up72da".to_string(), + ]; + + will_success( + &mut deps, + Some("terra1r0rm0evrlkfvpt0csrcpmnpmrega54czajfd86".to_string()), + Some("terra1fmcjjt6yc9wqup2r06urnrd928jhrde6gcld6n".to_string()), + Some("terra199vw7724lzkwz6lf2hsx04lrxfkz09tg8dlp6r".to_string()), + Some("terra1e8ryd9ezefuucd4mje33zdms9m2s90m57878v9".to_string()), + Some(whitelisted_contracts.clone()), + ); + + let config = Config::load(&deps.storage).unwrap(); + assert_eq!( + config.token, + Addr::unchecked("terra1r0rm0evrlkfvpt0csrcpmnpmrega54czajfd86".to_string()) + ); + assert_eq!( + config.pair, + Addr::unchecked("terra1fmcjjt6yc9wqup2r06urnrd928jhrde6gcld6n".to_string()) + ); + assert_eq!( + config.lp_token, + Addr::unchecked("terra199vw7724lzkwz6lf2hsx04lrxfkz09tg8dlp6r".to_string()) + ); + assert_eq!(config.admin, info.sender); + assert_eq!(config.whitelisted_contracts, whitelisted_contracts); + + let admin_nominee = Config::may_load_admin_nominee(&deps.storage).unwrap(); + assert_eq!( + admin_nominee, + Some(Addr::unchecked( + "terra1e8ryd9ezefuucd4mje33zdms9m2s90m57878v9".to_string() + )) + ); +} + +#[test] +fn failed_invalid_permission() { + let mut deps = custom_deps(); + + let (_, mut info, _response) = default(&mut deps, vec![], None); + + info.sender = Addr::unchecked("terra1e8ryd9ezefuucd4mje33zdms9m2s90m57878v9"); + + let result = exec(&mut deps, info, None, None, None, None, None); + + expect_unauthorized_err(&result); +} diff --git a/contracts/stakings/staking_vkr/src/tests/validate.rs b/contracts/stakings/staking_vkr/src/tests/validate.rs new file mode 100644 index 0000000..1db7fdc --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/validate.rs @@ -0,0 +1,78 @@ +use crate::entrypoints::query; +use crate::errors::ContractError; +use crate::tests::bond::exec_bond; +use crate::tests::instantiate::default; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::unbond::exec_unbond; +use ap_valkyrie::staking_vkr::{QueryMsg, StakerInfoResponse}; +use cosmwasm_std::{from_binary, Env, Uint128}; + +fn query_staker_info(deps: &CustomDeps, env: Env, sender: String) -> StakerInfoResponse { + from_binary::( + &query( + deps.as_ref(), + env.clone(), + QueryMsg::StakerInfo { staker: sender }, + ) + .unwrap(), + ) + .unwrap() +} + +#[test] +fn calculation() { + let mut deps = custom_deps(); + + let (mut env, info, _response) = default(&mut deps, vec![], None); + + // try to bond 100 tokens without schedules + let err = exec_bond(&mut deps, &env, &info.sender, vec![], Uint128::new(100u128)).unwrap_err(); + assert_eq!(ContractError::SchedulesAmountError {}, err); + + exec_bond( + &mut deps, + &env, + &info.sender, + vec![(0, 100, Uint128::new(100))], + Uint128::new(100u128), + ) + .unwrap(); + env.block.height += 100; + + // try to register new schedule with wrong block height + let err = exec_bond( + &mut deps, + &env, + &info.sender, + vec![(0, 100, Uint128::new(100))], + Uint128::new(100u128), + ) + .unwrap_err(); + assert_eq!(ContractError::ScheduleError {}, err); + + exec_bond( + &mut deps, + &env, + &info.sender, + vec![(100, 200, Uint128::new(100))], + Uint128::new(100u128), + ) + .unwrap(); + + let res = query_staker_info(&deps, env.clone(), info.sender.to_string()); + assert_eq!(res.pending_reward, Uint128::new(100)); + assert_eq!(res.bond_amount, Uint128::new(200u128)); + + env.block.height += 10; + exec_unbond(&mut deps, env.clone(), info.clone(), Uint128::new(100u128)).unwrap(); + + let res = query_staker_info(&deps, env.clone(), info.sender.to_string()); + assert_eq!(res.pending_reward, Uint128::new(110)); + assert_eq!(res.bond_amount, Uint128::new(100u128)); + + env.block.height += 10; + + let res = query_staker_info(&deps, env.clone(), info.sender.to_string()); + assert_eq!(res.pending_reward, Uint128::new(120)); + assert_eq!(res.bond_amount, Uint128::new(100u128)); +} diff --git a/contracts/stakings/staking_vkr/src/tests/withdraw.rs b/contracts/stakings/staking_vkr/src/tests/withdraw.rs new file mode 100644 index 0000000..28b6ad8 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/tests/withdraw.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, Env, MessageInfo, Response, SubMsg, Uint128, WasmMsg, +}; +use cw20::Cw20ExecuteMsg; + +use crate::executions::withdraw; +use crate::tests::instantiate::default; +use crate::tests::mock_querier::{custom_deps, CustomDeps}; +use crate::tests::test_utils::{LP_WHITELISTED1, VALKYRIE_TOKEN}; +use crate::ContractResult; + +pub fn exec(deps: &mut CustomDeps, env: Env, info: MessageInfo) -> ContractResult { + withdraw(deps.as_mut(), env, info) +} + +fn will_success(deps: &mut CustomDeps) -> Response { + let (mut env, info, _response) = default( + deps, + vec![(0, 100, Uint128::new(1000))], + Some(Uint128::new(1000u128)), + ); + env.block.height = 20; + exec(deps, env, info).unwrap() +} + +#[test] +fn succeed() { + let mut deps = custom_deps(); + + let res = will_success(&mut deps); + + assert_eq!( + res.messages, + vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: Addr::unchecked(VALKYRIE_TOKEN).to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: Addr::unchecked(LP_WHITELISTED1).to_string(), + amount: Uint128::new(200u128), + }) + .unwrap(), + funds: vec![], + }))] + ); +} diff --git a/contracts/stakings/staking_vkr/src/utils.rs b/contracts/stakings/staking_vkr/src/utils.rs new file mode 100644 index 0000000..481b044 --- /dev/null +++ b/contracts/stakings/staking_vkr/src/utils.rs @@ -0,0 +1,30 @@ +use crate::errors::ContractError; +use crate::states::Config; +use cosmwasm_std::Uint128; + +pub fn register_schedules( + mut current_block: u64, + config: &mut Config, + schedules: Vec<(u64, u64, Uint128)>, + amount: Uint128, +) -> Result<(), ContractError> { + let mut to_deposit = Uint128::zero(); + + for schedule in schedules { + to_deposit = to_deposit.checked_add(schedule.2)?; + + if !(schedule.0 >= current_block && schedule.0 < schedule.1 && schedule.1 > current_block) { + return Err(ContractError::ScheduleError {}); + } + + config.distribution_schedule.push(schedule); + // start block of next schedule should be higher then end block of previous schedule + current_block = schedule.1; + } + + if to_deposit != amount { + return Err(ContractError::SchedulesAmountError {}); + } + + Ok(()) +} diff --git a/packages/valkyrie/Cargo.toml b/packages/valkyrie/Cargo.toml index c36e2a7..447a003 100644 --- a/packages/valkyrie/Cargo.toml +++ b/packages/valkyrie/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ap-valkyrie" -version = "1.0.0" +version = "1.1.0" authors = ["Terraform Labs, PTE."] edition = "2021" description = "Common astroport types" @@ -18,4 +18,5 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" cosmwasm-schema = "1.1" +cw20 = { version = "1.1" } diff --git a/packages/valkyrie/src/lib.rs b/packages/valkyrie/src/lib.rs index e11afc4..1239429 100644 --- a/packages/valkyrie/src/lib.rs +++ b/packages/valkyrie/src/lib.rs @@ -1,6 +1 @@ -use cosmwasm_schema::cw_serde; - -/// This structure describes a migration message. -/// We currently take no arguments for migrations -#[cw_serde] -pub struct MigrateMsg {} +pub mod staking_vkr; diff --git a/packages/valkyrie/src/staking_vkr.rs b/packages/valkyrie/src/staking_vkr.rs new file mode 100644 index 0000000..09d5d83 --- /dev/null +++ b/packages/valkyrie/src/staking_vkr.rs @@ -0,0 +1,79 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; + +use cosmwasm_std::{Decimal, Uint128}; +use cw20::Cw20ReceiveMsg; + +#[cw_serde] +pub struct InstantiateMsg { + pub token: String, + pub pair: String, + pub lp_token: String, + pub whitelisted_contracts: Vec, +} + +#[cw_serde] +pub struct ConfigResponse { + pub admin: String, + pub token: String, + pub pair: String, + pub lp_token: String, + pub whitelisted_contracts: Vec, + pub distribution_schedule: Vec<(u64, u64, Uint128)>, +} + +#[cw_serde] +pub struct StateResponse { + pub last_distributed: u64, + pub total_bond_amount: Uint128, + pub global_reward_index: Decimal, +} + +#[cw_serde] +pub struct StakerInfoResponse { + pub staker: String, + pub reward_index: Decimal, + pub bond_amount: Uint128, + pub pending_reward: Uint128, +} + +#[cw_serde] +pub enum Cw20HookMsg { + Bond { schedules: Vec<(u64, u64, Uint128)> }, +} + +/// We currently take no arguments for migrations +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + Receive(Cw20ReceiveMsg), + Unbond { + amount: Uint128, + }, + /// Withdraw pending rewards + Withdraw {}, + UpdateConfig { + token: Option, + pair: Option, + lp_token: Option, + admin: Option, + whitelisted_contracts: Option>, + }, + MigrateReward { + recipient: String, + amount: Uint128, + }, + ApproveAdminNominee {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(ConfigResponse)] + Config {}, + #[returns(StateResponse)] + State { block_height: Option }, + #[returns(StakerInfoResponse)] + StakerInfo { staker: String }, +}