diff --git a/Cargo.lock b/Cargo.lock index aec608d4b..965f46eb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,16 +28,6 @@ dependencies = [ "cosmwasm-std", ] -[[package]] -name = "arrayvec" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06f59fe10306bb78facd90d28c2038ad23ffaaefa85bac43c8a434cde383334f" -dependencies = [ - "nodrop", - "odds", -] - [[package]] name = "astro-satellite-package" version = "0.1.0" @@ -50,7 +40,7 @@ dependencies = [ [[package]] name = "astroport" -version = "3.6.1" +version = "3.8.0" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -91,7 +81,7 @@ dependencies = [ "cw20-ics20", "schemars", "semver", - "serde 1.0.180", + "serde", "thiserror", ] @@ -138,7 +128,7 @@ dependencies = [ "cosmos-sdk-proto", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.16.5", + "cw-multi-test 0.16.5 (git+https://github.com/astroport-fi/cw-multi-test.git?rev=269a2c829d1ad25d67caa4600f72d2a21fb8fdeb)", "cw-storage-plus 0.15.1", "cw-utils 1.0.1", "cw2 1.1.0", @@ -206,6 +196,30 @@ dependencies = [ "cw20 0.15.1", ] +[[package]] +name = "astroport-incentives" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport", + "astroport-factory", + "astroport-native-coin-registry", + "astroport-pair", + "astroport-pair-stable", + "astroport-vesting", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.16.5 (git+https://github.com/astroport-fi/cw-multi-test?branch=astroport_cozy_fork)", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", + "cw2 1.1.0", + "cw20 1.1.0", + "cw20-base 1.1.0", + "itertools 0.11.0", + "proptest", + "thiserror", +] + [[package]] name = "astroport-liquidity-manager" version = "1.0.3" @@ -221,13 +235,13 @@ dependencies = [ "astroport-whitelist", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.16.5", + "cw-multi-test 0.16.5 (git+https://github.com/astroport-fi/cw-multi-test.git?rev=269a2c829d1ad25d67caa4600f72d2a21fb8fdeb)", "cw-storage-plus 1.1.0", "cw20 0.15.1", "cw20-base 0.15.1", "derivative", "itertools 0.10.5", - "serde_json 1.0.104", + "serde_json", "thiserror", ] @@ -264,7 +278,6 @@ dependencies = [ "astroport-native-coin-registry", "astroport-pair", "astroport-pair-concentrated", - "astroport-pair-concentrated-injective", "astroport-pair-stable", "astroport-shared-multisig", "astroport-staking", @@ -274,13 +287,13 @@ dependencies = [ "astroport-xastro-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.16.5", + "cw-multi-test 0.16.5 (git+https://github.com/astroport-fi/cw-multi-test.git?rev=269a2c829d1ad25d67caa4600f72d2a21fb8fdeb)", "cw-utils 1.0.1", "cw20 0.15.1", "cw3", "injective-cosmwasm", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -415,31 +428,7 @@ dependencies = [ [[package]] name = "astroport-pair-concentrated" -version = "2.2.0" -dependencies = [ - "anyhow", - "astroport", - "astroport-circular-buffer", - "astroport-factory", - "astroport-mocks", - "astroport-native-coin-registry", - "astroport-pcl-common", - "astroport-token", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "derivative", - "itertools 0.10.5", - "proptest", - "thiserror", -] - -[[package]] -name = "astroport-pair-concentrated-injective" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "astroport", @@ -447,7 +436,6 @@ dependencies = [ "astroport-factory", "astroport-mocks", "astroport-native-coin-registry", - "astroport-pair-concentrated", "astroport-pcl-common", "astroport-token", "cosmwasm-schema", @@ -457,19 +445,14 @@ dependencies = [ "cw2 0.15.1", "cw20 0.15.1", "derivative", - "hex", - "injective-cosmwasm", - "injective-math", - "injective-testing", "itertools 0.10.5", "proptest", "thiserror", - "tiny-keccak 2.0.2", ] [[package]] name = "astroport-pair-stable" -version = "3.3.0" +version = "3.4.0" dependencies = [ "anyhow", "astroport", @@ -494,7 +477,7 @@ dependencies = [ [[package]] name = "astroport-pcl-common" -version = "1.0.0" +version = "1.1.0" dependencies = [ "anyhow", "astroport", @@ -509,7 +492,7 @@ dependencies = [ [[package]] name = "astroport-router" -version = "1.1.2" +version = "1.2.0" dependencies = [ "anyhow", "astroport", @@ -732,7 +715,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -800,22 +783,22 @@ dependencies = [ [[package]] name = "cosmwasm-schema" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c337e097a089e5b52b5d914a7ff6613332777f38ea6d9d36e1887cd0baa72e" +checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" dependencies = [ "cosmwasm-schema-derive", "schemars", - "serde 1.0.180", - "serde_json 1.0.104", + "serde", + "serde_json", "thiserror", ] [[package]] name = "cosmwasm-schema-derive" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766cc9e7c1762d8fc9c0265808910fcad755200cd0e624195a491dd885a61169" +checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" dependencies = [ "proc-macro2", "quote", @@ -836,7 +819,7 @@ dependencies = [ "forward_ref", "hex", "schemars", - "serde 1.0.180", + "serde", "serde-json-wasm", "sha2 0.10.7", "thiserror", @@ -849,7 +832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "800aaddd70ba915e19bf3d2d992aa3689d8767857727fdd3b414df4fd52d2aa1" dependencies = [ "cosmwasm-std", - "serde 1.0.180", + "serde", ] [[package]] @@ -918,7 +901,7 @@ dependencies = [ "cw-storage-plus 0.13.4", "cw-utils 0.13.4", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -933,7 +916,7 @@ dependencies = [ "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -952,7 +935,25 @@ dependencies = [ "itertools 0.10.5", "prost 0.9.0", "schemars", - "serde 1.0.180", + "serde", + "thiserror", +] + +[[package]] +name = "cw-multi-test" +version = "0.16.5" +source = "git+https://github.com/astroport-fi/cw-multi-test?branch=astroport_cozy_fork#08a11aa26f9f35b41f707e803d4cdec38fd2e78d" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "derivative", + "itertools 0.11.0", + "prost 0.11.9", + "schemars", + "serde", + "sha2 0.10.7", "thiserror", ] @@ -970,7 +971,7 @@ dependencies = [ "k256", "prost 0.9.0", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -982,7 +983,7 @@ checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -993,7 +994,7 @@ checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1004,7 +1005,7 @@ checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1015,7 +1016,7 @@ checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1026,7 +1027,7 @@ checksum = "ef842a1792e4285beff7b3b518705f760fa4111dc1e296e53f3e92d1ef7f6220" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1038,7 +1039,7 @@ checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1053,7 +1054,7 @@ dependencies = [ "cw2 0.15.1", "schemars", "semver", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1068,7 +1069,7 @@ dependencies = [ "cw2 1.1.0", "schemars", "semver", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1081,7 +1082,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1097,7 +1098,7 @@ dependencies = [ "cw1", "cw2 0.15.1", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1110,7 +1111,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.11.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1122,7 +1123,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.13.4", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1135,7 +1136,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1148,7 +1149,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 1.1.0", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1161,7 +1162,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.11.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1173,7 +1174,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.13.4", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1186,7 +1187,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.15.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1199,7 +1200,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 1.0.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1214,7 +1215,7 @@ dependencies = [ "cw2 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1232,7 +1233,25 @@ dependencies = [ "cw20 0.15.1", "schemars", "semver", - "serde 1.0.180", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-base" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3ad456059901a36cfa68b596d85d579c3df2b797dae9950dc34c27e14e995f" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "cw20 1.1.0", + "schemars", + "semver", + "serde", "thiserror", ] @@ -1250,7 +1269,7 @@ dependencies = [ "cw20 0.13.4", "schemars", "semver", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1265,7 +1284,7 @@ dependencies = [ "cw-utils 1.0.1", "cw20 1.1.0", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1279,7 +1298,7 @@ dependencies = [ "cosmwasm-std", "cw-utils 0.15.1", "schemars", - "serde 1.0.180", + "serde", ] [[package]] @@ -1295,7 +1314,7 @@ dependencies = [ "cw2 0.15.1", "cw721", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -1380,7 +1399,7 @@ dependencies = [ "hashbrown", "hex", "rand_core 0.6.4", - "serde 1.0.180", + "serde", "sha2 0.9.9", "zeroize", ] @@ -1442,7 +1461,7 @@ dependencies = [ "fixed-hash", "impl-rlp", "impl-serde", - "tiny-keccak 1.5.0", + "tiny-keccak", ] [[package]] @@ -1455,7 +1474,7 @@ dependencies = [ "ethbloom", "ethereum-types-serialize", "fixed-hash", - "serde 1.0.180", + "serde", "uint 0.5.0", ] @@ -1465,7 +1484,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1873d77b32bc1891a79dad925f2acbc318ee942b38b9110f9dbc5fbeffcea350" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -1524,12 +1543,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -[[package]] -name = "gcc" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" - [[package]] name = "generator-controller" version = "1.3.0" @@ -1617,7 +1630,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -1644,7 +1657,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58e3cae7e99c7ff5a995da2cf78dd0a5383740eda71d98cf7b1910c301ac69b8" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -1665,10 +1678,10 @@ dependencies = [ "hex", "injective-math", "schemars", - "serde 1.0.180", + "serde", "serde_repr", "subtle-encoding", - "tiny-keccak 1.5.0", + "tiny-keccak", ] [[package]] @@ -1680,30 +1693,12 @@ dependencies = [ "bigint", "cosmwasm-std", "ethereum-types", - "num 0.4.1", + "num", "schemars", - "serde 1.0.180", + "serde", "subtle-encoding", ] -[[package]] -name = "injective-testing" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980b8e23acb5310a44ea725fe05c15ba01d3e00bbb3725076373ae27111efa3d" -dependencies = [ - "anyhow", - "base64", - "cosmwasm-std", - "cw-multi-test 0.16.5", - "injective-cosmwasm", - "injective-math", - "rand 0.4.6", - "secp256k1", - "serde 1.0.180", - "tiny-keccak 1.5.0", -] - [[package]] name = "integer-sqrt" version = "0.1.5" @@ -1792,52 +1787,20 @@ dependencies = [ "autocfg", ] -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - -[[package]] -name = "num" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" -dependencies = [ - "num-bigint 0.1.44", - "num-complex 0.1.43", - "num-integer", - "num-iter", - "num-rational 0.1.42", - "num-traits", -] - [[package]] name = "num" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" dependencies = [ - "num-bigint 0.4.3", - "num-complex 0.4.3", + "num-bigint", + "num-complex", "num-integer", "num-iter", - "num-rational 0.4.1", + "num-rational", "num-traits", ] -[[package]] -name = "num-bigint" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.4.6", - "rustc-serialize", -] - [[package]] name = "num-bigint" version = "0.4.3" @@ -1849,16 +1812,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-complex" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656" -dependencies = [ - "num-traits", - "rustc-serialize", -] - [[package]] name = "num-complex" version = "0.4.3" @@ -1900,18 +1853,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.1.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" -dependencies = [ - "num-bigint 0.1.44", - "num-integer", - "num-traits", - "rustc-serialize", -] - [[package]] name = "num-rational" version = "0.4.1" @@ -1919,7 +1860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", - "num-bigint 0.4.3", + "num-bigint", "num-integer", "num-traits", ] @@ -1934,12 +1875,6 @@ dependencies = [ "libm", ] -[[package]] -name = "odds" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eae0151b9dacf24fcc170d9995e511669a082856a91f958a2fe380bfab3fb22" - [[package]] name = "once_cell" version = "1.18.0" @@ -2032,13 +1967,13 @@ dependencies = [ [[package]] name = "proptest" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", - "bitflags 1.3.2", - "byteorder", + "bit-vec", + "bitflags 2.3.3", "lazy_static", "num-traits", "rand 0.8.5", @@ -2189,29 +2124,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" -dependencies = [ - "libc", - "rand 0.4.6", -] - -[[package]] -name = "rand" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -dependencies = [ - "fuchsia-cprng", - "libc", - "rand_core 0.3.1", - "rdrand", - "winapi", -] - [[package]] name = "rand" version = "0.5.6" @@ -2285,15 +2197,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rdrand" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" -dependencies = [ - "rand_core 0.3.1", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -2305,9 +2208,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rfc6979" @@ -2335,12 +2238,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" -[[package]] -name = "rustc-serialize" -version = "0.3.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" - [[package]] name = "rustix" version = "0.38.6" @@ -2380,8 +2277,8 @@ checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" dependencies = [ "dyn-clone", "schemars_derive", - "serde 1.0.180", - "serde_json 1.0.104", + "serde", + "serde_json", ] [[package]] @@ -2416,36 +2313,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10915a2fa4f8016ed747eb847f096b0d44b22c6b624a36d3cc76964f6af4821a" -dependencies = [ - "arrayvec", - "gcc", - "libc", - "rand 0.3.23", - "rustc-serialize", - "serde 0.6.15", - "serde_json 0.6.1", -] - [[package]] name = "semver" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" -[[package]] -name = "serde" -version = "0.6.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c97b18e9e53de541f11e497357d6c5eaeb39f0cb9c8734e274abe4935f6991fa" -dependencies = [ - "num 0.1.42", -] - [[package]] name = "serde" version = "1.0.180" @@ -2461,7 +2334,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -2470,7 +2343,7 @@ version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ - "serde 1.0.180", + "serde", ] [[package]] @@ -2495,16 +2368,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "serde_json" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aaee47e038bf9552d30380d3973fff2593ee0a76d81ad4c581f267cdcadf36" -dependencies = [ - "num 0.1.42", - "serde 0.6.15", -] - [[package]] name = "serde_json" version = "1.0.104" @@ -2513,7 +2376,7 @@ checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", - "serde 1.0.180", + "serde", ] [[package]] @@ -2686,7 +2549,7 @@ dependencies = [ "num-traits", "prost 0.11.9", "prost-types", - "serde 1.0.180", + "serde", "serde_bytes", "subtle-encoding", "time", @@ -2782,15 +2645,6 @@ dependencies = [ "crunchy 0.2.2", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy 0.2.2", -] - [[package]] name = "typenum" version = "1.16.0" @@ -2850,7 +2704,7 @@ dependencies = [ "cw-storage-plus 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] @@ -2863,7 +2717,7 @@ dependencies = [ "cw-storage-plus 0.11.1", "cw20 0.11.1", "schemars", - "serde 1.0.180", + "serde", "valkyrie", ] @@ -2878,7 +2732,7 @@ dependencies = [ "cw20 0.11.1", "cw20-base 0.11.1", "schemars", - "serde 1.0.180", + "serde", "thiserror", ] diff --git a/Cargo.toml b/Cargo.toml index 50a817b10..fffd676a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,19 +6,14 @@ members = [ "contracts/pair", "contracts/pair_stable", "contracts/pair_concentrated", - "contracts/pair_concentrated_inj", +# "contracts/pair_concentrated_inj", TODO: rewrite OB liquidity deployment "contracts/pair_astro_xastro", "contracts/router", "contracts/token", "contracts/whitelist", "contracts/cw20_ics20", "templates/*", - "contracts/tokenomics/generator", - "contracts/tokenomics/maker", - "contracts/tokenomics/staking", - "contracts/tokenomics/vesting", - "contracts/tokenomics/xastro_token", - "contracts/tokenomics/xastro_outpost_token", + "contracts/tokenomics/*", "contracts/periphery/*", ] diff --git a/contracts/pair/src/testing.rs b/contracts/pair/src/testing.rs index 37ff5de9d..9755bb39a 100644 --- a/contracts/pair/src/testing.rs +++ b/contracts/pair/src/testing.rs @@ -391,7 +391,7 @@ fn provide_liquidity() { assert_eq!( res, ContractError::Std(StdError::generic_err( - "Native token balance mismatch between the argument and the transferred", + "Native token balance mismatch between the argument (50000000000000000000uusd) and the transferred (100000000000000000000uusd)", )) ); diff --git a/contracts/pair/tests/integration.rs b/contracts/pair/tests/integration.rs index a7851199b..1486973bd 100644 --- a/contracts/pair/tests/integration.rs +++ b/contracts/pair/tests/integration.rs @@ -2179,3 +2179,93 @@ fn test_fee_share( + acceptable_spread_amount ); } + +#[test] +fn test_provide_liquidity_without_funds() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2_00_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::from(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair_instance.to_string(), &QueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // provide some liquidity to assume contract have funds (to prevent underflow err) + let (msg, coins) = provide_liquidity_msg( + Uint128::new(100_000_000), + Uint128::new(100_000_000), + None, + None, + ); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // provide liquidity without funds + let err = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Native token balance mismatch between the argument (100000000uusd) and the transferred (0uusd)" + ); +} diff --git a/contracts/pair_concentrated/Cargo.toml b/contracts/pair_concentrated/Cargo.toml index 1d80b4d23..9f4a6c0a0 100644 --- a/contracts/pair_concentrated/Cargo.toml +++ b/contracts/pair_concentrated/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated" -version = "2.2.0" +version = "2.3.0" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair" diff --git a/contracts/pair_concentrated/src/queries.rs b/contracts/pair_concentrated/src/queries.rs index 053c9b150..7b5e70bd2 100644 --- a/contracts/pair_concentrated/src/queries.rs +++ b/contracts/pair_concentrated/src/queries.rs @@ -339,11 +339,9 @@ mod testing { let array = (1..=30) .into_iter() .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), + ts: env.block.time.seconds() + i * 1000, + price_sma: Decimal::from_ratio(i, i * i), + price: Default::default(), }) .collect_vec(); buffer.push_many(&array); @@ -389,11 +387,9 @@ mod testing { let array = (1..=30) .into_iter() .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), + ts: env.block.time.seconds() + i * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i, i * i), }) .collect_vec(); buffer.push_many(&array); @@ -433,11 +429,9 @@ mod testing { let array = (1..=CAPACITY * 3) .into_iter() .map(|i| Observation { - timestamp: ts + i as u64 * 1000, - base_sma: Default::default(), - base_amount: (i * i).into(), - quote_sma: Default::default(), - quote_amount: i.into(), + ts: ts + i as u64 * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i * i, i), }) .collect_vec(); diff --git a/contracts/pair_concentrated/src/utils.rs b/contracts/pair_concentrated/src/utils.rs index 56d0d1c8a..ca4489c20 100644 --- a/contracts/pair_concentrated/src/utils.rs +++ b/contracts/pair_concentrated/src/utils.rs @@ -1,12 +1,12 @@ -use cosmwasm_std::{Addr, Env, QuerierWrapper, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Decimal, Env, QuerierWrapper, StdResult, Storage, Uint128}; use astroport::asset::{Asset, DecimalAsset}; +use astroport::observation::{safe_sma_buffer_not_full, safe_sma_calculation}; use astroport::observation::{Observation, PrecommitObservation}; use astroport::querier::query_supply; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; use astroport_pcl_common::state::{Config, Precisions}; -use astroport_pcl_common::utils::{safe_sma_buffer_not_full, safe_sma_calculation}; use crate::error::ContractError; use crate::state::OBSERVATIONS; @@ -43,7 +43,7 @@ pub(crate) fn query_pools( .collect() } -/// Calculate and save moving averages of swap sizes. +/// Calculate and save price moving average pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { if let Some(PrecommitObservation { base_amount, @@ -52,45 +52,35 @@ pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResu }) = PrecommitObservation::may_load(storage)? { let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + let observed_price = Decimal::from_ratio(base_amount, quote_amount); let new_observation; if let Some(last_obs) = buffer.read_last(storage)? { // Skip saving observation if it has been already saved - if last_obs.timestamp < precommit_ts { + if last_obs.ts < precommit_ts { // Since this is circular buffer the next index contains the oldest value let count = buffer.capacity(); if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { - let new_base_sma = safe_sma_calculation( - last_obs.base_sma, - oldest_obs.base_amount, + let price_sma = safe_sma_calculation( + last_obs.price_sma, + oldest_obs.price, count, - base_amount, - )?; - let new_quote_sma = safe_sma_calculation( - last_obs.quote_sma, - oldest_obs.quote_amount, - count, - quote_amount, + observed_price, )?; new_observation = Observation { - base_amount, - quote_amount, - base_sma: new_base_sma, - quote_sma: new_quote_sma, - timestamp: precommit_ts, + ts: precommit_ts, + price: observed_price, + price_sma, }; } else { // Buffer is not full yet let count = buffer.head(); - let base_sma = safe_sma_buffer_not_full(last_obs.base_sma, count, base_amount)?; - let quote_sma = - safe_sma_buffer_not_full(last_obs.quote_sma, count, quote_amount)?; + let price_sma = + safe_sma_buffer_not_full(last_obs.price_sma, count, observed_price)?; new_observation = Observation { - base_amount, - quote_amount, - base_sma, - quote_sma, - timestamp: precommit_ts, + ts: precommit_ts, + price: observed_price, + price_sma, }; } @@ -100,11 +90,9 @@ pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResu // Buffer is empty if env.block.time.seconds() > precommit_ts { new_observation = Observation { - timestamp: precommit_ts, - base_sma: base_amount, - base_amount, - quote_sma: quote_amount, - quote_amount, + ts: precommit_ts, + price: observed_price, + price_sma: observed_price, }; buffer.instant_push(storage, &new_observation)? @@ -117,11 +105,18 @@ pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResu #[cfg(test)] mod tests { + use std::fmt::Display; + use std::str::FromStr; + use cosmwasm_std::testing::{mock_env, MockStorage}; use cosmwasm_std::{BlockInfo, Timestamp}; use super::*; + pub fn dec_to_f64(val: impl Display) -> f64 { + f64::from_str(&val.to_string()).unwrap() + } + #[test] fn test_swap_observations() { let mut store = MockStorage::new(); @@ -144,9 +139,9 @@ mod tests { let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); let obs = buffer.read_last(&store).unwrap().unwrap(); - assert_eq!(obs.timestamp, 50); + assert_eq!(obs.ts, 50); assert_eq!(buffer.head(), 0); - assert_eq!(obs.base_sma.u128(), 1000u128); - assert_eq!(obs.quote_sma.u128(), 500u128); + assert_eq!(dec_to_f64(obs.price_sma), 2.0); + assert_eq!(dec_to_f64(obs.price), 2.0); } } diff --git a/contracts/pair_concentrated/tests/helper.rs b/contracts/pair_concentrated/tests/helper.rs index 3a13b4dee..e7743ea8f 100644 --- a/contracts/pair_concentrated/tests/helper.rs +++ b/contracts/pair_concentrated/tests/helper.rs @@ -31,9 +31,7 @@ use astroport_pair_concentrated::contract::{execute, instantiate, reply}; use astroport_pair_concentrated::queries::query; use astroport_pcl_common::state::Config; -const NATIVE_TOKEN_PRECISION: u8 = 6; - -const INIT_BALANCE: u128 = 1_000_000_000000; +const INIT_BALANCE: u128 = u128::MAX; pub fn common_pcl_params() -> ConcentratedPoolParams { ConcentratedPoolParams { @@ -99,7 +97,7 @@ pub fn init_native_coins(test_coins: &[TestCoin]) -> Vec { .iter() .filter_map(|test_coin| match test_coin { TestCoin::Native(name) => { - let init_balance = INIT_BALANCE * 10u128.pow(NATIVE_TOKEN_PRECISION as u32); + let init_balance = INIT_BALANCE; Some(coin(init_balance, name)) } _ => None, @@ -214,7 +212,12 @@ impl Helper { owner.clone(), coin_registry_address.clone(), &astroport::native_coin_registry::ExecuteMsg::Add { - native_coins: vec![("uluna".to_owned(), 6), ("uusd".to_owned(), 6)], + native_coins: vec![ + ("uluna".to_owned(), 6), + ("uusd".to_owned(), 6), + ("wsteth".to_owned(), 18), + ("eth".to_owned(), 18), + ], }, &[], ) @@ -414,7 +417,7 @@ impl Helper { decimals: u8, owner: &Addr, ) -> Addr { - let init_balance = INIT_BALANCE * 10u128.pow(decimals as u32); + let init_balance = INIT_BALANCE; app.instantiate_contract( token_code, owner.clone(), diff --git a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs index 567b743a3..fe70ecb42 100644 --- a/contracts/pair_concentrated/tests/pair_concentrated_integration.rs +++ b/contracts/pair_concentrated/tests/pair_concentrated_integration.rs @@ -1748,3 +1748,115 @@ fn check_small_trades_18decimals() { relative_diff ); } + +#[test] +fn check_lsd_swaps_with_price_update() { + let owner = Addr::unchecked("owner"); + let half = Decimal::from_ratio(1u8, 2u8); + let price_scale = 0.87; + + let test_coins = vec![TestCoin::native("wsteth"), TestCoin::native("eth")]; + + // checking swaps in PCL pair with LSD params + let params = ConcentratedPoolParams { + amp: f64_to_dec(500_f64), + gamma: f64_to_dec(0.00000001), + mid_fee: f64_to_dec(0.0003), + out_fee: f64_to_dec(0.0045), + fee_gamma: f64_to_dec(0.3), + repeg_profit_threshold: f64_to_dec(0.00000001), + min_price_scale_delta: f64_to_dec(0.0000055), + price_scale: f64_to_dec(price_scale), + ma_half_time: 600, + track_asset_balances: None, + fee_share: None, + }; + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + helper.app.next_block(1000); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance((1e18 * price_scale) as u128), + helper.assets[&test_coins[1]].with_balance(1e18 as u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + + for _ in 0..10 { + let assets = vec![ + helper.assets[&test_coins[0]].with_balance((1e15 * price_scale) as u128), + helper.assets[&test_coins[1]].with_balance(1e15 as u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + } + + for _ in 0..10 { + let assets = vec![ + helper.assets[&test_coins[0]].with_balance((1e13 * price_scale) as u128), + helper.assets[&test_coins[1]].with_balance(1e13 as u128), + ]; + helper.provide_liquidity(&owner, &assets).unwrap(); + helper.app.next_block(1000); + } + + let user1 = Addr::unchecked("user1"); + let offer_asset = helper.assets[&test_coins[0]].with_balance(1e16 as u128); + + for _ in 0..10 { + helper.give_me_money(&[offer_asset.clone()], &user1); + helper.swap(&user1, &offer_asset, Some(half)).unwrap(); + helper.app.next_block(1000); + } + + let offer_asset = helper.assets[&test_coins[1]].with_balance(1e16 as u128); + for _ in 0..10 { + helper.give_me_money(&[offer_asset.clone()], &user1); + helper.swap(&user1, &offer_asset, Some(half)).unwrap(); + helper.app.next_block(1000); + } +} + +#[test] +fn test_provide_liquidity_without_funds() { + let owner = Addr::unchecked("owner"); + + let test_coins = vec![TestCoin::native("uluna"), TestCoin::cw20("USDC")]; + + let params = ConcentratedPoolParams { + price_scale: Decimal::from_ratio(2u8, 1u8), + ..common_pcl_params() + }; + + let mut helper = Helper::new(&owner, test_coins.clone(), params).unwrap(); + + let user1 = Addr::unchecked("user1"); + + let assets = vec![ + helper.assets[&test_coins[0]].with_balance(100_000_000000u128), + helper.assets[&test_coins[1]].with_balance(50_000_000000u128), + ]; + + // provide some liquidity + for _ in 0..3 { + helper.give_me_money(&assets, &user1); + helper.provide_liquidity(&user1, &assets).unwrap(); + } + + let msg = ExecuteMsg::ProvideLiquidity { + assets: assets.clone().to_vec(), + slippage_tolerance: Some(f64_to_dec(0.5)), + auto_stake: None, + receiver: None, + }; + + let err = helper + .app + .execute_contract(user1.clone(), helper.pair_addr.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Native token balance mismatch between the argument (100000000000uluna) and the transferred (0uluna)" + ) +} diff --git a/contracts/pair_concentrated_inj/Cargo.toml b/contracts/pair_concentrated_inj/Cargo.toml index cd77807df..2832fa854 100644 --- a/contracts/pair_concentrated_inj/Cargo.toml +++ b/contracts/pair_concentrated_inj/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-concentrated-injective" -version = "2.2.0" +version = "2.2.2" authors = ["Astroport"] edition = "2021" description = "The Astroport concentrated liquidity pair which supports Injective orderbook integration" diff --git a/contracts/pair_concentrated_inj/src/migrate.rs b/contracts/pair_concentrated_inj/src/migrate.rs index 06211d10f..7ec3aa42b 100644 --- a/contracts/pair_concentrated_inj/src/migrate.rs +++ b/contracts/pair_concentrated_inj/src/migrate.rs @@ -12,7 +12,7 @@ use crate::orderbook::state::OrderbookState; use crate::state::CONFIG; const MIGRATE_FROM: &str = "astroport-pair-concentrated"; -const MIGRATION_VERSION: &str = "2.2.0"; +const MIGRATION_VERSION: &str = "2.2.2"; /// Manages the contract migration. #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/pair_concentrated_inj/src/queries.rs b/contracts/pair_concentrated_inj/src/queries.rs index 6bdcb22bf..5f71ddfd3 100644 --- a/contracts/pair_concentrated_inj/src/queries.rs +++ b/contracts/pair_concentrated_inj/src/queries.rs @@ -379,7 +379,7 @@ mod testing { let array = (1..=CAPACITY * 3) .into_iter() .map(|i| Observation { - timestamp: ts + i as u64 * 1000, + ts: ts + i as u64 * 1000, base_sma: Default::default(), base_amount: (i * i).into(), quote_sma: Default::default(), @@ -441,7 +441,7 @@ mod testing { let array = (1..=30) .into_iter() .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, + ts: env.block.time.seconds() + i * 1000, base_sma: Default::default(), base_amount: i.into(), quote_sma: Default::default(), @@ -491,7 +491,7 @@ mod testing { let array = (1..=30) .into_iter() .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, + ts: env.block.time.seconds() + i * 1000, base_sma: Default::default(), base_amount: i.into(), quote_sma: Default::default(), diff --git a/contracts/pair_concentrated_inj/src/utils.rs b/contracts/pair_concentrated_inj/src/utils.rs index ca81b033d..80f087e5c 100644 --- a/contracts/pair_concentrated_inj/src/utils.rs +++ b/contracts/pair_concentrated_inj/src/utils.rs @@ -95,7 +95,7 @@ pub fn accumulate_swap_sizes( let new_observation; if let Some(last_obs) = buffer.read_last(storage)? { // Skip saving observation if it has been already saved - if last_obs.timestamp < precommit_ts { + if last_obs.ts < precommit_ts { // Since this is circular buffer the next index contains the oldest value let count = buffer.capacity(); if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { @@ -116,7 +116,7 @@ pub fn accumulate_swap_sizes( quote_amount, base_sma: new_base_sma, quote_sma: new_quote_sma, - timestamp: precommit_ts, + ts: precommit_ts, }; } else { // Buffer is not full yet @@ -129,7 +129,7 @@ pub fn accumulate_swap_sizes( quote_amount, base_sma, quote_sma, - timestamp: precommit_ts, + ts: precommit_ts, }; } @@ -144,7 +144,7 @@ pub fn accumulate_swap_sizes( // Buffer is empty if env.block.time.seconds() > precommit_ts { new_observation = Observation { - timestamp: precommit_ts, + ts: precommit_ts, base_sma: base_amount, base_amount, quote_sma: quote_amount, @@ -203,7 +203,7 @@ mod tests { let buffer = BufferManager::new(&store, OBSERVATIONS).unwrap(); let obs = buffer.read_last(&store).unwrap().unwrap(); - assert_eq!(obs.timestamp, 50); + assert_eq!(obs.ts, 50); assert_eq!(buffer.head(), 0); assert_eq!(obs.base_sma.u128(), 1000u128); assert_eq!(obs.quote_sma.u128(), 500u128); diff --git a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs index 25518eb2b..bb12b7c5d 100644 --- a/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs +++ b/contracts/pair_concentrated_inj/tests/pair_inj_concentrated_integration.rs @@ -132,7 +132,7 @@ fn provide_and_withdraw() { helper.give_me_money(&wrong_assets, &user1); let err = helper.provide_liquidity(&user1, &wrong_assets).unwrap_err(); assert_eq!( - "Generic error: Asset random-coin is not in the pool", + "Generic error: Unexpected asset random-coin", err.root_cause().to_string() ); @@ -147,7 +147,7 @@ fn provide_and_withdraw() { ) .unwrap_err(); assert_eq!( - "Generic error: Asset random-coin is not in the pool", + "Generic error: Unexpected asset random-coin", err.root_cause().to_string() ); diff --git a/contracts/pair_stable/Cargo.toml b/contracts/pair_stable/Cargo.toml index 25a0af206..e34779e69 100644 --- a/contracts/pair_stable/Cargo.toml +++ b/contracts/pair_stable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pair-stable" -version = "3.3.0" +version = "3.4.0" authors = ["Astroport"] edition = "2021" description = "The Astroport stableswap pair contract implementation" diff --git a/contracts/pair_stable/src/contract.rs b/contracts/pair_stable/src/contract.rs index d179b95e2..0c75c7aea 100644 --- a/contracts/pair_stable/src/contract.rs +++ b/contracts/pair_stable/src/contract.rs @@ -702,7 +702,7 @@ pub fn swap( // Store time series data in precommit observation. // Skipping small unsafe values which can seriously mess oracle price due to rounding errors. - // This data will be reflected in observations in the next action. + // This data will be reflected in observations on the next action. let ask_precision = get_precision(deps.storage, &ask_pool.info)?; if offer_asset_dec.amount >= MIN_TRADE_SIZE && return_amount.to_decimal256(ask_precision)? >= MIN_TRADE_SIZE @@ -1238,119 +1238,3 @@ fn query_compute_d(deps: Deps, env: Env) -> StdResult { .map_err(|_| StdError::generic_err("Failed to calculate the D"))? .to_uint128_with_precision(config.greatest_precision) } - -#[cfg(test)] -mod testing { - use std::error::Error; - use std::str::FromStr; - - use cosmwasm_std::testing::{mock_dependencies, mock_env}; - use cosmwasm_std::Timestamp; - - use astroport::observation::{query_observation, Observation, OracleObservation}; - use astroport_circular_buffer::BufferManager; - - use super::*; - - pub fn f64_to_dec(val: f64) -> T - where - T: FromStr, - T::Err: Error, - { - T::from_str(&val.to_string()).unwrap() - } - - #[test] - fn observations_full_buffer() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(100_000); - BufferManager::init(&mut deps.storage, OBSERVATIONS, 20).unwrap(); - - let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - - let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); - assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - - let array = (1..=30) - .into_iter() - .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), - }) - .collect_vec(); - buffer.push_many(&array); - buffer.commit(&mut deps.storage).unwrap(); - - env.block.time = env.block.time.plus_seconds(30_000); - - assert_eq!( - OracleObservation { - timestamp: 120_000, - price: f64_to_dec(20.0 / 400.0), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() - ); - - assert_eq!( - OracleObservation { - timestamp: 124_411, - price: f64_to_dec(0.04098166666666694), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() - ); - - let err = query_observation(deps.as_ref(), env, OBSERVATIONS, 35_000).unwrap_err(); - assert_eq!( - err.to_string(), - "Generic error: Requested observation is too old. Last known observation is at 111000" - ); - } - - #[test] - fn observations_incomplete_buffer() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(100_000); - BufferManager::init(&mut deps.storage, OBSERVATIONS, 3000).unwrap(); - - let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - - let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); - assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - - let array = (1..=30) - .into_iter() - .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), - }) - .collect_vec(); - buffer.push_many(&array); - buffer.commit(&mut deps.storage).unwrap(); - - env.block.time = env.block.time.plus_seconds(30_000); - - assert_eq!( - OracleObservation { - timestamp: 120_000, - price: f64_to_dec(20.0 / 400.0), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() - ); - - assert_eq!( - OracleObservation { - timestamp: 124_411, - price: f64_to_dec(0.04098166666666694), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() - ); - } -} diff --git a/contracts/pair_stable/src/testing.rs b/contracts/pair_stable/src/testing.rs index a7c1cca03..e65f61a66 100644 --- a/contracts/pair_stable/src/testing.rs +++ b/contracts/pair_stable/src/testing.rs @@ -1,27 +1,37 @@ -use crate::contract::{ - assert_max_spread, execute, instantiate, query, query_pool, query_reverse_simulation, - query_share, query_simulation, reply, +use std::error::Error; +use std::str::FromStr; + +use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; +use cosmwasm_std::{ + attr, coin, from_binary, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, + DepsMut, Env, Reply, ReplyOn, Response, SubMsg, SubMsgResponse, SubMsgResult, Timestamp, + Uint128, WasmMsg, }; -use crate::error::ContractError; -use crate::mock_querier::mock_dependencies; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; +use itertools::Itertools; +use proptest::prelude::*; +use prost::Message; +use sim::StableSwapModel; -use crate::state::{CONFIG, OBSERVATIONS}; use astroport::asset::{native_asset, native_asset_info, Asset, AssetInfo}; - +use astroport::observation::query_observation; +use astroport::observation::Observation; +use astroport::observation::OracleObservation; use astroport::pair::{ ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, PoolResponse, QueryMsg, SimulationResponse, StablePoolParams, }; use astroport::token::InstantiateMsg as TokenInstantiateMsg; -use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; -use cosmwasm_std::{ - attr, coin, from_binary, to_binary, Addr, BankMsg, BlockInfo, Coin, CosmosMsg, Decimal, - DepsMut, Env, Reply, ReplyOn, Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, - Timestamp, Uint128, WasmMsg, +use astroport_circular_buffer::BufferManager; + +use crate::contract::{ + assert_max_spread, execute, instantiate, query, query_pool, query_reverse_simulation, + query_share, query_simulation, reply, }; -use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg, MinterResponse}; -use itertools::Itertools; -use prost::Message; +use crate::error::ContractError; +use crate::mock_querier::mock_dependencies; +use crate::state::{CONFIG, OBSERVATIONS}; +use crate::utils::{compute_swap, select_pools}; #[derive(Clone, PartialEq, Message)] struct MsgInstantiateContractResponse { @@ -398,13 +408,7 @@ fn provide_liquidity() { }], ); let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - match res { - ContractError::Std(StdError::GenericErr { msg, .. }) => assert_eq!( - msg, - "Native token balance mismatch between the argument and the transferred".to_string() - ), - _ => panic!("Must return generic error"), - } + assert_eq!(res.to_string(), "Generic error: Native token balance mismatch between the argument (50000000000000000000uusd) and the transferred (100000000000000000000uusd)"); // Initialize token balances with a ratio of 1:1 deps.querier.with_balance(&[( @@ -1151,179 +1155,154 @@ fn test_query_share() { assert_eq!(res[1].amount, Uint128::new(500)); } -#[cfg(test)] -mod testing { - use std::error::Error; - use std::str::FromStr; +pub fn f64_to_dec(val: f64) -> T +where + T: FromStr, + T::Err: Error, +{ + T::from_str(&val.to_string()).unwrap() +} - use astroport::observation::{query_observation, Observation, OracleObservation}; - use astroport_circular_buffer::BufferManager; - use cosmwasm_std::testing::{mock_dependencies, mock_env}; - use cosmwasm_std::Timestamp; +#[test] +fn observations_full_buffer() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + BufferManager::init(&mut deps.storage, OBSERVATIONS, 20).unwrap(); - use super::*; + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - pub fn f64_to_dec(val: f64) -> T - where - T: FromStr, - T::Err: Error, - { - T::from_str(&val.to_string()).unwrap() - } + let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - #[test] - fn observations_full_buffer() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(100_000); - BufferManager::init(&mut deps.storage, OBSERVATIONS, 20).unwrap(); - - let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - - let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); - assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - - let array = (1..=30) - .into_iter() - .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), - }) - .collect_vec(); - buffer.push_many(&array); - buffer.commit(&mut deps.storage).unwrap(); + let array = (1..=30) + .into_iter() + .map(|i| Observation { + ts: env.block.time.seconds() + i * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i, i * i), + }) + .collect_vec(); + buffer.push_many(&array); + buffer.commit(&mut deps.storage).unwrap(); - env.block.time = env.block.time.plus_seconds(30_000); + env.block.time = env.block.time.plus_seconds(30_000); - assert_eq!( - OracleObservation { - timestamp: 120_000, - price: f64_to_dec(20.0 / 400.0), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() - ); + assert_eq!( + OracleObservation { + timestamp: 120_000, + price: f64_to_dec(20.0 / 400.0), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() + ); - assert_eq!( - OracleObservation { - timestamp: 124_411, - price: f64_to_dec(0.04098166666666694), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() - ); + assert_eq!( + OracleObservation { + timestamp: 124_411, + price: f64_to_dec(0.04098166666666694), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() + ); - let err = query_observation(deps.as_ref(), env, OBSERVATIONS, 35_000).unwrap_err(); - assert_eq!( - err.to_string(), - "Generic error: Requested observation is too old. Last known observation is at 111000" - ); - } + let err = query_observation(deps.as_ref(), env, OBSERVATIONS, 35_000).unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Requested observation is too old. Last known observation is at 111000" + ); +} - #[test] - fn observations_incomplete_buffer() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(100_000); - BufferManager::init(&mut deps.storage, OBSERVATIONS, 3000).unwrap(); - - let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - - let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); - assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - - let array = (1..=30) - .into_iter() - .map(|i| Observation { - timestamp: env.block.time.seconds() + i * 1000, - base_sma: Default::default(), - base_amount: i.into(), - quote_sma: Default::default(), - quote_amount: (i * i).into(), - }) - .collect_vec(); - buffer.push_many(&array); - buffer.commit(&mut deps.storage).unwrap(); +#[test] +fn observations_incomplete_buffer() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + BufferManager::init(&mut deps.storage, OBSERVATIONS, 3000).unwrap(); - env.block.time = env.block.time.plus_seconds(30_000); + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - assert_eq!( - OracleObservation { - timestamp: 120_000, - price: f64_to_dec(20.0 / 400.0), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() - ); + let err = query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 11000).unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Buffer is empty"); - assert_eq!( - OracleObservation { - timestamp: 124_411, - price: f64_to_dec(0.04098166666666694), - }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() - ); - } + let array = (1..=30) + .into_iter() + .map(|i| Observation { + ts: env.block.time.seconds() + i * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i, i * i), + }) + .collect_vec(); + buffer.push_many(&array); + buffer.commit(&mut deps.storage).unwrap(); - #[test] - fn observations_checking_triple_capacity_step_by_step() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(100_000); - const CAPACITY: u32 = 20; - BufferManager::init(&mut deps.storage, OBSERVATIONS, CAPACITY).unwrap(); - - let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); - - let ts = env.block.time.seconds(); - - let array = (1..=CAPACITY * 3) - .into_iter() - .map(|i| Observation { - timestamp: ts + i as u64 * 1000, - base_sma: Default::default(), - base_amount: (i * i).into(), - quote_sma: Default::default(), - quote_amount: i.into(), - }) - .collect_vec(); - - for (k, obs) in array.iter().enumerate() { - env.block.time = env.block.time.plus_seconds(1000); - - buffer.push(&obs); - buffer.commit(&mut deps.storage).unwrap(); - let k1 = k as u32 + 1; - - let from = k1.saturating_sub(CAPACITY) + 1; - let to = k1; - - for i in from..=to { - let shift = (to - i) as u64; - if shift != 0 { - assert_eq!( - OracleObservation { - timestamp: ts + i as u64 * 1000 + 500, - price: f64_to_dec(i as f64 + 0.5), - }, - query_observation( - deps.as_ref(), - env.clone(), - OBSERVATIONS, - shift * 1000 - 500 - ) - .unwrap() - ); - } + env.block.time = env.block.time.plus_seconds(30_000); + + assert_eq!( + OracleObservation { + timestamp: 120_000, + price: f64_to_dec(20.0 / 400.0), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 10000).unwrap() + ); + + assert_eq!( + OracleObservation { + timestamp: 124_411, + price: f64_to_dec(0.04098166666666694), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, 5589).unwrap() + ); +} + +#[test] +fn observations_checking_triple_capacity_step_by_step() { + let mut deps = mock_dependencies(&[]); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100_000); + const CAPACITY: u32 = 20; + BufferManager::init(&mut deps.storage, OBSERVATIONS, CAPACITY).unwrap(); + + let mut buffer = BufferManager::new(&deps.storage, OBSERVATIONS).unwrap(); + + let ts = env.block.time.seconds(); + + let array = (1..=CAPACITY * 3) + .into_iter() + .map(|i| Observation { + ts: ts + i as u64 * 1000, + price: Default::default(), + price_sma: Decimal::from_ratio(i * i, i), + }) + .collect_vec(); + + for (k, obs) in array.iter().enumerate() { + env.block.time = env.block.time.plus_seconds(1000); + + buffer.push(&obs); + buffer.commit(&mut deps.storage).unwrap(); + let k1 = k as u32 + 1; + + let from = k1.saturating_sub(CAPACITY) + 1; + let to = k1; + + for i in from..=to { + let shift = (to - i) as u64; + if shift != 0 { assert_eq!( OracleObservation { - timestamp: ts + i as u64 * 1000, - price: f64_to_dec(i as f64), + timestamp: ts + i as u64 * 1000 + 500, + price: f64_to_dec(i as f64 + 0.5), }, - query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, shift * 1000) + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, shift * 1000 - 500) .unwrap() ); } + assert_eq!( + OracleObservation { + timestamp: ts + i as u64 * 1000, + price: f64_to_dec(i as f64), + }, + query_observation(deps.as_ref(), env.clone(), OBSERVATIONS, shift * 1000).unwrap() + ); } } } @@ -1338,10 +1317,6 @@ fn mock_env_with_block_time(time: u64) -> Env { env } -use crate::utils::{compute_swap, select_pools}; -use proptest::prelude::*; -use sim::StableSwapModel; - proptest! { #[test] fn constant_product_swap_no_fee( @@ -1392,7 +1367,7 @@ proptest! { let diff = (sim_result as i128 - result.return_amount.u128() as i128).abs(); assert!( - diff <= 9, + diff <= 10, "result={}, sim_result={}, amp={}, amount_in={}, balance_in={}, balance_out={}, diff={}", result.return_amount, sim_result, diff --git a/contracts/pair_stable/src/utils.rs b/contracts/pair_stable/src/utils.rs index eb6a0f2e7..e2aedc22b 100644 --- a/contracts/pair_stable/src/utils.rs +++ b/contracts/pair_stable/src/utils.rs @@ -8,7 +8,9 @@ use cw20::Cw20ExecuteMsg; use itertools::Itertools; use astroport::asset::{Asset, AssetInfo, Decimal256Ext, DecimalAsset}; -use astroport::observation::{Observation, PrecommitObservation}; +use astroport::observation::{ + safe_sma_buffer_not_full, safe_sma_calculation, Observation, PrecommitObservation, +}; use astroport::querier::query_factory_config; use astroport_circular_buffer::error::BufferResult; use astroport_circular_buffer::BufferManager; @@ -291,7 +293,7 @@ pub(crate) fn compute_swap( }) } -/// Calculate and save moving averages of swap sizes. +/// Calculate and save price moving average pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResult<()> { if let Some(PrecommitObservation { base_amount, @@ -300,32 +302,50 @@ pub fn accumulate_swap_sizes(storage: &mut dyn Storage, env: &Env) -> BufferResu }) = PrecommitObservation::may_load(storage)? { let mut buffer = BufferManager::new(storage, OBSERVATIONS)?; + let observed_price = Decimal::from_ratio(base_amount, quote_amount); + let new_observation; if let Some(last_obs) = buffer.read_last(storage)? { // Skip saving observation if it has been already saved - if last_obs.timestamp < precommit_ts { - buffer.instant_push( - storage, - &Observation { - base_amount, - quote_amount, - timestamp: precommit_ts, - ..Default::default() - }, - )? + if last_obs.ts < precommit_ts { + // Since this is circular buffer the next index contains the oldest value + let count = buffer.capacity(); + if let Some(oldest_obs) = buffer.read_single(storage, buffer.head() + 1)? { + let price_sma = safe_sma_calculation( + last_obs.price_sma, + oldest_obs.price, + count, + observed_price, + )?; + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma, + }; + } else { + // Buffer is not full yet + let count = buffer.head(); + let price_sma = + safe_sma_buffer_not_full(last_obs.price_sma, count, observed_price)?; + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma, + }; + } + + buffer.instant_push(storage, &new_observation)? } } else { // Buffer is empty if env.block.time.seconds() > precommit_ts { - buffer.instant_push( - storage, - &Observation { - timestamp: precommit_ts, - base_amount, - quote_amount, - ..Default::default() - }, - )? + new_observation = Observation { + ts: precommit_ts, + price: observed_price, + price_sma: observed_price, + }; + + buffer.instant_push(storage, &new_observation)? } } } diff --git a/contracts/pair_stable/tests/integration.rs b/contracts/pair_stable/tests/integration.rs index 572bb5dea..43075e0e9 100644 --- a/contracts/pair_stable/tests/integration.rs +++ b/contracts/pair_stable/tests/integration.rs @@ -1681,6 +1681,94 @@ fn test_imbalance_withdraw_is_disabled() { ); } +#[test] +fn test_provide_liquidity_without_funds() { + let owner = Addr::unchecked("owner"); + let alice_address = Addr::unchecked("alice"); + let mut router = mock_app( + owner.clone(), + vec![ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::new(100_000_000_000u128), + }, + ], + ); + + // Set Alice's balances + router + .send_tokens( + owner.clone(), + alice_address.clone(), + &[ + Coin { + denom: "uusd".to_string(), + amount: Uint128::new(233_000_000u128), + }, + Coin { + denom: "uluna".to_string(), + amount: Uint128::new(2_00_000_000u128), + }, + Coin { + denom: "cny".to_string(), + amount: Uint128::from(100_000_000u128), + }, + ], + ) + .unwrap(); + + // Init pair + let pair_instance = instantiate_pair(&mut router, &owner); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair_instance.to_string(), &QueryMsg::Pair {}) + .unwrap(); + + assert_eq!( + res.asset_infos, + [ + AssetInfo::NativeToken { + denom: "uusd".to_string(), + }, + AssetInfo::NativeToken { + denom: "uluna".to_string(), + }, + ], + ); + + // provide some liquidity to assume contract have funds (to prevent underflow err) + let (msg, coins) = + provide_liquidity_msg(Uint128::new(100_000_000), Uint128::new(100_000_000), None); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &coins) + .unwrap(); + + // provide liquidity without funds + + let err = router + .execute_contract(alice_address.clone(), pair_instance.clone(), &msg, &[]) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Native token balance mismatch between the argument (100000000uusd) and the transferred (0uusd)" + ); +} + #[test] fn check_correct_fee_share() { // Validate the resulting values diff --git a/contracts/pair_stable/tests/stablepool_tests.rs b/contracts/pair_stable/tests/stablepool_tests.rs index 80fc662ad..de018aecd 100644 --- a/contracts/pair_stable/tests/stablepool_tests.rs +++ b/contracts/pair_stable/tests/stablepool_tests.rs @@ -480,7 +480,7 @@ fn check_pool_prices() { helper.query_observe(0).unwrap(), OracleObservation { timestamp: helper.app.block_info().time.seconds(), - price: Decimal::from_str("0.9994992083").unwrap() + price: Decimal::from_str("0.999999778261572849").unwrap() } ); diff --git a/contracts/router/Cargo.toml b/contracts/router/Cargo.toml index 074f1276f..06b7027bd 100644 --- a/contracts/router/Cargo.toml +++ b/contracts/router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-router" -version = "1.1.2" +version = "1.2.0" authors = ["Astroport"] edition = "2021" description = "The Astroport router contract - provides multi-hop swap functionality for Astroport pools" @@ -28,7 +28,7 @@ cw20 = "0.15" cosmwasm-std = "1.1" cw-storage-plus = "0.15" integer-sqrt = "0.1" -astroport = { path = "../../packages/astroport", version = "3" } +astroport = { path = "../../packages/astroport", version = "3.8" } thiserror = { version = "1.0" } cosmwasm-schema = "1.1" diff --git a/contracts/router/README.md b/contracts/router/README.md index 26b5c6acf..db2ebd40d 100644 --- a/contracts/router/README.md +++ b/contracts/router/README.md @@ -70,6 +70,8 @@ Swap UST => mABNB ### `execute_swap_operations` Performs multi-hop swap operations for native & Astroport tokens. Swaps execute one-by-one and the last swap will return the ask token. This function is public (can be called by anyone). +Contract sets total 'return_amount' in response data after all routes are processed. See `SwapResponseData` type for more info. +Note: Response data makes sense ONLY if the first token in multi-hop swap is native. Otherwise, cw20::send message resets response data. ### Example diff --git a/contracts/router/src/contract.rs b/contracts/router/src/contract.rs index ca15cac1e..0ade7480c 100644 --- a/contracts/router/src/contract.rs +++ b/contracts/router/src/contract.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - entry_point, from_binary, to_binary, Addr, Api, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, - MessageInfo, Response, StdResult, Uint128, WasmMsg, + entry_point, from_binary, to_binary, wasm_execute, Addr, Api, Binary, Decimal, Deps, DepsMut, + Env, MessageInfo, Reply, Response, StdError, StdResult, SubMsg, SubMsgResult, Uint128, }; use cw2::{get_contract_version, set_contract_version}; use cw20::Cw20ReceiveMsg; @@ -10,18 +10,20 @@ use astroport::pair::{QueryMsg as PairQueryMsg, SimulationResponse}; use astroport::querier::query_pair_info; use astroport::router::{ ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, - SimulateSwapOperationsResponse, SwapOperation, MAX_SWAP_OPERATIONS, + SimulateSwapOperationsResponse, SwapOperation, SwapResponseData, MAX_SWAP_OPERATIONS, }; use crate::error::ContractError; use crate::operations::execute_swap_operation; -use crate::state::{Config, CONFIG}; +use crate::state::{Config, ReplyData, CONFIG, REPLY_DATA}; /// Contract name that is used for migration. const CONTRACT_NAME: &str = "astroport-router"; /// Contract version that is used for migration. const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const AFTER_SWAP_REPLY_ID: u64 = 1; + /// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -91,18 +93,6 @@ pub fn execute( max_spread, single, } => execute_swap_operation(deps, env, info, operation, to, max_spread, single), - ExecuteMsg::AssertMinimumReceive { - asset_info, - prev_balance, - minimum_receive, - receiver, - } => assert_minimum_receive( - deps.as_ref(), - asset_info, - prev_balance, - minimum_receive, - deps.api.addr_validate(&receiver)?, - ), } } @@ -157,72 +147,82 @@ pub fn execute_swap_operations( let target_asset_info = operations.last().unwrap().get_target_asset_info(); let operations_len = operations.len(); - let mut messages = operations + let messages = operations .into_iter() .enumerate() .map(|(operation_index, op)| { - Ok(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - funds: vec![], - msg: to_binary(&ExecuteMsg::ExecuteSwapOperation { - operation: op, - to: if operation_index == operations_len - 1 { - Some(to.to_string()) - } else { - None + if operation_index == operations_len - 1 { + wasm_execute( + env.contract.address.to_string(), + &ExecuteMsg::ExecuteSwapOperation { + operation: op, + to: Some(to.to_string()), + max_spread, + single: operations_len == 1, + }, + vec![], + ) + .map(|inner_msg| SubMsg::reply_on_success(inner_msg, AFTER_SWAP_REPLY_ID)) + } else { + wasm_execute( + env.contract.address.to_string(), + &ExecuteMsg::ExecuteSwapOperation { + operation: op, + to: None, + max_spread, + single: operations_len == 1, }, - max_spread, - single: operations_len == 1, - })?, - })) + vec![], + ) + .map(SubMsg::new) + } }) - .collect::>>()?; - - // Execute minimum amount assertion - if let Some(minimum_receive) = minimum_receive { - let receiver_balance = target_asset_info.query_pool(&deps.querier, &to)?; - messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - funds: vec![], - msg: to_binary(&ExecuteMsg::AssertMinimumReceive { - asset_info: target_asset_info, - prev_balance: receiver_balance, - minimum_receive, - receiver: to.to_string(), - })?, - })); - } + .collect::>>()?; + + let prev_balance = target_asset_info.query_pool(&deps.querier, &to)?; + REPLY_DATA.save( + deps.storage, + &ReplyData { + asset_info: target_asset_info, + prev_balance, + minimum_receive, + receiver: to.to_string(), + }, + )?; - Ok(Response::new().add_messages(messages)) + Ok(Response::new().add_submessages(messages)) } -/// Checks if an ask amount is equal to or above a minimum amount. -/// -/// * **asset_info** asset to check the ask amount for. -/// -/// * **prev_balance** previous balance that the swap receive had before getting `ask` assets. -/// -/// * **minimum_receive** minimum amount of `ask` assets to receive. -/// -/// * **receiver** address that received `ask` assets. -fn assert_minimum_receive( - deps: Deps, - asset_info: AssetInfo, - prev_balance: Uint128, - minimum_receive: Uint128, - receiver: Addr, -) -> Result { - asset_info.check(deps.api)?; - let receiver_balance = asset_info.query_pool(&deps.querier, receiver)?; - let swap_amount = receiver_balance.checked_sub(prev_balance)?; - - if swap_amount < minimum_receive { - Err(ContractError::AssertionMinimumReceive { - receive: minimum_receive, - amount: swap_amount, - }) - } else { - Ok(Response::default()) +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg { + Reply { + id: AFTER_SWAP_REPLY_ID, + result: SubMsgResult::Ok(..), + } => { + let reply_data = REPLY_DATA.load(deps.storage)?; + let receiver_balance = reply_data + .asset_info + .query_pool(&deps.querier, reply_data.receiver)?; + let swap_amount = receiver_balance.checked_sub(reply_data.prev_balance)?; + + if let Some(minimum_receive) = reply_data.minimum_receive { + if swap_amount < minimum_receive { + return Err(ContractError::AssertionMinimumReceive { + receive: minimum_receive, + amount: swap_amount, + }); + } + } + + // Reply data makes sense ONLY if the first token in multi-hop swap is native. + let data = to_binary(&SwapResponseData { + return_amount: swap_amount, + })?; + + Ok(Response::new().set_data(data)) + } + _ => Err(StdError::generic_err("Failed to process reply").into()), } } @@ -259,13 +259,14 @@ pub fn query_config(deps: Deps) -> Result { } /// Manages contract migration. +#[cfg(not(tarpaulin_include))] #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { let contract_version = get_contract_version(deps.storage)?; match contract_version.contract.as_ref() { "astroport-router" => match contract_version.version.as_ref() { - "1.0.0" | "1.1.0" | "1.1.1" => {} + "1.1.1" => {} _ => return Err(ContractError::MigrationError {}), }, _ => return Err(ContractError::MigrationError {}), diff --git a/contracts/router/src/error.rs b/contracts/router/src/error.rs index c31f4be52..ff611fa9d 100644 --- a/contracts/router/src/error.rs +++ b/contracts/router/src/error.rs @@ -7,6 +7,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + #[error("Unauthorized")] Unauthorized {}, @@ -41,9 +44,3 @@ pub enum ContractError { #[error("Contract can't be migrated!")] MigrationError {}, } - -impl From for ContractError { - fn from(o: OverflowError) -> Self { - StdError::from(o).into() - } -} diff --git a/contracts/router/src/state.rs b/contracts/router/src/state.rs index 87590e7e1..ea34fefde 100644 --- a/contracts/router/src/state.rs +++ b/contracts/router/src/state.rs @@ -1,5 +1,6 @@ +use astroport::asset::AssetInfo; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, Uint128}; use cw_storage_plus::Item; /// Stores the contract config at the given key @@ -11,3 +12,13 @@ pub struct Config { /// The factory contract address pub astroport_factory: Addr, } + +pub const REPLY_DATA: Item = Item::new("reply_data"); + +#[cw_serde] +pub struct ReplyData { + pub asset_info: AssetInfo, + pub prev_balance: Uint128, + pub minimum_receive: Option, + pub receiver: String, +} diff --git a/contracts/router/src/testing/tests.rs b/contracts/router/src/testing/tests.rs index 84597e83e..a9aedc89a 100644 --- a/contracts/router/src/testing/tests.rs +++ b/contracts/router/src/testing/tests.rs @@ -1,10 +1,5 @@ use cosmwasm_std::testing::{mock_env, mock_info, MOCK_CONTRACT_ADDR}; use cosmwasm_std::{from_binary, to_binary, Addr, Coin, ReplyOn, SubMsg, Uint128, WasmMsg}; - -use crate::contract::{execute, instantiate, query}; -use crate::error::ContractError; -use crate::testing::mock_querier::mock_dependencies; - use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use astroport::asset::{native_asset_info, AssetInfo}; @@ -13,6 +8,10 @@ use astroport::router::{ SimulateSwapOperationsResponse, SwapOperation, MAX_SWAP_OPERATIONS, }; +use crate::contract::{execute, instantiate, query, AFTER_SWAP_REPLY_ID}; +use crate::error::ContractError; +use crate::testing::mock_querier::mock_dependencies; + #[test] fn proper_initialization() { let mut deps = mock_dependencies(&[]); @@ -164,29 +163,10 @@ fn execute_swap_operations() { .unwrap(), } .into(), - id: 0, - gas_limit: None, - reply_on: ReplyOn::Never, - }, - SubMsg { - msg: WasmMsg::Execute { - contract_addr: String::from(MOCK_CONTRACT_ADDR), - funds: vec![], - msg: to_binary(&ExecuteMsg::AssertMinimumReceive { - asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("asset0002"), - }, - prev_balance: Uint128::zero(), - minimum_receive: Uint128::from(1000000u128), - receiver: String::from("addr0000"), - }) - .unwrap(), - } - .into(), - id: 0, + id: AFTER_SWAP_REPLY_ID, gas_limit: None, - reply_on: ReplyOn::Never, - }, + reply_on: ReplyOn::Success, + } ] ); @@ -301,9 +281,9 @@ fn execute_swap_operations() { .unwrap(), } .into(), - id: 0, + id: AFTER_SWAP_REPLY_ID, gas_limit: None, - reply_on: ReplyOn::Never, + reply_on: ReplyOn::Success, } ] ); @@ -439,90 +419,16 @@ fn query_buy_with_routes() { amount: Uint128::from(1000000u128), } ); -} -#[test] -fn assert_minimum_receive_native_token() { - let mut deps = mock_dependencies(&[]); - deps.querier.with_balance(&[( - &String::from("addr0000"), - &[Coin { - denom: "uusd".to_string(), - amount: Uint128::from(1000000u128), + let msg = QueryMsg::SimulateSwapOperations { + offer_amount: Uint128::from(1000000u128), + operations: vec![SwapOperation::NativeSwap { + offer_denom: "ukrw".to_string(), + ask_denom: "test".to_string(), }], - )]); - - let env = mock_env(); - let info = mock_info("addr0000", &[]); - // Success - let msg = ExecuteMsg::AssertMinimumReceive { - asset_info: AssetInfo::NativeToken { - denom: "uusd".to_string(), - }, - prev_balance: Uint128::zero(), - minimum_receive: Uint128::from(1000000u128), - receiver: String::from("addr0000"), - }; - let _res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // Assertion failed; native token - let msg = ExecuteMsg::AssertMinimumReceive { - asset_info: AssetInfo::NativeToken { - denom: "uusd".to_string(), - }, - prev_balance: Uint128::zero(), - minimum_receive: Uint128::from(1000001u128), - receiver: String::from("addr0000"), - }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - res, - ContractError::AssertionMinimumReceive { - receive: Uint128::new(1000001), - amount: Uint128::new(1000000), - } - ); -} - -#[test] -fn assert_minimum_receive_token() { - let mut deps = mock_dependencies(&[]); - - deps.querier.with_token_balances(&[( - &String::from("token0000"), - &[(&String::from("addr0000"), &Uint128::from(1000000u128))], - )]); - - let env = mock_env(); - let info = mock_info("addr0000", &[]); - // Success - let msg = ExecuteMsg::AssertMinimumReceive { - asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("token0000"), - }, - prev_balance: Uint128::zero(), - minimum_receive: Uint128::from(1000000u128), - receiver: String::from("addr0000"), - }; - let _res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); - - // Assertion failed; native token - let msg = ExecuteMsg::AssertMinimumReceive { - asset_info: AssetInfo::Token { - contract_addr: Addr::unchecked("token0000"), - }, - prev_balance: Uint128::zero(), - minimum_receive: Uint128::from(1000001u128), - receiver: String::from("addr0000"), }; - let res = execute(deps.as_mut(), env.clone(), info, msg).unwrap_err(); - assert_eq!( - res, - ContractError::AssertionMinimumReceive { - receive: Uint128::new(1000001), - amount: Uint128::new(1000000), - } - ); + let err = query(deps.as_ref(), env.clone(), msg).unwrap_err(); + assert_eq!(err, ContractError::NativeSwapNotSupported {}); } #[test] diff --git a/contracts/router/tests/factory_helper.rs b/contracts/router/tests/factory_helper.rs index 216336d96..7bae53f85 100644 --- a/contracts/router/tests/factory_helper.rs +++ b/contracts/router/tests/factory_helper.rs @@ -1,12 +1,13 @@ #![cfg(not(tarpaulin_include))] use anyhow::Result as AnyResult; -use astroport::asset::{AssetInfo, PairInfo}; -use astroport::factory::{PairConfig, PairType, QueryMsg}; -use cosmwasm_std::{Addr, Binary}; +use cosmwasm_std::{coins, Addr, Binary}; use cw20::MinterResponse; use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::{PairConfig, PairType, QueryMsg}; + pub struct FactoryHelper { pub owner: Addr, pub astro_token: Addr, @@ -120,50 +121,21 @@ impl FactoryHelper { router: &mut App, sender: &Addr, pair_type: PairType, - tokens: [&Addr; 2], + asset_infos: [AssetInfo; 2], init_params: Option, - ) -> AnyResult { - let asset_infos = vec![ - AssetInfo::Token { - contract_addr: tokens[0].clone(), - }, - AssetInfo::Token { - contract_addr: tokens[1].clone(), - }, - ]; - + ) -> AnyResult { let msg = astroport::factory::ExecuteMsg::CreatePair { pair_type, - asset_infos, + asset_infos: asset_infos.to_vec(), init_params, }; - router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[]) - } - - pub fn create_pair_with_addr( - &mut self, - router: &mut App, - sender: &Addr, - pair_type: PairType, - tokens: [&Addr; 2], - init_params: Option, - ) -> AnyResult { - self.create_pair(router, sender, pair_type, tokens, init_params)?; - - let asset_infos = vec![ - AssetInfo::Token { - contract_addr: tokens[0].clone(), - }, - AssetInfo::Token { - contract_addr: tokens[1].clone(), - }, - ]; + router.execute_contract(sender.clone(), self.factory.clone(), &msg, &[])?; let res: PairInfo = router.wrap().query_wasm_smart( self.factory.clone(), &QueryMsg::Pair { - asset_infos: asset_infos.clone(), + asset_infos: asset_infos.to_vec(), }, )?; @@ -218,3 +190,22 @@ pub fn mint( &[], ) } + +pub fn mint_native( + app: &mut App, + denom: &str, + amount: u128, + receiver: &Addr, +) -> AnyResult { + // .init_balance() erases previous balance thus we use such hack and create intermediate "denom admin" + let denom_admin = Addr::unchecked(format!("{denom}_admin")); + let coins_vec = coins(amount, denom); + app.init_modules(|router, _, storage| { + router + .bank + .init_balance(storage, &denom_admin, coins_vec.clone()) + }) + .unwrap(); + + app.send_tokens(denom_admin, receiver.clone(), &coins_vec) +} diff --git a/contracts/router/tests/router_integration.rs b/contracts/router/tests/router_integration.rs index 12071b6b2..7ef895ee0 100644 --- a/contracts/router/tests/router_integration.rs +++ b/contracts/router/tests/router_integration.rs @@ -1,21 +1,27 @@ #![cfg(not(tarpaulin_include))] -mod factory_helper; - -use crate::factory_helper::{instantiate_token, mint, FactoryHelper}; -use astroport::asset::token_asset_info; -use astroport::factory::PairType; -use astroport::router::{ExecuteMsg, InstantiateMsg, SwapOperation}; -use cosmwasm_std::{to_binary, Addr, Empty, StdError}; +use cosmwasm_std::{coins, from_binary, to_binary, Addr, Empty, StdError}; use cw20::Cw20ExecuteMsg; use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use astroport::asset::{native_asset_info, token_asset_info}; +use astroport::factory::PairType; +use astroport::router::{ExecuteMsg, InstantiateMsg, SwapOperation, SwapResponseData}; +use astroport_router::error::ContractError; + +use crate::factory_helper::{instantiate_token, mint, mint_native, FactoryHelper}; + +mod factory_helper; + fn router_contract() -> Box> { - Box::new(ContractWrapper::new_with_empty( - astroport_router::contract::execute, - astroport_router::contract::instantiate, - astroport_router::contract::query, - )) + Box::new( + ContractWrapper::new_with_empty( + astroport_router::contract::execute, + astroport_router::contract::instantiate, + astroport_router::contract::query, + ) + .with_reply_empty(astroport_router::contract::reply), + ) } #[test] @@ -34,7 +40,13 @@ fn router_does_not_enforce_spread_assertion() { (&token_y, &token_z, PairType::Stable {}, 1_000_000_000000), ] { let pair = helper - .create_pair_with_addr(&mut app, &owner, typ, [a, b], None) + .create_pair( + &mut app, + &owner, + typ, + [token_asset_info(a.clone()), token_asset_info(b.clone())], + None, + ) .unwrap(); mint(&mut app, &owner, a, liq, &pair).unwrap(); mint(&mut app, &owner, b, liq, &pair).unwrap(); @@ -56,32 +68,39 @@ fn router_does_not_enforce_spread_assertion() { // Triggering swap with a huge spread fees mint(&mut app, &owner, &token_x, 50_000_000000, &owner).unwrap(); - app.execute_contract( - owner.clone(), - token_x.clone(), - &Cw20ExecuteMsg::Send { - contract: router.to_string(), - amount: 50_000_000000u128.into(), - msg: to_binary(&ExecuteMsg::ExecuteSwapOperations { - operations: vec![ - SwapOperation::AstroSwap { - offer_asset_info: token_asset_info(token_x.clone()), - ask_asset_info: token_asset_info(token_y.clone()), - }, - SwapOperation::AstroSwap { - offer_asset_info: token_asset_info(token_y.clone()), - ask_asset_info: token_asset_info(token_z.clone()), - }, - ], - minimum_receive: None, - to: None, - max_spread: None, - }) - .unwrap(), - }, - &[], - ) - .unwrap(); + let resp = app + .execute_contract( + owner.clone(), + token_x.clone(), + &Cw20ExecuteMsg::Send { + contract: router.to_string(), + amount: 50_000_000000u128.into(), + msg: to_binary(&ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: token_asset_info(token_x.clone()), + ask_asset_info: token_asset_info(token_y.clone()), + }, + SwapOperation::AstroSwap { + offer_asset_info: token_asset_info(token_y.clone()), + ask_asset_info: token_asset_info(token_z.clone()), + }, + ], + minimum_receive: None, + to: None, + max_spread: None, + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + // We can't set data in response if the first message dispatched from cw20 contract + assert!( + resp.data.is_none(), + "Unexpected data set after cw20 send hook" + ); // However, single hop will still enforce spread assertion mint(&mut app, &owner, &token_x, 50_000_000000, &owner).unwrap(); @@ -112,6 +131,181 @@ fn router_does_not_enforce_spread_assertion() { ) } +#[test] +fn route_through_pairs_with_natives() { + let mut app = App::default(); + + let owner = Addr::unchecked("owner"); + let mut helper = FactoryHelper::init(&mut app, &owner); + + let denom_x = "denom_x"; + let denom_y = "denom_y"; + let denom_z = "denom_z"; + + for (a, b, typ, liq) in [ + (&denom_x, &denom_y, PairType::Xyk {}, 100_000_000000), + (&denom_y, &denom_z, PairType::Stable {}, 1_000_000_000000), + ] { + let pair = helper + .create_pair( + &mut app, + &owner, + typ, + [ + native_asset_info(a.to_string()), + native_asset_info(b.to_string()), + ], + None, + ) + .unwrap(); + mint_native(&mut app, a, liq, &pair).unwrap(); + mint_native(&mut app, b, liq, &pair).unwrap(); + } + + let router_code = app.store_code(router_contract()); + let router = app + .instantiate_contract( + router_code, + owner.clone(), + &InstantiateMsg { + astroport_factory: helper.factory.to_string(), + }, + &[], + "router", + None, + ) + .unwrap(); + + // Sanity checks + + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperation { + operation: SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + to: None, + max_spread: None, + single: false, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::NativeSwap { + offer_denom: denom_x.to_string(), + ask_denom: denom_y.to_string(), + }], + to: None, + max_spread: None, + minimum_receive: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NativeSwapNotSupported {} + ); + + let err = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_x.to_string()), + }], + to: None, + max_spread: None, + minimum_receive: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::DoublingAssetsPath { + offer_asset: denom_x.to_string(), + ask_asset: denom_x.to_string() + } + ); + + // End sanity checks + + mint_native(&mut app, &denom_x, 50_000_000000, &owner).unwrap(); + let resp = app + .execute_contract( + owner.clone(), + router.clone(), + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_y.to_string()), + ask_asset_info: native_asset_info(denom_z.to_string()), + }, + ], + minimum_receive: None, + to: None, + max_spread: None, + }, + &coins(50_000_000000, denom_x), + ) + .unwrap(); + + let resp_data: SwapResponseData = from_binary(&resp.data.unwrap()).unwrap(); + + assert_eq!(resp_data.return_amount.u128(), 32_258_064515); + + mint_native(&mut app, &denom_x, 50_000_000000, &owner).unwrap(); + let err = app + .execute_contract( + owner.clone(), + router, + &ExecuteMsg::ExecuteSwapOperations { + operations: vec![ + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_x.to_string()), + ask_asset_info: native_asset_info(denom_y.to_string()), + }, + SwapOperation::AstroSwap { + offer_asset_info: native_asset_info(denom_y.to_string()), + ask_asset_info: native_asset_info(denom_z.to_string()), + }, + ], + minimum_receive: Some(50_000_000000u128.into()), // <--- enforcing minimum receive with 1:1 rate (which practically impossible) + to: None, + max_spread: None, + }, + &coins(50_000_000000, denom_x), + ) + .unwrap_err(); + + assert_eq!( + err.downcast::().unwrap(), + ContractError::AssertionMinimumReceive { + receive: 50_000_000000u128.into(), + amount: 15_360_983102u128.into() + } + ); +} + #[test] fn test_swap_route() { use crate::factory_helper::{instantiate_token, mint, FactoryHelper}; @@ -136,7 +330,13 @@ fn test_swap_route() { (&atom, &osmo, PairType::Xyk {}, 100_000_000000), ] { let pair = helper - .create_pair_with_addr(&mut app, &owner, typ, [a, b], None) + .create_pair( + &mut app, + &owner, + typ, + [token_asset_info(a.clone()), token_asset_info(b.clone())], + None, + ) .unwrap(); mint(&mut app, &owner, a, liq, &pair).unwrap(); mint(&mut app, &owner, b, liq, &pair).unwrap(); diff --git a/contracts/tokenomics/incentives/Cargo.toml b/contracts/tokenomics/incentives/Cargo.toml new file mode 100644 index 000000000..55f557131 --- /dev/null +++ b/contracts/tokenomics/incentives/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "astroport-incentives" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +library = [] + +[dependencies] +cosmwasm-std = "1.3" +cw-storage-plus = "0.15" +cosmwasm-schema = "1.4" +cw2 = "1" +cw20 = "1" +cw-utils = "1" +astroport = { path = "../../../packages/astroport" } +thiserror = "1" +itertools = "0.11" + +[dev-dependencies] +cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "astroport_cozy_fork" } +anyhow = "1" +astroport-factory = { path = "../../factory" } +astroport-pair = { path = "../../pair" } +astroport-pair-stable = { path = "../../pair_stable" } +astroport-native-coin-registry = { path = "../../periphery/native_coin_registry" } +astroport-vesting = { path = "../vesting" } +cw20-base = "1" +proptest = "1.3" diff --git a/contracts/tokenomics/incentives/README.md b/contracts/tokenomics/incentives/README.md new file mode 100644 index 000000000..56a6506a0 --- /dev/null +++ b/contracts/tokenomics/incentives/README.md @@ -0,0 +1,57 @@ +# Astroport Incentives (formerly Generator) + +The Astroport Incentives contract allocates token rewards for various LP tokens and distributes them pro-rata to LP stakers. +This is completely reworked version of the original [Generator](https://github.com/astroport-fi/astroport-core/tree/main/contracts/tokenomics/generator) contract. +In this version we support both cw20 and native LP tokens. New generator also got rid of proxy contracts and made it much easier to add incentives (permissonless!). +However, generator could require incentivization fee to be paid to add new reward schedule which is exclusively needed to prevent spamming. +One more improvement is that ASTRO emissions are counted by seconds instead of blocks which makes more sense since Astroport is multichain protocol. + +## Endpoints Description +Contract supports following execute endpoints: +- `setup_pools` - is meant to be called either by owner or generator controller. Reset previous active pools and set new alloc points. +- `deposit` - stake LP tokens in the generator in order to receive rewards. Rewards are updated and withdrawn automatically. All pools registered the Astroport factory are stakable. However, it doesn't mean that the pool is incentivized. +- `withdraw` - withdraw part or all LP tokens from the generator. Rewards are updated and withdrawn automatically. +- `claim_rewards` - update and withdraw all rewards associated with the LP tokens. This endpoint accepts multiple LP tokens. +- `set_tokens_per_second` - set new number of ASTRO emissions per second. Only owner can call this endpoint. +- `incentivize` - add new reward schedule to a specific pool. All overlapped schedules are thoroughly considered and summed up. This is permissonless endpoint. However, it requires to pay incentivization fee in case this reward is new. +- `remove_reward_from_pool` - completely remove reward from pool. However, all accrued rewards will be considered at current point. This endpoint can be called only by owner. One must supply remaining rewards receiver address. +- `update_config` - is meant to update general contract settings. Only owner can call this endpoint. +- `update_blocked_tokens_list` - update list of tokens that are not allowed to be incentivized with ASTRO as well as can't be used as external rewards. Only owner can call this endpoint. +- `deactivate_pool` - only factory can call this endpoint. Called from deregistration context in factory. +- `propose_new_owner`, `drop_ownership_proposal`, `claim_ownership` - endpoints to change ownership. Only current owner can propose new owner or drop proposal and only proposed owner can claim ownership. + +### Deposit +Anyone can deposit either through direct `deposit` call with native LP tokens supplied or via cw20 send hook. +Contract checks if LP token corresponds to a pair registered in factory. Any LP token is stakable by default although it doesn't mean that it is incentivized in generator. + +![deposit_figure](./assets/deposit.png "Deposit figure") + +### Withdraw +Partially or fully withdraw LP tokens from the generator. Rewards are updated and withdrawn automatically. + +![withdraw_figure](./assets/withdraw.png "Withdraw figure") + +### Incentivize +Add new reward schedule to a specific pool. All overlapped schedules are thoroughly considered and summed up. +This is permissonless endpoint. However, it requires to pay incentivization fee in case this reward is new. +Reward schedules are counted by periods where period is one week. Each period starts on Monday 00:00 UTC and ends on Sunday 23:59 UTC. +New reward schedule always starts right away and lasts **till the next Monday + X weeks**, where X - number of weeks specified in the schedule. + +See in figure below possible scenarios. The first line represents current reward schedule, +2nd red line shows new reward schedule and 3rd line shows the result. + +![incentivize_figure](./assets/incentivize.png "Incentivize figure") + +### Update pool rewards +This is internal logic which is launched whenever LP tokens amount changes, new reward schedule is added or rewards are claimed. +Each time _update_rewards_ is called, accrued rewards / total LP staked value is added to the current reward index. +_rps_ - rewards per second + +![update_rewards_figure](./assets/schedules_flow.png "Update rewards figure") + +## Limitations and requirements +1. Chain doesn't allow to mint native tokens in the form of bech32 addresses. +I.e. `wasm1xxxxxxx` denom is prohibited but `factory/wasm1xxxxxxx/astroport_lp` is allowed. +2. Chain has TokenFactory module. Produced denom strictly follows these [rules](https://github.com/osmosis-labs/osmosis/tree/main/x/tokenfactory#expectations-from-the-chain) +3. Generator assumes active pool set size is bounded to a reasonable value (i.e. max 30). Generator controller and owner must consider this. +Otherwise, some endpoints working with active pools might fail due to gas limit. \ No newline at end of file diff --git a/contracts/tokenomics/incentives/assets/deposit.excalidraw b/contracts/tokenomics/incentives/assets/deposit.excalidraw new file mode 100644 index 000000000..123a1bcc7 --- /dev/null +++ b/contracts/tokenomics/incentives/assets/deposit.excalidraw @@ -0,0 +1,729 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw-jetbrains-plugin", + "elements": [ + { + "type": "rectangle", + "version": 318, + "versionNonce": 695757907, + "isDeleted": false, + "id": "z-D9CyMgLtiipaoetummB", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 587.2578125, + "y": 174.15625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 336.1015625, + "height": 117.7421875, + "seed": 673187677, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "id": "MBS8kFP_JhnRzQkuZIV_P", + "type": "arrow" + }, + { + "id": "wGXN7DnaxZM15k8NFJHkh", + "type": "arrow" + } + ], + "updated": 1698835027364, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 359, + "versionNonce": 7119549, + "isDeleted": false, + "id": "6JGi5eX6Afv6Il-_5wqEL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 331.6484375, + "y": 370.78515625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 327.9453125, + "height": 126.421875, + "seed": 330368445, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "f0VhvSb1j6rxZi1Ovf7uv" + }, + { + "id": "wGXN7DnaxZM15k8NFJHkh", + "type": "arrow" + }, + { + "id": "d2nN109BO8U4Ua6sxrWpL", + "type": "arrow" + } + ], + "updated": 1698835027364, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 295, + "versionNonce": 2002961538, + "isDeleted": false, + "id": "f0VhvSb1j6rxZi1Ovf7uv", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 340.501220703125, + "y": 396.49609375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 310.23974609375, + "height": 75, + "seed": 1726124157, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854586965, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": " Claim all rewards: \n (pool_index - user_index) *\nuser LP amount", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "6JGi5eX6Afv6Il-_5wqEL", + "originalText": " Claim all rewards: \n (pool_index - user_index) * user LP amount", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "text", + "version": 24, + "versionNonce": 1419818590, + "isDeleted": false, + "id": "STDXd0rZWYszZLQaWX9Vf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 623, + "y": 232, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 253.17977905273438, + "height": 25, + "seed": 1054713373, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854578914, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Pool exists in Generator?", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Pool exists in Generator?", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 534, + "versionNonce": 1312109392, + "isDeleted": false, + "id": "7MBBa3YapwkPyc453AgRq", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 599.328125, + "y": 603.26171875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 315.4609375, + "height": 116.39453125, + "seed": 365313811, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "-wOQNn3lVX1whXdbIgXzJ" + }, + { + "id": "d2nN109BO8U4Ua6sxrWpL", + "type": "arrow" + }, + { + "id": "SAwozYSfdz9G24x4r2tY_", + "type": "arrow" + }, + { + "id": "zuBCsX8fyEnHYZnOeax65", + "type": "arrow" + } + ], + "updated": 1698851021852, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 465, + "versionNonce": 1393384414, + "isDeleted": false, + "id": "-wOQNn3lVX1whXdbIgXzJ", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 615.9486999511719, + "y": 623.958984375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 282.21978759765625, + "height": 75, + "seed": 1157864947, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854591370, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": " Sync user info with pool \nindexes:\n user_index = pool_index", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "7MBBa3YapwkPyc453AgRq", + "originalText": " Sync user info with pool indexes:\n user_index = pool_index", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "rectangle", + "version": 631, + "versionNonce": 895875294, + "isDeleted": false, + "id": "3hzC9q_VOdJ1uRbH3vR1L", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 868.328125, + "y": 371.68359375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 327.9453125, + "height": 126.421875, + "seed": 333221779, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "9AhbeVvImNZO0LndntLU9" + }, + { + "id": "MBS8kFP_JhnRzQkuZIV_P", + "type": "arrow" + }, + { + "id": "SAwozYSfdz9G24x4r2tY_", + "type": "arrow" + } + ], + "updated": 1698854587746, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 512, + "versionNonce": 97217026, + "isDeleted": false, + "id": "9AhbeVvImNZO0LndntLU9", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 974.6608352661133, + "y": 422.39453125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 115.27989196777344, + "height": 25, + "seed": 1735075123, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854589035, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Create pool", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "3hzC9q_VOdJ1uRbH3vR1L", + "originalText": "Create pool", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 248, + "versionNonce": 587267486, + "isDeleted": false, + "id": "MBS8kFP_JhnRzQkuZIV_P", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 923.0671872028466, + "y": 296.046875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 47.46795582596121, + "height": 64.30468750000006, + "seed": 745201651, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854587747, + "link": null, + "locked": false, + "startBinding": { + "elementId": "z-D9CyMgLtiipaoetummB", + "focus": -0.5739105966193319, + "gap": 4.1484375 + }, + "endBinding": { + "elementId": "3hzC9q_VOdJ1uRbH3vR1L", + "focus": -0.031999053153602276, + "gap": 11.33203125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 47.46795582596121, + 64.30468750000006 + ] + ] + }, + { + "type": "arrow", + "version": 121, + "versionNonce": 1050561693, + "isDeleted": false, + "id": "wGXN7DnaxZM15k8NFJHkh", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 610.9131349415908, + "y": 305.0234375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 36.96990087482175, + "height": 50.6953125, + "seed": 2134346579, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698835027364, + "link": null, + "locked": false, + "startBinding": { + "elementId": "z-D9CyMgLtiipaoetummB", + "focus": 0.43503919413537756, + "gap": 13.125 + }, + "endBinding": { + "elementId": "6JGi5eX6Afv6Il-_5wqEL", + "focus": 0.10109911016167246, + "gap": 15.06640625 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -36.96990087482175, + 50.6953125 + ] + ] + }, + { + "type": "arrow", + "version": 605, + "versionNonce": 355218768, + "isDeleted": false, + "id": "d2nN109BO8U4Ua6sxrWpL", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 580.9103062739425, + "y": 516.1953125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 81.87991382172129, + "height": 74.80078125, + "seed": 279547155, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698851021852, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6JGi5eX6Afv6Il-_5wqEL", + "focus": 0.019404980298737596, + "gap": 18.98828125 + }, + "endBinding": { + "elementId": "7MBBa3YapwkPyc453AgRq", + "focus": -0.07738996967562115, + "gap": 12.265625 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 81.87991382172129, + 74.80078125 + ] + ] + }, + { + "type": "arrow", + "version": 602, + "versionNonce": 955821598, + "isDeleted": false, + "id": "SAwozYSfdz9G24x4r2tY_", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 941.2211001137598, + "y": 513.3515625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 90.45474783521354, + "height": 73.48046875, + "seed": 1716447539, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854587747, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3hzC9q_VOdJ1uRbH3vR1L", + "focus": -0.023045882264659343, + "gap": 15.24609375 + }, + "endBinding": { + "elementId": "7MBBa3YapwkPyc453AgRq", + "focus": 0.008029260937030911, + "gap": 16.4296875 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -90.45474783521354, + 73.48046875 + ] + ] + }, + { + "type": "text", + "version": 23, + "versionNonce": 1270015618, + "isDeleted": false, + "id": "NB8AerS0MQw12giO7SVs-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 612, + "y": 324, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.179962158203125, + "height": 25, + "seed": 206893011, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854578915, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "yes", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "yes", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 22, + "versionNonce": 708197022, + "isDeleted": false, + "id": "g9ErC67BXsi4pFlY9BVIU", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 967, + "y": 306, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 20.41998291015625, + "height": 25, + "seed": 1281122589, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854578915, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "no", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 495, + "versionNonce": 1956571330, + "isDeleted": false, + "id": "NXUrU0scwCH2gC6i0J9u0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 596.9453125, + "y": 786.427734375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 315.4609375, + "height": 116.39453125, + "seed": 1062993341, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "U7REVWFrXBvFybL1bpTsg" + }, + { + "id": "zuBCsX8fyEnHYZnOeax65", + "type": "arrow" + } + ], + "updated": 1698854591835, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 459, + "versionNonce": 1798118878, + "isDeleted": false, + "id": "U7REVWFrXBvFybL1bpTsg", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 605.2258911132812, + "y": 819.625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 298.8997802734375, + "height": 50, + "seed": 1441742365, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854593063, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Add LP tokens amount in user\nposition and pool info", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "NXUrU0scwCH2gC6i0J9u0", + "originalText": "Add LP tokens amount in user position and pool info", + "lineHeight": 1.25, + "baseline": 43 + }, + { + "type": "arrow", + "version": 832, + "versionNonce": 1981794306, + "isDeleted": false, + "id": "zuBCsX8fyEnHYZnOeax65", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 757.2732294290853, + "y": 734.95703125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.42432031837449813, + "height": 35.45703125, + "seed": 1642760477, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854591835, + "link": null, + "locked": false, + "startBinding": { + "elementId": "7MBBa3YapwkPyc453AgRq", + "focus": 0.0032763890837640007, + "gap": 15.30078125 + }, + "endBinding": { + "elementId": "NXUrU0scwCH2gC6i0J9u0", + "focus": 0.024679285984712866, + "gap": 16.013671875 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.42432031837449813, + 35.45703125 + ] + ] + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + } +} \ No newline at end of file diff --git a/contracts/tokenomics/incentives/assets/deposit.png b/contracts/tokenomics/incentives/assets/deposit.png new file mode 100644 index 000000000..67e9f61ac Binary files /dev/null and b/contracts/tokenomics/incentives/assets/deposit.png differ diff --git a/contracts/tokenomics/incentives/assets/incentivize.excalidraw b/contracts/tokenomics/incentives/assets/incentivize.excalidraw new file mode 100644 index 000000000..c46a83daf --- /dev/null +++ b/contracts/tokenomics/incentives/assets/incentivize.excalidraw @@ -0,0 +1,1686 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "line", + "version": 194, + "versionNonce": 1012185330, + "isDeleted": false, + "id": "IBvx9QRGid7OtpP-GqO5O", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -212.53611648252365, + "y": 356.80448208916545, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 562108974, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "line", + "version": 208, + "versionNonce": 1797588782, + "isDeleted": false, + "id": "7VaIagD6CvH2L6fIuCPBB", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -210.98838761953448, + "y": 447.27688603203586, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "width": 446.431225321017, + "height": 2.5795481049819386, + "seed": 1383485618, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 446.431225321017, + -2.5795481049819386 + ] + ] + }, + { + "type": "line", + "version": 415, + "versionNonce": 1670902126, + "isDeleted": false, + "id": "cMM_uOjXAMn-pva52_LEd", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -202.63792700006633, + "y": 558.3505075602158, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 1253078322, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672072, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 520, + "versionNonce": 1254311474, + "isDeleted": false, + "id": "Jm_9uW4EGLHEws1m-cIH7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 570.6839894823809, + "y": 345.01556224221787, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 523943794, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 642, + "versionNonce": 2124394482, + "isDeleted": false, + "id": "p3NyxMrVLsB9LFm9fwOLC", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 582.3805076061647, + "y": 543.6234410027376, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1604325422, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 718, + "versionNonce": 6881326, + "isDeleted": false, + "id": "-Gj855NRd3c3RJWBgciwH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 223.88877223424265, + "y": 545.7987315688791, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1897792750, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "s0d8OoNcUHv6ln42MtW-0", + "type": "arrow" + } + ], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 657, + "versionNonce": 1456005554, + "isDeleted": false, + "id": "4dA27fuNZJs-RmLnHI_hN", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 224.4239322142315, + "y": 434.97783167178966, + "strokeColor": "#e03131", + "backgroundColor": "#e86565", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 500804206, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "s0d8OoNcUHv6ln42MtW-0", + "type": "arrow" + } + ], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 212, + "versionNonce": 2146749038, + "isDeleted": false, + "id": "s0d8OoNcUHv6ln42MtW-0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 234.65118052244776, + "y": 462.6464424343087, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 0.24226777019501355, + "height": 72.76230243821828, + "seed": 1743239922, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": { + "elementId": "4dA27fuNZJs-RmLnHI_hN", + "focus": 0.013254302360040179, + "gap": 8.891952177290566 + }, + "endBinding": { + "elementId": "-Gj855NRd3c3RJWBgciwH", + "focus": 0.0745170887591537, + "gap": 10.401361686380566 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.24226777019501355, + 72.76230243821828 + ] + ] + }, + { + "type": "text", + "version": 67, + "versionNonce": 190743554, + "isDeleted": false, + "id": "vv1OaYhxjUdn2oQelLNuf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -38.75275148070182, + "y": 311.59693903140646, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 420.7396240234375, + "height": 25, + "seed": 1176552686, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145041, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Current schedule with x reward per second", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Current schedule with x reward per second", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 168, + "versionNonce": 1119696158, + "isDeleted": false, + "id": "bO99_gWlUxRRIlUG9tEjp", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -209.66265157319037, + "y": 387.3290344371859, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 381.85968017578125, + "height": 25, + "seed": 1762702258, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "New schedule with y reward per second", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "New schedule with y reward per second", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 54, + "versionNonce": 984789954, + "isDeleted": false, + "id": "mPNM5jZf1fG2VauIa89QW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -53.10849337924094, + "y": 519.558567226335, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 94.95994567871094, + "height": 25, + "seed": 792283694, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "x+y / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "x+y / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 197, + "versionNonce": 97837406, + "isDeleted": false, + "id": "n9I_QQd3INDWJdtnk7l30", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 384.2318250050705, + "y": 509.64196540311525, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 73.0799560546875, + "height": 25, + "seed": 591644658, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "x / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "x / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 598, + "versionNonce": 1482258610, + "isDeleted": false, + "id": "6UVJELH2PILRT8mjjvb-r", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -218.88488487821823, + "y": 799.3480974116667, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 442.90070948181017, + "height": 0.8508658674643357, + "seed": 501220846, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 442.90070948181017, + -0.8508658674643357 + ] + ] + }, + { + "type": "line", + "version": 578, + "versionNonce": 245179246, + "isDeleted": false, + "id": "Pbkhll4-SpmPl6oPKfH8K", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -217.33715601522906, + "y": 889.8205013545371, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "width": 800.3683257553365, + "height": 4.081076106389446, + "seed": 1812273198, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 800.3683257553365, + -4.081076106389446 + ] + ] + }, + { + "type": "line", + "version": 593, + "versionNonce": 264999538, + "isDeleted": false, + "id": "g8ko6A3TQpPiJJ1xacnEv", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -208.9866953957609, + "y": 1000.894122882717, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 1347281518, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 821, + "versionNonce": 1297380402, + "isDeleted": false, + "id": "VmLuowePKIDj8GVMiH13z", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 576.0317392104708, + "y": 986.1670563252387, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1699651310, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "kZJh1UqzaFVOjmHfM1UDE", + "type": "arrow" + } + ], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 897, + "versionNonce": 931241966, + "isDeleted": false, + "id": "c3o-nh5_ttBPtp31MUBIq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 217.5400038385481, + "y": 988.3423468913802, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1547211054, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 378, + "versionNonce": 392883074, + "isDeleted": false, + "id": "XPT0CSz47FzHFl636EEsF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -211.26174271722616, + "y": 749.9170255909744, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 420.7396240234375, + "height": 25, + "seed": 673757166, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Current schedule with x reward per second", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Current schedule with x reward per second", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 490, + "versionNonce": 430769566, + "isDeleted": false, + "id": "sXlj-aLZSz3Lg4jeTqlWV", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -8.669653333511746, + "y": 835.5938564522293, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 381.85968017578125, + "height": 25, + "seed": 1315408430, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "New schedule with y reward per second", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "New schedule with y reward per second", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 232, + "versionNonce": 1741040450, + "isDeleted": false, + "id": "ojy558UD1mdLpPvzana49", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -59.45726177493552, + "y": 962.102182548836, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 94.95994567871094, + "height": 25, + "seed": 1636963438, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "x+y / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "x+y / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 377, + "versionNonce": 2138481118, + "isDeleted": false, + "id": "mb39NotSR7Bt6Gtk9pcVq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 377.88305660937596, + "y": 952.1855807256165, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 71.21995544433594, + "height": 25, + "seed": 997283502, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "y / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "y / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 8, + "versionNonce": 1378054914, + "isDeleted": false, + "id": "YTA5XRMNd4HJpmxNLcFhq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -293.1566199427076, + "y": 310.46750445510565, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 19.6199951171875, + "height": 45, + "seed": 408485298, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "1.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "text", + "version": 96, + "versionNonce": 1782756894, + "isDeleted": false, + "id": "dPxjoPiQhGuFiwFXll935", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -287.66643217413946, + "y": 690.8249807002654, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 35.49598693847656, + "height": 45, + "seed": 120821294, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "2.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "2.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "ellipse", + "version": 745, + "versionNonce": 810259698, + "isDeleted": false, + "id": "k2wwyVyqb8B9nTfggOIWf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 215.01435673874425, + "y": 789.3042226073804, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 286090798, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 943, + "versionNonce": 2073684782, + "isDeleted": false, + "id": "-E_pA6jlMEUpkOTT-qmJ4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 572.062315186236, + "y": 875.1877742160867, + "strokeColor": "#e03131", + "backgroundColor": "#e86565", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 581647150, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "kZJh1UqzaFVOjmHfM1UDE", + "type": "arrow" + } + ], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 110, + "versionNonce": 330311346, + "isDeleted": false, + "id": "kZJh1UqzaFVOjmHfM1UDE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 583.8204344587958, + "y": 904.3616932690735, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 0.5082094773995323, + "height": 71.0877256871454, + "seed": 93323058, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": { + "elementId": "-E_pA6jlMEUpkOTT-qmJ4", + "focus": -0.12752384428046307, + "gap": 10.445863720829879 + }, + "endBinding": { + "elementId": "VmLuowePKIDj8GVMiH13z", + "focus": -0.18073827632845949, + "gap": 10.808814286378816 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.5082094773995323, + 71.0877256871454 + ] + ] + }, + { + "type": "line", + "version": 958, + "versionNonce": 1540380014, + "isDeleted": false, + "id": "AFupcOD0gb8pri1wqaEop", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -209.99726450836147, + "y": 1262.5089394451795, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 640.2136400380547, + "height": 4.230637583011003, + "seed": 1368326126, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 640.2136400380547, + -4.230637583011003 + ] + ] + }, + { + "type": "line", + "version": 693, + "versionNonce": 1209827442, + "isDeleted": false, + "id": "cl339CHzYbFvxEEVRnjax", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -208.4495356453723, + "y": 1352.9813433880497, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "width": 800.3683257553365, + "height": 4.081076106389446, + "seed": 63230510, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 800.3683257553365, + -4.081076106389446 + ] + ] + }, + { + "type": "line", + "version": 708, + "versionNonce": 910129070, + "isDeleted": false, + "id": "wTtWmHSKLQv2JaDSdATEz", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -200.09907502590409, + "y": 1464.0549649162301, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 201743470, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 1034, + "versionNonce": 451410418, + "isDeleted": false, + "id": "pBg7TplHcqOxKmF7Vdirk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 584.9193595803276, + "y": 1449.4422246747108, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 538746542, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "YeajhPKrb8DjNY8qOQQYQ", + "type": "arrow" + } + ], + "updated": 1698836672854, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1164, + "versionNonce": 856707566, + "isDeleted": false, + "id": "dc6Lg7rZi_vtAKvFUek-S", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 17.25333837138379, + "y": 1452.4856807026679, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1420859630, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 609, + "versionNonce": 1493287618, + "isDeleted": false, + "id": "iwEBoGgS2N457ZaMlIRBt", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 0.2179670363450441, + "y": 1298.754698485742, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 385.13970947265625, + "height": 25, + "seed": 609596782, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "New schedule with m reward per second", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "New schedule with m reward per second", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 504, + "versionNonce": 50479710, + "isDeleted": false, + "id": "m20RQ66hK07908X4H8Bo7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -132.43442902667994, + "y": 1417.2423189783299, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 98.23994445800781, + "height": 25, + "seed": 119068590, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145042, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "x+m / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "x+m / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 583, + "versionNonce": 1459624578, + "isDeleted": false, + "id": "gx5z_OO_DKV2ixAMKuwoa", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 288.5429353859577, + "y": 1416.6468846031662, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 108.43994140625, + "height": 25, + "seed": 1639821806, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "z +m / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "z +m / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 214, + "versionNonce": 746997406, + "isDeleted": false, + "id": "xj4Pn5u5PGTVLUPEkh3i8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -278.89313812024193, + "y": 1153.9858227337784, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 34.37998962402344, + "height": 45, + "seed": 1062028334, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "3.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "3.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "ellipse", + "version": 861, + "versionNonce": 2089234606, + "isDeleted": false, + "id": "fo3SugTUMTgrxGizCt-P1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 224.0163034245603, + "y": 1252.4650646408934, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1638960750, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1058, + "versionNonce": 1464072498, + "isDeleted": false, + "id": "6f0dTgTb0oa5wdMC8AL2Z", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 580.9499355560928, + "y": 1338.3486162495994, + "strokeColor": "#e03131", + "backgroundColor": "#e86565", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1706070190, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "YeajhPKrb8DjNY8qOQQYQ", + "type": "arrow" + } + ], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "arrow", + "version": 649, + "versionNonce": 1755555186, + "isDeleted": false, + "id": "YeajhPKrb8DjNY8qOQQYQ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 592.7080540787614, + "y": 1367.5224304087055, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 0.5083915572040496, + "height": 71.20233592374484, + "seed": 1727417070, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672854, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6f0dTgTb0oa5wdMC8AL2Z", + "focus": -0.12752384428047406, + "gap": 10.445863720829937 + }, + "endBinding": { + "elementId": "pBg7TplHcqOxKmF7Vdirk", + "focus": -0.18073827632847073, + "gap": 10.808814286378489 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0.5083915572040496, + 71.20233592374484 + ] + ] + }, + { + "type": "ellipse", + "version": 942, + "versionNonce": 1049756402, + "isDeleted": false, + "id": "nDZfwm-7z9LM1o9NqLGRn", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 16.87092111908356, + "y": 1250.7188558647108, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1134934638, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 594, + "versionNonce": 2061335106, + "isDeleted": false, + "id": "LTk7Sb24oa9h-J08VcB8q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": -121.27991101954734, + "y": 1215.363662147412, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 73.0799560546875, + "height": 25, + "seed": 1042854642, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "x / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "x / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 683, + "versionNonce": 1199555294, + "isDeleted": false, + "id": "1FD4r_vNTFc_6pKMFUOqh", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 88.42918043552658, + "y": 1212.8806374726712, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 71.21995544433594, + "height": 25, + "seed": 574057330, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "y / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "y / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 659, + "versionNonce": 1131995650, + "isDeleted": false, + "id": "xM1xc2Kt4cWm8S5aU_785", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 297.83569160184004, + "y": 1205.3208098298628, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 73.27995300292969, + "height": 25, + "seed": 362610226, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "z / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "z / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "ellipse", + "version": 1017, + "versionNonce": 236940914, + "isDeleted": false, + "id": "99sYtlV9nlZd_NrifD65_", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 231.2149000558881, + "y": 1451.397267347008, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1483648430, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836672073, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 605, + "versionNonce": 1988081438, + "isDeleted": false, + "id": "Qz58xLz-k9luJ-7_aUZ5J", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 96.47599390916861, + "y": 1417.8284223165729, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 116.37994384765625, + "height": 25, + "seed": 802128498, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "y + m / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "y + m / sec", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "ellipse", + "version": 1156, + "versionNonce": 403944626, + "isDeleted": false, + "id": "UEMAczWmUUCVF0tCPqvvj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 425.36599343638284, + "y": 1453.6444939950834, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1829400818, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836677055, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 1067, + "versionNonce": 628647986, + "isDeleted": false, + "id": "tiUa4VAl66cDNXnLVMK4q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 421.2359552723535, + "y": 1248.5502285588595, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1487908914, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698836679553, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 724, + "versionNonce": 2077315522, + "isDeleted": false, + "id": "Jt6StuM_E_ivg414xeepe", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 476.47184691503304, + "y": 1412.8659456644664, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 74.49995422363281, + "height": 25, + "seed": 2056926258, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698851145043, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "m / sec", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "m / sec", + "lineHeight": 1.25, + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/contracts/tokenomics/incentives/assets/incentivize.png b/contracts/tokenomics/incentives/assets/incentivize.png new file mode 100644 index 000000000..ffaaedfd7 Binary files /dev/null and b/contracts/tokenomics/incentives/assets/incentivize.png differ diff --git a/contracts/tokenomics/incentives/assets/schedules_flow.excalidraw b/contracts/tokenomics/incentives/assets/schedules_flow.excalidraw new file mode 100644 index 000000000..7f9f4db67 --- /dev/null +++ b/contracts/tokenomics/incentives/assets/schedules_flow.excalidraw @@ -0,0 +1,7932 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "line", + "version": 363, + "versionNonce": 1284147215, + "isDeleted": false, + "id": "1jsQR9rIB2_7yPPLHJh0R", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1181.508502796505, + "y": 1259.8705405179078, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 255.30515312873422, + "height": 3.4603705643696685, + "seed": 595651905, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258952, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 255.30515312873422, + -3.4603705643696685 + ] + ] + }, + { + "type": "text", + "version": 616, + "versionNonce": 1386406502, + "isDeleted": false, + "id": "k7BvAfC8MOghGJR52cccE", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1059.939772401809, + "y": 305.90341252860617, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 78.2999267578125, + "height": 25, + "seed": 248668737, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940984, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps_new", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps_new", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 451, + "versionNonce": 142381359, + "isDeleted": false, + "id": "wsZ5EbZ5BwRL5XzlPTUZB", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 611.4715258690671, + "y": 88.45310731884666, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 815186049, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 769, + "versionNonce": 1295011105, + "isDeleted": false, + "id": "qx37xddQbheZ15U7VMFKn", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1395.4509427126463, + "y": 76.07896061091208, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 924656673, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 253, + "versionNonce": 1195335418, + "isDeleted": false, + "id": "oickl9yzpb8WpInSC34xo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 531.4715258690671, + "y": 45.53090282379992, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 19.6199951171875, + "height": 45, + "seed": 2021467905, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940984, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "1.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "diamond", + "version": 613, + "versionNonce": 1371638849, + "isDeleted": false, + "id": "8CyofUsUf0MdS-Z6b9J9q", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 845.2199370068209, + "y": 75.16008962662335, + "strokeColor": "#1971c2", + "backgroundColor": "#15aabf", + "width": 27.8612515301719, + "height": 22.0703795400147, + "seed": 735455343, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 543, + "versionNonce": 1310528591, + "isDeleted": false, + "id": "rGr_ztufSRp2TcK3-C8g8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1035.6766694228252, + "y": 76.02097658076423, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 21.623593161747976, + "height": 18.85612025559044, + "seed": 2070270049, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "freedraw", + "version": 438, + "versionNonce": 1099752385, + "isDeleted": false, + "id": "r9hjjqcRikFnFkcCZ4Aa-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 857.695194795006, + "y": 118.81703879416546, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 190.2962952870373, + "height": 37.334857842159295, + "seed": 938101857, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0043377318278318144, + 1.4661533578075703 + ], + [ + 0.0043377318278319255, + 3.0537632067944855 + ], + [ + 0.004337731827831703, + 4.112169772785762 + ], + [ + 0.004337731827831703, + 4.95368974738534 + ], + [ + 0.004337731827831703, + 5.786534258329311 + ], + [ + 0.22556205504724813, + 7.682123067092391 + ], + [ + 0.9456255384675387, + 8.888012515229946 + ], + [ + 1.748105926616745, + 10.800952251304352 + ], + [ + 2.3510506506854654, + 11.213036774948478 + ], + [ + 2.68505600142862, + 11.508002539241147 + ], + [ + 4.211937604825835, + 12.700878791895263 + ], + [ + 4.567631614708262, + 13.043559606294082 + ], + [ + 5.049119847597694, + 13.525047839183514 + ], + [ + 5.886302090369554, + 13.841702262615343 + ], + [ + 7.768877703648968, + 14.63984491893666 + ], + [ + 8.289405522989, + 14.813354192049985 + ], + [ + 9.768572076280066, + 15.195074592899289 + ], + [ + 10.905057815172313, + 15.550768602781602 + ], + [ + 12.366873441152165, + 15.923813539975242 + ], + [ + 16.37927538189774, + 17.042948351556163 + ], + [ + 19.146748288055278, + 17.719634516698136 + ], + [ + 21.302601006488203, + 18.279201922488596 + ], + [ + 22.959614564720482, + 18.513439441191565 + ], + [ + 30.954054323416813, + 20.417703713610308 + ], + [ + 35.08357502351396, + 21.445746156806763 + ], + [ + 39.20875799178316, + 22.47378860000316 + ], + [ + 41.62053688805838, + 22.829482609885474 + ], + [ + 46.47445880340365, + 23.527857434166606 + ], + [ + 49.72775767427834, + 24.000670203400432 + ], + [ + 51.54526731014039, + 24.25659638124256 + ], + [ + 55.27137895024907, + 24.82917698251657 + ], + [ + 57.258060127396675, + 24.937620278212364 + ], + [ + 59.60477304625442, + 24.937620278212364 + ], + [ + 64.54111186632849, + 24.937620278212364 + ], + [ + 66.98325488539854, + 24.937620278212364 + ], + [ + 67.29990930883037, + 24.937620278212364 + ], + [ + 68.6966589573924, + 24.937620278212364 + ], + [ + 69.51649027285293, + 24.937620278212364 + ], + [ + 70.00231623757031, + 24.937620278212364 + ], + [ + 70.46211581132047, + 25.080765428530867 + ], + [ + 70.68767786636795, + 25.306327483578173 + ], + [ + 71.4034036179603, + 25.761789325500672 + ], + [ + 71.61595247752416, + 25.91360993947484 + ], + [ + 71.906580509989, + 26.20423797193962 + ], + [ + 72.21022173793722, + 26.624997959239465 + ], + [ + 72.40108193836204, + 27.13685031492372 + ], + [ + 72.40108193836204, + 27.553272570395734 + ], + [ + 72.40108193836204, + 27.869926993827562 + ], + [ + 72.71339862996592, + 28.650718722837496 + ], + [ + 72.71339862996592, + 29.56598013851027 + ], + [ + 72.71339862996592, + 29.709125288828773 + ], + [ + 72.71339862996592, + 30.255679499135738 + ], + [ + 72.71339862996592, + 30.780545050303544 + ], + [ + 72.71339862996592, + 31.066835350940494 + ], + [ + 72.71339862996592, + 31.209980501258997 + ], + [ + 72.71339862996592, + 31.504946265551666 + ], + [ + 72.71339862996592, + 31.799912029844336 + ], + [ + 72.71339862996592, + 32.09487779413695 + ], + [ + 72.71339862996592, + 32.94507323239225 + ], + [ + 72.71339862996592, + 33.235701264857084 + ], + [ + 72.71339862996592, + 33.37884641517553 + ], + [ + 72.71339862996592, + 34.10324763042365 + ], + [ + 72.71339862996592, + 34.454603908478134 + ], + [ + 72.71339862996592, + 34.60208679062447 + ], + [ + 72.71339862996592, + 34.74523194094297 + ], + [ + 72.71339862996592, + 35.335163469528254 + ], + [ + 72.71339862996592, + 35.48264635167459 + ], + [ + 72.71339862996592, + 35.842678093384734 + ], + [ + 72.71339862996592, + 36.055226952948544 + ], + [ + 72.71339862996592, + 36.34151725358555 + ], + [ + 72.71339862996592, + 36.62780755422256 + ], + [ + 72.71339862996592, + 37.00519022324403 + ], + [ + 72.71339862996592, + 37.29148052388098 + ], + [ + 73.11680768995438, + 37.334857842159295 + ], + [ + 73.34236974500163, + 37.334857842159295 + ], + [ + 73.77180519595731, + 37.334857842159295 + ], + [ + 74.03206910562722, + 37.334857842159295 + ], + [ + 74.34872352905904, + 37.334857842159295 + ], + [ + 74.97769464409475, + 37.09194485980066 + ], + [ + 75.62835441826974, + 36.432609621970016 + ], + [ + 76.30070285158399, + 35.75158572500027 + ], + [ + 76.93834943027537, + 35.10526368265312 + ], + [ + 77.79288260035855, + 33.981791139244365 + ], + [ + 78.48258196098391, + 33.3745086833477 + ], + [ + 79.90535800051316, + 32.11656645327611 + ], + [ + 81.47561692218869, + 30.875975150515842 + ], + [ + 83.48832449030328, + 29.61803292044425 + ], + [ + 86.09096358700322, + 28.273336053816024 + ], + [ + 88.8237346385381, + 27.29734639255355 + ], + [ + 92.13342402317471, + 26.334369926774627 + ], + [ + 95.57324536264628, + 25.59261778421518 + ], + [ + 98.03273930902765, + 25.046063573908214 + ], + [ + 100.89564231539748, + 24.538548950051734 + ], + [ + 105.57171722580142, + 23.88788917587675 + ], + [ + 109.41060989343373, + 23.714379902763426 + ], + [ + 113.80473223502872, + 23.345672697397617 + ], + [ + 115.45307032960523, + 23.345672697397617 + ], + [ + 121.57794767050564, + 23.345672697397617 + ], + [ + 125.70746837060278, + 23.345672697397617 + ], + [ + 130.12327937133682, + 23.345672697397617 + ], + [ + 135.2244520008685, + 23.345672697397617 + ], + [ + 141.2712501688677, + 23.345672697397617 + ], + [ + 144.84120346317445, + 23.345672697397617 + ], + [ + 151.04415997697583, + 23.345672697397617 + ], + [ + 155.91977055146003, + 23.345672697397617 + ], + [ + 160.75200380766614, + 23.345672697397617 + ], + [ + 165.04202058539317, + 23.345672697397617 + ], + [ + 167.45379948166828, + 23.345672697397617 + ], + [ + 170.72444927985453, + 23.345672697397617 + ], + [ + 174.77155307522276, + 23.345672697397617 + ], + [ + 176.24204416485804, + 23.345672697397617 + ], + [ + 178.25475173297264, + 23.345672697397617 + ], + [ + 180.60146465183038, + 23.13746156966164 + ], + [ + 183.6118505403465, + 22.434749013552675 + ], + [ + 184.9999247252531, + 21.93590985335186 + ], + [ + 185.91084840909798, + 21.358991520250072 + ], + [ + 186.57018364692863, + 20.90352967832763 + ], + [ + 187.2208434211036, + 20.37432639533199 + ], + [ + 187.86282773162293, + 20.0489965082445 + ], + [ + 188.3356405008567, + 19.632574252772542 + ], + [ + 188.6739835834278, + 19.08602004246552 + ], + [ + 189.03835305696566, + 18.71297510527188 + ], + [ + 189.2595773801852, + 18.326916972594745 + ], + [ + 189.64563551286233, + 17.771687298632116 + ], + [ + 189.9536144726385, + 17.19476896553033 + ], + [ + 190.252917968759, + 16.830399491992353 + ], + [ + 190.2962952870373, + 16.52675826404402 + ], + [ + 190.2962952870373, + 16.22745476792352 + ], + [ + 190.2962952870373, + 15.589808189232087 + ], + [ + 190.2962952870373, + 15.268816033972428 + ], + [ + 190.2962952870373, + 14.475011109478999 + ], + [ + 190.2962952870373, + 14.136668026908012 + ], + [ + 190.2962952870373, + 13.152002901989874 + ], + [ + 190.2962952870373, + 12.501343127814948 + ], + [ + 190.2962952870373, + 11.855021085467797 + ], + [ + 190.2962952870373, + 11.534028930208137 + ], + [ + 190.2962952870373, + 11.286778216021673 + ], + [ + 190.2962952870373, + 10.39754319131589 + ], + [ + 190.2962952870373, + 10.080888767884062 + ], + [ + 190.2962952870373, + 9.707843830690422 + ], + [ + 190.2962952870373, + 9.486619507470948 + ], + [ + 190.2962952870373, + 9.2220178659731 + ], + [ + 190.2962952870373, + 9.087548179310318 + ], + [ + 190.2962952870373, + 8.809933342328975 + ], + [ + 190.16616333220236, + 8.805595610501143 + ], + [ + 190.0360313773674, + 8.805595610501143 + ], + [ + 189.77576746769728, + 8.805595610501143 + ], + [ + 189.50249036254377, + 8.805595610501143 + ], + [ + 189.50249036254377, + 8.805595610501143 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 890, + "versionNonce": 1445307814, + "isDeleted": false, + "id": "jadZYYntZDOXat2d8kQtW", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 807.8982736779853, + "y": 193.44563989114158, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 267.31976318359375, + "height": 50, + "seed": 86104367, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "accrued_reward = \n(block_ts - last_ts) * rps", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "accrued_reward = \n(block_ts - last_ts) * rps", + "lineHeight": 1.25, + "baseline": 43 + }, + { + "type": "diamond", + "version": 616, + "versionNonce": 412926881, + "isDeleted": false, + "id": "FqBOvDw5fk5iHqqN4TEZH", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 614.8407718160294, + "y": -140.50124533735234, + "strokeColor": "#1971c2", + "backgroundColor": "#15aabf", + "width": 27.8612515301719, + "height": 22.0703795400147, + "seed": 1838028079, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 625, + "versionNonce": 1597735663, + "isDeleted": false, + "id": "GTwD9AN2GruwTiBD0zjnb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 618.2849308873288, + "y": -92.35459091431915, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 21.623593161747976, + "height": 18.85612025559044, + "seed": 77491375, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "ellipse", + "version": 824, + "versionNonce": 1717938049, + "isDeleted": false, + "id": "RLM3bRf8uU4aaIRcX3t27", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 619.9651229289838, + "y": -42.942867202722624, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1232815713, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 192, + "versionNonce": 1152217018, + "isDeleted": false, + "id": "DggWAibseCWjlbFdmh3A0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 707.1652072637038, + "y": -141.33408984829634, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 417.7796630859375, + "height": 25, + "seed": 1408909391, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "last time when pool rewards were updated", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "last time when pool rewards were updated", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 367, + "versionNonce": 988676326, + "isDeleted": false, + "id": "_vrQk_Accn37f4wZVZWc9", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 710.2118274618541, + "y": -96.22901117467296, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 175.73983764648438, + "height": 25, + "seed": 236989217, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "current block time", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "current block time", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 355, + "versionNonce": 865822842, + "isDeleted": false, + "id": "iNtqk9WCi-P2OgybxUMGf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 705.594948342028, + "y": -49.11690579257788, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 653.7994384765625, + "height": 25, + "seed": 795584673, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "next time when reward per second should be updated (or removed)", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "next time when reward per second should be updated (or removed)", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 607, + "versionNonce": 954297121, + "isDeleted": false, + "id": "lHvCvvJBohUVYE6PPjnfA", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 606.7826343837527, + "y": 389.4911605642729, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 286142735, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 1229, + "versionNonce": 1352078191, + "isDeleted": false, + "id": "Xh8N7MmOvkr5t_7jDfhJF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1026.084598729576, + "y": 376.4620163503356, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 895694639, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "Dz1mBBvcz_XC4USj-NU9j", + "type": "arrow" + } + ], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 410, + "versionNonce": 1955132454, + "isDeleted": false, + "id": "qAFz6koIDCEtaI8auAEZ9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 526.7826343837527, + "y": 346.4388241143912, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 35.49598693847656, + "height": 45, + "seed": 829585743, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "2.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "2.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "diamond", + "version": 768, + "versionNonce": 710987151, + "isDeleted": false, + "id": "FOBDweXYdfOvljCmtb3yE", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 840.5310455215066, + "y": 376.06801091721456, + "strokeColor": "#1971c2", + "backgroundColor": "#15aabf", + "width": 27.8612515301719, + "height": 22.0703795400147, + "seed": 177502063, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 908, + "versionNonce": 1124587233, + "isDeleted": false, + "id": "Ud9DWsJgNx8Bzj1EpzIxv", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1168.99271604002, + "y": 375.5451614182766, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 21.623593161747976, + "height": 18.85612025559044, + "seed": 118600079, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1698854258953, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1661, + "versionNonce": 734808378, + "isDeleted": false, + "id": "6zs8j0qjY600aoutD6DMp", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 499.6117528037413, + "y": 489.4345732889699, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 486.53961181640625, + "height": 25, + "seed": 1408227791, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "Dz1mBBvcz_XC4USj-NU9j", + "type": "arrow" + } + ], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "accrued_reward = (end_ts - last_ts) * rps_old", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "accrued_reward = (end_ts - last_ts) * rps_old", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "ellipse", + "version": 1353, + "versionNonce": 1406554785, + "isDeleted": false, + "id": "C8ZZEQTg43LdsMr_7tR8L", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1391.7517206643572, + "y": 375.28822244145954, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 2008097281, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "-Oss2ZzO5I4ednR6BlyJo", + "type": "arrow" + } + ], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "freedraw", + "version": 575, + "versionNonce": 963011567, + "isDeleted": false, + "id": "IgeoRwq-K1JsZ6ZPcp1IY", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 848.1521847737738, + "y": 414.86506717785886, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 190.2962952870373, + "height": 37.334857842159295, + "seed": 1267476961, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0043377318278318144, + 1.4661533578075703 + ], + [ + 0.0043377318278319255, + 3.0537632067944855 + ], + [ + 0.004337731827831703, + 4.112169772785762 + ], + [ + 0.004337731827831703, + 4.95368974738534 + ], + [ + 0.004337731827831703, + 5.786534258329311 + ], + [ + 0.22556205504724813, + 7.682123067092391 + ], + [ + 0.9456255384675387, + 8.888012515229946 + ], + [ + 1.748105926616745, + 10.800952251304352 + ], + [ + 2.3510506506854654, + 11.213036774948478 + ], + [ + 2.68505600142862, + 11.508002539241147 + ], + [ + 4.211937604825835, + 12.700878791895263 + ], + [ + 4.567631614708262, + 13.043559606294082 + ], + [ + 5.049119847597694, + 13.525047839183514 + ], + [ + 5.886302090369554, + 13.841702262615343 + ], + [ + 7.768877703648968, + 14.63984491893666 + ], + [ + 8.289405522989, + 14.813354192049985 + ], + [ + 9.768572076280066, + 15.195074592899289 + ], + [ + 10.905057815172313, + 15.550768602781602 + ], + [ + 12.366873441152165, + 15.923813539975242 + ], + [ + 16.37927538189774, + 17.042948351556163 + ], + [ + 19.146748288055278, + 17.719634516698136 + ], + [ + 21.302601006488203, + 18.279201922488596 + ], + [ + 22.959614564720482, + 18.513439441191565 + ], + [ + 30.954054323416813, + 20.417703713610308 + ], + [ + 35.08357502351396, + 21.445746156806763 + ], + [ + 39.20875799178316, + 22.47378860000316 + ], + [ + 41.62053688805838, + 22.829482609885474 + ], + [ + 46.47445880340365, + 23.527857434166606 + ], + [ + 49.72775767427834, + 24.000670203400432 + ], + [ + 51.54526731014039, + 24.25659638124256 + ], + [ + 55.27137895024907, + 24.82917698251657 + ], + [ + 57.258060127396675, + 24.937620278212364 + ], + [ + 59.60477304625442, + 24.937620278212364 + ], + [ + 64.54111186632849, + 24.937620278212364 + ], + [ + 66.98325488539854, + 24.937620278212364 + ], + [ + 67.29990930883037, + 24.937620278212364 + ], + [ + 68.6966589573924, + 24.937620278212364 + ], + [ + 69.51649027285293, + 24.937620278212364 + ], + [ + 70.00231623757031, + 24.937620278212364 + ], + [ + 70.46211581132047, + 25.080765428530867 + ], + [ + 70.68767786636795, + 25.306327483578173 + ], + [ + 71.4034036179603, + 25.761789325500672 + ], + [ + 71.61595247752416, + 25.91360993947484 + ], + [ + 71.906580509989, + 26.20423797193962 + ], + [ + 72.21022173793722, + 26.624997959239465 + ], + [ + 72.40108193836204, + 27.13685031492372 + ], + [ + 72.40108193836204, + 27.553272570395734 + ], + [ + 72.40108193836204, + 27.869926993827562 + ], + [ + 72.71339862996592, + 28.650718722837496 + ], + [ + 72.71339862996592, + 29.56598013851027 + ], + [ + 72.71339862996592, + 29.709125288828773 + ], + [ + 72.71339862996592, + 30.255679499135738 + ], + [ + 72.71339862996592, + 30.780545050303544 + ], + [ + 72.71339862996592, + 31.066835350940494 + ], + [ + 72.71339862996592, + 31.209980501258997 + ], + [ + 72.71339862996592, + 31.504946265551666 + ], + [ + 72.71339862996592, + 31.799912029844336 + ], + [ + 72.71339862996592, + 32.09487779413695 + ], + [ + 72.71339862996592, + 32.94507323239225 + ], + [ + 72.71339862996592, + 33.235701264857084 + ], + [ + 72.71339862996592, + 33.37884641517553 + ], + [ + 72.71339862996592, + 34.10324763042365 + ], + [ + 72.71339862996592, + 34.454603908478134 + ], + [ + 72.71339862996592, + 34.60208679062447 + ], + [ + 72.71339862996592, + 34.74523194094297 + ], + [ + 72.71339862996592, + 35.335163469528254 + ], + [ + 72.71339862996592, + 35.48264635167459 + ], + [ + 72.71339862996592, + 35.842678093384734 + ], + [ + 72.71339862996592, + 36.055226952948544 + ], + [ + 72.71339862996592, + 36.34151725358555 + ], + [ + 72.71339862996592, + 36.62780755422256 + ], + [ + 72.71339862996592, + 37.00519022324403 + ], + [ + 72.71339862996592, + 37.29148052388098 + ], + [ + 73.11680768995438, + 37.334857842159295 + ], + [ + 73.34236974500163, + 37.334857842159295 + ], + [ + 73.77180519595731, + 37.334857842159295 + ], + [ + 74.03206910562722, + 37.334857842159295 + ], + [ + 74.34872352905904, + 37.334857842159295 + ], + [ + 74.97769464409475, + 37.09194485980066 + ], + [ + 75.62835441826974, + 36.432609621970016 + ], + [ + 76.30070285158399, + 35.75158572500027 + ], + [ + 76.93834943027537, + 35.10526368265312 + ], + [ + 77.79288260035855, + 33.981791139244365 + ], + [ + 78.48258196098391, + 33.3745086833477 + ], + [ + 79.90535800051316, + 32.11656645327611 + ], + [ + 81.47561692218869, + 30.875975150515842 + ], + [ + 83.48832449030328, + 29.61803292044425 + ], + [ + 86.09096358700322, + 28.273336053816024 + ], + [ + 88.8237346385381, + 27.29734639255355 + ], + [ + 92.13342402317471, + 26.334369926774627 + ], + [ + 95.57324536264628, + 25.59261778421518 + ], + [ + 98.03273930902765, + 25.046063573908214 + ], + [ + 100.89564231539748, + 24.538548950051734 + ], + [ + 105.57171722580142, + 23.88788917587675 + ], + [ + 109.41060989343373, + 23.714379902763426 + ], + [ + 113.80473223502872, + 23.345672697397617 + ], + [ + 115.45307032960523, + 23.345672697397617 + ], + [ + 121.57794767050564, + 23.345672697397617 + ], + [ + 125.70746837060278, + 23.345672697397617 + ], + [ + 130.12327937133682, + 23.345672697397617 + ], + [ + 135.2244520008685, + 23.345672697397617 + ], + [ + 141.2712501688677, + 23.345672697397617 + ], + [ + 144.84120346317445, + 23.345672697397617 + ], + [ + 151.04415997697583, + 23.345672697397617 + ], + [ + 155.91977055146003, + 23.345672697397617 + ], + [ + 160.75200380766614, + 23.345672697397617 + ], + [ + 165.04202058539317, + 23.345672697397617 + ], + [ + 167.45379948166828, + 23.345672697397617 + ], + [ + 170.72444927985453, + 23.345672697397617 + ], + [ + 174.77155307522276, + 23.345672697397617 + ], + [ + 176.24204416485804, + 23.345672697397617 + ], + [ + 178.25475173297264, + 23.345672697397617 + ], + [ + 180.60146465183038, + 23.13746156966164 + ], + [ + 183.6118505403465, + 22.434749013552675 + ], + [ + 184.9999247252531, + 21.93590985335186 + ], + [ + 185.91084840909798, + 21.358991520250072 + ], + [ + 186.57018364692863, + 20.90352967832763 + ], + [ + 187.2208434211036, + 20.37432639533199 + ], + [ + 187.86282773162293, + 20.0489965082445 + ], + [ + 188.3356405008567, + 19.632574252772542 + ], + [ + 188.6739835834278, + 19.08602004246552 + ], + [ + 189.03835305696566, + 18.71297510527188 + ], + [ + 189.2595773801852, + 18.326916972594745 + ], + [ + 189.64563551286233, + 17.771687298632116 + ], + [ + 189.9536144726385, + 17.19476896553033 + ], + [ + 190.252917968759, + 16.830399491992353 + ], + [ + 190.2962952870373, + 16.52675826404402 + ], + [ + 190.2962952870373, + 16.22745476792352 + ], + [ + 190.2962952870373, + 15.589808189232087 + ], + [ + 190.2962952870373, + 15.268816033972428 + ], + [ + 190.2962952870373, + 14.475011109478999 + ], + [ + 190.2962952870373, + 14.136668026908012 + ], + [ + 190.2962952870373, + 13.152002901989874 + ], + [ + 190.2962952870373, + 12.501343127814948 + ], + [ + 190.2962952870373, + 11.855021085467797 + ], + [ + 190.2962952870373, + 11.534028930208137 + ], + [ + 190.2962952870373, + 11.286778216021673 + ], + [ + 190.2962952870373, + 10.39754319131589 + ], + [ + 190.2962952870373, + 10.080888767884062 + ], + [ + 190.2962952870373, + 9.707843830690422 + ], + [ + 190.2962952870373, + 9.486619507470948 + ], + [ + 190.2962952870373, + 9.2220178659731 + ], + [ + 190.2962952870373, + 9.087548179310318 + ], + [ + 190.2962952870373, + 8.809933342328975 + ], + [ + 190.16616333220236, + 8.805595610501143 + ], + [ + 190.0360313773674, + 8.805595610501143 + ], + [ + 189.77576746769728, + 8.805595610501143 + ], + [ + 189.50249036254377, + 8.805595610501143 + ], + [ + 189.50249036254377, + 8.805595610501143 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 262, + "versionNonce": 1307708943, + "isDeleted": false, + "id": "vRJPkwUGuNZy5iiZ4L-rE", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1048.2777803826807, + "y": 415.7521333366507, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 132.45697909471073, + "height": 30.975742982556085, + "seed": 1887587599, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.34268081439881826 + ], + [ + -2.7755575615628914e-17, + 0.4858259647173213 + ], + [ + 0, + 1.0714197614747718 + ], + [ + 0, + 1.7220795356497547 + ], + [ + 6.661338147750939e-16, + 2.0560848863929095 + ], + [ + 0, + 3.0581009386223172 + ], + [ + 0, + 3.8909454495662885 + ], + [ + 0, + 4.224950800309443 + ], + [ + 0, + 5.6086872533882115 + ], + [ + 0, + 5.925341676819983 + ], + [ + 0, + 7.300402666243087 + ], + [ + 0, + 7.946724708590239 + ], + [ + 0, + 8.263379132022067 + ], + [ + 0, + 9.187316011350504 + ], + [ + 0, + 9.89870403111513 + ], + [ + 0, + 10.536350609806561 + ], + [ + 0, + 11.351844193439206 + ], + [ + 0, + 11.65982315321537 + ], + [ + 0, + 12.301807463734633 + ], + [ + 0, + 12.661839205444778 + ], + [ + 0, + 12.956804969737448 + ], + [ + 0, + 13.247433002202229 + ], + [ + 0, + 13.538061034667066 + ], + [ + 0, + 13.828689067131904 + ], + [ + 0, + 14.114979367768854 + ], + [ + 0, + 14.483686573134662 + ], + [ + 0, + 14.635507187108828 + ], + [ + 0, + 14.848056046672639 + ], + [ + 0, + 15.364246134184782 + ], + [ + 0, + 15.650536434821788 + ], + [ + 0, + 15.945502199114458 + ], + [ + 0, + 16.24046796340707 + ], + [ + 0, + 16.383613113725573 + ], + [ + 0, + 16.56146011866673 + ], + [ + 0.13013195483472373, + 16.704605268985233 + ], + [ + 0.5031768920284776, + 16.704605268985233 + ], + [ + 0.7591030698706618, + 16.704605268985233 + ], + [ + 0.9282746111562119, + 16.704605268985233 + ], + [ + 1.062744297818881, + 16.704605268985233 + ], + [ + 1.331683671144674, + 16.704605268985233 + ], + [ + 1.6266494354372298, + 16.704605268985233 + ], + [ + 1.9216151997297857, + 16.704605268985233 + ], + [ + 2.216580964022569, + 16.413977236520395 + ], + [ + 2.559261778421387, + 16.409639504692564 + ], + [ + 2.8672407381975518, + 16.32288486813593 + ], + [ + 3.153531038834444, + 16.114673740399894 + ], + [ + 3.4484968031272274, + 16.114673740399894 + ], + [ + 3.739124835592065, + 16.114673740399894 + ], + [ + 4.155547091063909, + 16.114673740399894 + ], + [ + 4.6717371785762225, + 16.114673740399894 + ], + [ + 4.966702942868778, + 16.114673740399894 + ], + [ + 5.331072416406641, + 15.897787149008252 + ], + [ + 5.547959007798454, + 15.819707976107281 + ], + [ + 5.834249308435346, + 15.819707976107281 + ], + [ + 6.202956513801155, + 15.585470457404256 + ], + [ + 6.766861651419504, + 15.524742211814612 + ], + [ + 7.126893393129649, + 15.28182922945598 + ], + [ + 7.421859157422205, + 15.229776447522 + ], + [ + 7.71248718988727, + 15.229776447522 + ], + [ + 8.007452954179826, + 14.96083707419632 + ], + [ + 8.298080986644436, + 14.93481068322933 + ], + [ + 9.013806738237008, + 14.856731510328302 + ], + [ + 9.161289620383286, + 14.709248628182024 + ], + [ + 9.547347753060421, + 14.63984491893666 + ], + [ + 10.237047113686003, + 14.548752550552194 + ], + [ + 10.64479390550241, + 14.344879154644048 + ], + [ + 10.961448328934239, + 14.344879154644048 + ], + [ + 11.208699043120532, + 14.344879154644048 + ], + [ + 12.349522513840839, + 14.219084931636871 + ], + [ + 13.308161247791759, + 13.989185144761734 + ], + [ + 14.171369881530609, + 13.989185144761734 + ], + [ + 15.485702625363956, + 13.989185144761734 + ], + [ + 16.500731873076802, + 13.989185144761734 + ], + [ + 18.075328526580506, + 13.989185144761734 + ], + [ + 19.94489094437631, + 13.989185144761734 + ], + [ + 21.59756677078076, + 13.989185144761734 + ], + [ + 23.137461569661582, + 13.989185144761734 + ], + [ + 25.184870992398828, + 13.989185144761734 + ], + [ + 26.798507232352677, + 13.989185144761734 + ], + [ + 28.338402031233272, + 13.989185144761734 + ], + [ + 30.294719085586166, + 14.084615244974032 + ], + [ + 31.092861741907427, + 14.271137713570852 + ], + [ + 33.4178860016259, + 14.470673377651167 + ], + [ + 34.80162245470456, + 14.735275019149014 + ], + [ + 35.86436675252389, + 14.843718314844807 + ], + [ + 36.93144878217072, + 15.077955833547833 + ], + [ + 37.45197660151075, + 15.077955833547833 + ], + [ + 38.90945449566266, + 15.550768602781602 + ], + [ + 39.56445200166536, + 15.73295333955059 + ], + [ + 40.28451548508565, + 15.854409830729935 + ], + [ + 41.152061850652444, + 16.270832086201892 + ], + [ + 41.555470910640906, + 16.518082800388413 + ], + [ + 42.18877975750456, + 16.834737223820184 + ], + [ + 42.73099623598364, + 17.220795356497376 + ], + [ + 43.303576837257424, + 17.533112048101316 + ], + [ + 43.65927084713985, + 17.828077812393985 + ], + [ + 43.94989887960469, + 18.118705844858823 + ], + [ + 44.24486464389747, + 18.413671609151436 + ], + [ + 44.86516029527752, + 18.734663764411096 + ], + [ + 45.25555615978237, + 19.103370969776904 + ], + [ + 45.40303904192888, + 19.25085385192324 + ], + [ + 45.615587901492745, + 19.46340271148705 + ], + [ + 46.127440257176886, + 19.688964766534355 + ], + [ + 46.27058540749522, + 19.832109916852858 + ], + [ + 47.055714868333325, + 20.300584954258852 + ], + [ + 47.46779939197722, + 20.599888450379353 + ], + [ + 48.12279689798015, + 20.925218337466788 + ], + [ + 48.764781208499244, + 21.241872760898616 + ], + [ + 49.272295832355894, + 21.562864916158276 + ], + [ + 49.89259148373594, + 21.94892304883541 + ], + [ + 50.19623271168439, + 22.170147372054885 + ], + [ + 50.656032285434776, + 22.469450868175386 + ], + [ + 51.19824876391385, + 22.85550900085252 + ], + [ + 51.48887679637846, + 23.14613703331736 + ], + [ + 51.77950482884353, + 23.43676506578214 + ], + [ + 52.07013286130814, + 23.727393098246978 + ], + [ + 52.38678728473997, + 24.330337822315755 + ], + [ + 52.70777943999974, + 24.980997596490738 + ], + [ + 52.73380583096673, + 25.458148097552396 + ], + [ + 52.73380583096673, + 25.783477984639887 + ], + [ + 52.73380583096673, + 26.087119212588163 + ], + [ + 52.73380583096673, + 26.945990114499125 + ], + [ + 52.73380583096673, + 27.345061442659755 + ], + [ + 52.73380583096673, + 27.878602457483225 + ], + [ + 52.73380583096673, + 28.02174760780173 + ], + [ + 52.73380583096673, + 28.576977281764357 + ], + [ + 52.73380583096673, + 29.106180564759995 + ], + [ + 52.73380583096673, + 29.392470865397 + ], + [ + 52.73380583096673, + 29.691774361517446 + ], + [ + 52.73380583096673, + 30.056143835055423 + ], + [ + 52.73380583096673, + 30.264354962791458 + ], + [ + 52.73380583096673, + 30.633062168157267 + ], + [ + 52.73380583096673, + 30.923690200622048 + ], + [ + 52.859600053973736, + 30.975742982556085 + ], + [ + 52.989732008808915, + 30.975742982556085 + ], + [ + 53.2630091139622, + 30.975742982556085 + ], + [ + 53.618703123844625, + 30.975742982556085 + ], + [ + 53.761848274163185, + 30.975742982556085 + ], + [ + 54.1999591887743, + 30.594022581706724 + ], + [ + 54.4992626848948, + 30.18627578989043 + ], + [ + 54.65542103069674, + 29.86528363463077 + ], + [ + 54.99810184509556, + 29.258001178734162 + ], + [ + 55.11522060444713, + 28.58565274542002 + ], + [ + 55.43621275970668, + 28.05211173059655 + ], + [ + 55.74852945131079, + 27.57929896136278 + ], + [ + 56.14326304764336, + 26.7724808413858 + ], + [ + 56.38183829817444, + 26.29099260849631 + ], + [ + 56.71150591708965, + 25.449472633896733 + ], + [ + 57.13660363621739, + 24.408416995216783 + ], + [ + 57.76991248308104, + 23.419414138470813 + ], + [ + 58.42490998908352, + 22.69501292322269 + ], + [ + 58.828319049072206, + 21.671308211854125 + ], + [ + 59.8086464421624, + 20.912205141983293 + ], + [ + 60.507021266443644, + 20.23985670866921 + ], + [ + 61.43095814577191, + 19.71065342567357 + ], + [ + 61.960161428767606, + 19.354959415791257 + ], + [ + 62.73661542594982, + 19.007940869564607 + ], + [ + 63.77767106462966, + 18.83009386462345 + ], + [ + 65.82941821919485, + 18.352943363561792 + ], + [ + 66.5841835572378, + 18.16208316313714 + ], + [ + 68.89619462147266, + 18.092679453891833 + ], + [ + 70.65731374357301, + 17.92784564443417 + ], + [ + 72.63531945706472, + 17.67625719841982 + ], + [ + 74.87792681205451, + 17.485396997995167 + ], + [ + 77.27669251284624, + 17.25983494294786 + ], + [ + 80.16562191018306, + 16.947518251343865 + ], + [ + 81.69250351358028, + 16.843412687475904 + ], + [ + 84.35153312404191, + 16.50940733673275 + ], + [ + 86.42496893774614, + 16.42265270017606 + ], + [ + 88.08632022780625, + 16.097322813088567 + ], + [ + 90.09035233226518, + 16.036594567498923 + ], + [ + 91.71700176770264, + 16.036594567498923 + ], + [ + 93.77308665409555, + 16.036594567498923 + ], + [ + 95.7250659766205, + 15.672225093960947 + ], + [ + 96.55791048756419, + 15.650536434821788 + ], + [ + 97.71174715376787, + 15.650536434821788 + ], + [ + 99.22561556168148, + 15.650536434821788 + ], + [ + 101.82825465838141, + 15.650536434821788 + ], + [ + 103.29440801618921, + 15.650536434821788 + ], + [ + 104.43523148690929, + 15.650536434821788 + ], + [ + 105.64112093504673, + 15.650536434821788 + ], + [ + 107.0508837790926, + 15.650536434821788 + ], + [ + 108.11362807691148, + 15.650536434821788 + ], + [ + 109.19806103386986, + 15.650536434821788 + ], + [ + 110.52106924135887, + 15.650536434821788 + ], + [ + 111.28884777488543, + 15.650536434821788 + ], + [ + 112.63788237334165, + 15.650536434821788 + ], + [ + 113.70062667116053, + 15.650536434821788 + ], + [ + 114.76337096897987, + 15.650536434821788 + ], + [ + 115.83912846228236, + 15.650536434821788 + ], + [ + 116.88452183279014, + 15.650536434821788 + ], + [ + 117.59590985255477, + 15.650536434821788 + ], + [ + 118.80613703252016, + 15.650536434821788 + ], + [ + 119.15315557874692, + 15.650536434821788 + ], + [ + 119.83851720754456, + 15.650536434821788 + ], + [ + 120.67569945031619, + 15.650536434821788 + ], + [ + 121.82519838469216, + 15.650536434821788 + ], + [ + 122.4715204270392, + 15.542093139125939 + ], + [ + 123.11784246938623, + 15.212425520210616 + ], + [ + 123.6296948250706, + 14.900108828606676 + ], + [ + 124.06780573968172, + 14.583454405174848 + ], + [ + 124.56230716805476, + 14.392594204750196 + ], + [ + 125.1826028194348, + 13.924119167344202 + ], + [ + 125.49491951103892, + 13.607464743912374 + ], + [ + 125.78120981167581, + 13.316836711447593 + ], + [ + 126.42319412219513, + 12.917765383286962 + ], + [ + 126.80925225487226, + 12.327833854701623 + ], + [ + 127.39484605162966, + 11.941775722024488 + ], + [ + 127.76789098882318, + 11.33883099795571 + ], + [ + 128.36649798106419, + 10.96578606076207 + ], + [ + 128.73086745460228, + 10.336814945726246 + ], + [ + 129.03017095072278, + 10.002809594983091 + ], + [ + 129.75023443414307, + 9.508308166610163 + ], + [ + 129.91506824360067, + 9.182978279522672 + ], + [ + 130.35751689003973, + 8.653774996527034 + ], + [ + 130.64814492250434, + 8.354471500406532 + ], + [ + 130.7912900728229, + 8.206988618260198 + ], + [ + 130.84334285475688, + 7.612719357847084 + ], + [ + 130.84334285475688, + 7.465236475700749 + ], + [ + 130.96046161410845, + 7.157257515924584 + ], + [ + 131.103606764427, + 7.0097746337783065 + ], + [ + 131.12529542356606, + 6.849278556148477 + ], + [ + 131.12529542356606, + 6.710471137657805 + ], + [ + 131.12529542356606, + 6.571663719167134 + ], + [ + 131.12529542356606, + 6.3157375413250065 + ], + [ + 131.12529542356606, + 6.181267854662167 + ], + [ + 131.12529542356606, + 6.038122704343664 + ], + [ + 131.12529542356606, + 5.903653017680824 + ], + [ + 131.24241418291763, + 5.777858794673705 + ], + [ + 131.37254613775258, + 5.643389108010865 + ], + [ + 131.5113535562432, + 5.63037591252737 + ], + [ + 131.64148551107837, + 5.63037591252737 + ], + [ + 131.77595519774104, + 5.63037591252737 + ], + [ + 131.90608715257622, + 5.491568494036699 + ], + [ + 131.99717952096057, + 5.352761075546027 + ], + [ + 132.1403246712789, + 5.352761075546027 + ], + [ + 132.1923774532131, + 5.222629120711076 + ], + [ + 132.1923774532131, + 5.088159434048237 + ], + [ + 132.45697909471073, + 5.083821702220405 + ], + [ + 132.45697909471073, + 5.083821702220405 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 1886, + "versionNonce": 2095284070, + "isDeleted": false, + "id": "3GbNzb7gNEEkOefFLH_zm", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1031.3741248021522, + "y": 543.7077085193864, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 575.6795654296875, + "height": 75, + "seed": 1265267617, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "-Oss2ZzO5I4ednR6BlyJo", + "type": "arrow" + } + ], + "updated": 1698914940985, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1. Set new next update ts in reward info\n2. Add rewards from new schedule: \naccrued_reward += (block_ts - prev_end_ts) * rps_new ", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1. Set new next update ts in reward info\n2. Add rewards from new schedule: \naccrued_reward += (block_ts - prev_end_ts) * rps_new ", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "arrow", + "version": 514, + "versionNonce": 144641679, + "isDeleted": false, + "id": "-Oss2ZzO5I4ednR6BlyJo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1308.2431286220844, + "y": 533.3335054634075, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 90.97427704211623, + "height": 134.8860747965935, + "seed": 1194553967, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3GbNzb7gNEEkOefFLH_zm", + "focus": -0.13813190004139983, + "gap": 10.37420305597891 + }, + "endBinding": { + "elementId": "C8ZZEQTg43LdsMr_7tR8L", + "focus": -0.5334175492921269, + "gap": 4.637619418441032 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 90.97427704211623, + -134.8860747965935 + ] + ] + }, + { + "type": "arrow", + "version": 345, + "versionNonce": 301753519, + "isDeleted": false, + "id": "Dz1mBBvcz_XC4USj-NU9j", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 742.2102053701782, + "y": 476.853423863508, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 293.8872240399472, + "height": 159.22078447244104, + "seed": 294406959, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6zs8j0qjY600aoutD6DMp", + "focus": -0.04324510302774323, + "gap": 12.581149425461888 + }, + "endBinding": { + "elementId": "Xh8N7MmOvkr5t_7jDfhJF", + "focus": 1.2246203471547532, + "gap": 11.575963692024931 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 63.033136162670644, + -157.06059402218017 + ], + [ + 258.93378097126833, + -159.22078447244104 + ], + [ + 293.8872240399472, + -111.96553394002746 + ] + ] + }, + { + "type": "arrow", + "version": 269, + "versionNonce": 1300887247, + "isDeleted": false, + "id": "M8b2o2IgNnBY3zqJaSwKT", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 612.4224863220122, + "y": 354.5163731231314, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 89.23581916218222, + "height": 0.5075146238564798, + "seed": 2062012047, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": { + "elementId": "D-ik0u__dglt3H23Yv_nH", + "focus": 1.7309676277930715, + "gap": 9.684503695353385 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 89.23581916218222, + -0.5075146238564798 + ] + ] + }, + { + "type": "text", + "version": 196, + "versionNonce": 1419973114, + "isDeleted": false, + "id": "D-ik0u__dglt3H23Yv_nH", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 608.2278996444977, + "y": 319.83186942777803, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 73.55992126464844, + "height": 25, + "seed": 1324639375, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "M8b2o2IgNnBY3zqJaSwKT", + "type": "arrow" + } + ], + "updated": 1698914940986, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps_old", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps_old", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 770, + "versionNonce": 1055262081, + "isDeleted": false, + "id": "VNKeTB-I2kvG0tPHlYOMj", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 612.9986083709992, + "y": 63.24036084056465, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 83.42188593108631, + "height": 0.15781612120517252, + "seed": 997522305, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3b1r4-1d-0HJcUQpVjWyv", + "focus": 2.111232857044099, + "gap": 13.916472183915317 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 83.42188593108631, + 0.15781612120517252 + ] + ] + }, + { + "type": "text", + "version": 374, + "versionNonce": 139109030, + "isDeleted": false, + "id": "3b1r4-1d-0HJcUQpVjWyv", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 615.513120249343, + "y": 24.32388865664933, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 29.319961547851562, + "height": 25, + "seed": 355197793, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "VNKeTB-I2kvG0tPHlYOMj", + "type": "arrow" + } + ], + "updated": 1698914940986, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 808, + "versionNonce": 1508617569, + "isDeleted": false, + "id": "Y8ZgF8FNvL0zv-Y9UDLRX", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1056.6634458807453, + "y": 348.030609902888, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 90.48208718782007, + "height": 0.37389907482673834, + "seed": 951703137, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 90.48208718782007, + 0.37389907482673834 + ] + ] + }, + { + "type": "text", + "version": 962, + "versionNonce": 1898103482, + "isDeleted": false, + "id": "z2qLjfU2V11uExXpYZEFF", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 885.6148795699141, + "y": 693.2769366659383, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 60.07994079589844, + "height": 25, + "seed": 1205281871, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940986, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps_2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps_2", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "line", + "version": 761, + "versionNonce": 2041665857, + "isDeleted": false, + "id": "OsMQBkEKsvI6558BUhU92", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 612.5623120276815, + "y": 775.9977248561099, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 793.9810566416572, + "height": 2.922204495046742, + "seed": 1697366639, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 793.9810566416572, + -2.922204495046742 + ] + ] + }, + { + "type": "ellipse", + "version": 1557, + "versionNonce": 933009231, + "isDeleted": false, + "id": "yr3UGm0bUcpbyUqsKCc-V", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 870.0317718810973, + "y": 766.0865941215844, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1183292559, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 566, + "versionNonce": 963337702, + "isDeleted": false, + "id": "8SSZAb_wSmmUyqoVwZ32H", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 532.0924951243027, + "y": 733.2348743390631, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 34.37998962402344, + "height": 45, + "seed": 1604530863, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940986, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "3.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "3.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "diamond", + "version": 1009, + "versionNonce": 1103811951, + "isDeleted": false, + "id": "GQCr8dDsI3Tk4ns8uoZKw", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 755.6196254097904, + "y": 762.8990514085351, + "strokeColor": "#1971c2", + "backgroundColor": "#15aabf", + "width": 27.8612515301719, + "height": 22.0703795400147, + "seed": 421987535, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 1124, + "versionNonce": 889896193, + "isDeleted": false, + "id": "-g7sCN1IsFJmp_R7aiDMN", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1214.9516730731195, + "y": 763.5473581921082, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 21.623593161747976, + "height": 18.85612025559044, + "seed": 850105071, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 2294, + "versionNonce": 2054526266, + "isDeleted": false, + "id": "WPMQvt-_FlUJV-gSJid2d", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 582.7580782521749, + "y": 885.4273991214868, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 284.6397705078125, + "height": 50, + "seed": 1689869583, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "22GOzLcGtOpmZCr4MM6zq", + "type": "arrow" + } + ], + "updated": 1698914949003, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "accrued_reward =\n (end_ts - last_ts) * rps_1", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "accrued_reward =\n (end_ts - last_ts) * rps_1", + "lineHeight": 1.25, + "baseline": 43 + }, + { + "type": "ellipse", + "version": 1779, + "versionNonce": 697136353, + "isDeleted": false, + "id": "N3HOk6O3NBd2CSTe6vLil", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1078.5763641565984, + "y": 763.8594742628519, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 1120461615, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 2179, + "versionNonce": 1431482662, + "isDeleted": false, + "id": "6RmBCFJP86KO1SNRC5bd3", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1132.6344521152896, + "y": 896.4527051416768, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 556.839599609375, + "height": 75, + "seed": 96918927, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "Z7g4utHA4YL9MOFjxHTMP", + "type": "arrow" + } + ], + "updated": 1698914940986, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1. Set new next update ts in reward info\n2. Add rewards from new schedule: \naccrued_reward += (block_ts - prev_end_ts) * rps_3 ", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1. Set new next update ts in reward info\n2. Add rewards from new schedule: \naccrued_reward += (block_ts - prev_end_ts) * rps_3 ", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "arrow", + "version": 694, + "versionNonce": 1834464385, + "isDeleted": false, + "id": "NPHb5YSSrqRBLDOyBAM0P", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 621.195502100637, + "y": 740.0851436395249, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 86.08024292774451, + "height": 0.592517251328843, + "seed": 264664047, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 86.08024292774451, + 0.592517251328843 + ] + ] + }, + { + "type": "text", + "version": 420, + "versionNonce": 1665719354, + "isDeleted": false, + "id": "o8GvNbk9IBjxgJXJF1EtL", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 641.2686479856595, + "y": 695.8385867019529, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 51.25994873046875, + "height": 25, + "seed": 2110791183, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps_1", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps_1", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1153, + "versionNonce": 1280277601, + "isDeleted": false, + "id": "83K-ZgD35i3MBQ0JMD15q", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 882.3385530488504, + "y": 735.4041340402201, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 90.48208718782007, + "height": 0.37389907482673834, + "seed": 1834976303, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 90.48208718782007, + 0.37389907482673834 + ] + ] + }, + { + "type": "text", + "version": 1061, + "versionNonce": 2003672166, + "isDeleted": false, + "id": "fNTqL5Ndh6OFvAex3AOut", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1093.9814728227273, + "y": 691.7265782854051, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 59.45994567871094, + "height": 25, + "seed": 17238159, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps_3", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps_3", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 1250, + "versionNonce": 750995521, + "isDeleted": false, + "id": "WqfX7cWPZFvSgFX3h4OpJ", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1090.7051463016635, + "y": 733.853775659687, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 90.48208718782007, + "height": 0.37389907482673834, + "seed": 1867849391, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 90.48208718782007, + 0.37389907482673834 + ] + ] + }, + { + "type": "ellipse", + "version": 1898, + "versionNonce": 1780815951, + "isDeleted": false, + "id": "yKBTSQY2fM7VTNTylkdVg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1400.3936196755144, + "y": 764.2657757327837, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 653080289, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false + }, + { + "type": "freedraw", + "version": 162, + "versionNonce": 1905997423, + "isDeleted": false, + "id": "lVCPCRcirRr1x6vw5hHcX", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 770.3749455483455, + "y": 792.7859510344881, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 102.86127243220994, + "height": 28.63489263615429, + "seed": 1731771855, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.29074601109721243 + ], + [ + 5.551115123125783e-17, + 0.5061134267248235 + ], + [ + 0, + 1.6278187164519977 + ], + [ + 0, + 2.2129001955736385 + ], + [ + 0, + 2.823107873185222 + ], + [ + -2.220446049250313e-16, + 3.5356150732199016 + ], + [ + -2.220446049250313e-16, + 3.9591709906208052 + ], + [ + 1.7763568394002505e-15, + 5.432643059206384 + ], + [ + 4.440892098500626e-16, + 6.114639875360581 + ], + [ + -4.440892098500626e-16, + 7.089177431075427 + ], + [ + 0, + 7.900394696606099 + ], + [ + 0, + 8.864163881539639 + ], + [ + 0, + 9.294898712794861 + ], + [ + 0, + 9.725633544050083 + ], + [ + 0, + 10.036121568246585 + ], + [ + 0, + 10.156368375305306 + ], + [ + 0.15973083325707194, + 10.29456246699965 + ], + [ + 0.2189568725547133, + 10.29456246699965 + ], + [ + 0.46842412899002284, + 10.29456246699965 + ], + [ + 1.20426279905098, + 10.29456246699965 + ], + [ + 1.5165455517110331, + 10.29456246699965 + ], + [ + 3.3776789684263804, + 10.29456246699965 + ], + [ + 5.443411429987805, + 10.29456246699965 + ], + [ + 7.523501719257865, + 10.29456246699965 + ], + [ + 7.956031278976525, + 10.35019904937019 + ], + [ + 9.253619958132958, + 10.461472214111154 + ], + [ + 9.919464218114967, + 10.461472214111154 + ], + [ + 10.26764154004627, + 10.461472214111154 + ], + [ + 11.186542513390691, + 10.461472214111154 + ], + [ + 11.513183093759267, + 10.461472214111154 + ], + [ + 12.281326876164485, + 10.461472214111154 + ], + [ + 12.593609628824424, + 10.461472214111154 + ], + [ + 13.553789356830862, + 10.461472214111154 + ], + [ + 14.456537774003209, + 10.461472214111154 + ], + [ + 15.138534590157406, + 10.461472214111154 + ], + [ + 16.99966800687264, + 10.461472214111154 + ], + [ + 18.406735122306372, + 10.461472214111154 + ], + [ + 19.937638501725928, + 10.362762148615047 + ], + [ + 22.3084748020932, + 10.251488983874196 + ], + [ + 24.715205671731724, + 10.251488983874196 + ], + [ + 26.65889659777099, + 10.251488983874196 + ], + [ + 28.11262665325728, + 10.251488983874196 + ], + [ + 29.49456757020107, + 10.251488983874196 + ], + [ + 30.901634685634917, + 10.251488983874196 + ], + [ + 32.28537033104226, + 10.251488983874196 + ], + [ + 33.45553328928554, + 10.251488983874196 + ], + [ + 34.81055327927595, + 10.251488983874196 + ], + [ + 35.255645938239695, + 10.251488983874196 + ], + [ + 35.89097981434111, + 10.335841221661667 + ], + [ + 37.418293736833675, + 10.691197457447288 + ], + [ + 38.14695349304043, + 11.01783803781575 + ], + [ + 38.671014204400876, + 11.387552101309893 + ], + [ + 39.21302220039706, + 11.818286932565115 + ], + [ + 39.49120511224942, + 12.153901155251333 + ], + [ + 39.772977481028875, + 12.523615218745476 + ], + [ + 40.061928763662536, + 12.880766182994648 + ], + [ + 40.3167802054885, + 13.426363635917824 + ], + [ + 40.50881615108983, + 13.849919553318841 + ], + [ + 40.85519874455758, + 14.783178354371785 + ], + [ + 41.08851344482082, + 15.727205526206149 + ], + [ + 41.16927622568119, + 16.055640835038275 + ], + [ + 41.404385654407974, + 17.008641649190395 + ], + [ + 41.56411648766516, + 17.651154439146126 + ], + [ + 41.72564204938578, + 18.304435599883163 + ], + [ + 41.88357815417942, + 18.64363927949671 + ], + [ + 42.16355579449532, + 19.43870398885531 + ], + [ + 42.359181197023645, + 19.9125123032361 + ], + [ + 42.6714639496837, + 20.538872537019643 + ], + [ + 42.906573378410485, + 21.1652327708033 + ], + [ + 43.23680341570616, + 21.983628950188177 + ], + [ + 43.520370512949285, + 23.1753286499943 + ], + [ + 43.59754383688244, + 23.487611402654352 + ], + [ + 43.89726349029752, + 24.130124192610083 + ], + [ + 44.1628833029049, + 25.215934913065894 + ], + [ + 44.32261413616209, + 25.883573901511568 + ], + [ + 44.512855353299756, + 26.574544359983406 + ], + [ + 44.808985549787735, + 27.5060084325728 + ], + [ + 44.83770120520478, + 27.942127449218788 + ], + [ + 44.88077468833035, + 28.109037196330178 + ], + [ + 44.96692165458137, + 28.32619934042134 + ], + [ + 45.07460536239512, + 28.509261643704804 + ], + [ + 45.191262712526736, + 28.62591899383642 + ], + [ + 45.25407820875148, + 28.63489263615429 + ], + [ + 45.46406143898844, + 28.63489263615429 + ], + [ + 45.523287478285965, + 28.63489263615429 + ], + [ + 45.70814451003298, + 28.555924583757474 + ], + [ + 45.88043844253514, + 28.286715314222874 + ], + [ + 46.21425793675792, + 27.949306363072992 + ], + [ + 46.61986656952331, + 27.495240061791492 + ], + [ + 46.76344484660831, + 27.27807791770033 + ], + [ + 48.51330509858258, + 25.892547543829323 + ], + [ + 49.319138178722596, + 25.3523342762968 + ], + [ + 50.11958707347185, + 24.83186302186334 + ], + [ + 50.553911361654286, + 24.56803793771951 + ], + [ + 51.85508949773771, + 23.622216037421595 + ], + [ + 52.16737225039776, + 23.464279932628074 + ], + [ + 53.5062396842161, + 22.42872160915192 + ], + [ + 54.540003279228586, + 21.597762330522073 + ], + [ + 55.76759754830596, + 20.612456404025806 + ], + [ + 56.35088429896405, + 20.12967444732726 + ], + [ + 57.68795700431883, + 19.06719519689773 + ], + [ + 59.44320144168387, + 17.87011131170084 + ], + [ + 60.001361993852015, + 17.588338942921382 + ], + [ + 60.81437398784624, + 17.18093558169255 + ], + [ + 62.43501379044403, + 16.44330218316793 + ], + [ + 64.91891798401582, + 15.766689552404614 + ], + [ + 66.62749948132819, + 15.337749449612943 + ], + [ + 67.45845875995803, + 15.129560947839536 + ], + [ + 69.1024300325821, + 14.985982670754538 + ], + [ + 70.51488133340649, + 14.867530592159255 + ], + [ + 71.7945227279273, + 14.867530592159255 + ], + [ + 72.61112417884863, + 14.867530592159255 + ], + [ + 74.01639656581881, + 14.77599944051758 + ], + [ + 75.10938620012894, + 14.684468288875792 + ], + [ + 76.99744054379767, + 14.684468288875792 + ], + [ + 78.21247171363007, + 14.684468288875792 + ], + [ + 79.32879281796647, + 14.684468288875792 + ], + [ + 80.69637590720185, + 14.684468288875792 + ], + [ + 81.50759317273241, + 14.684468288875792 + ], + [ + 84.08302851794599, + 14.684468288875792 + ], + [ + 84.96962437894626, + 14.684468288875792 + ], + [ + 87.09458287980533, + 14.684468288875792 + ], + [ + 88.3490980758362, + 14.684468288875792 + ], + [ + 89.60361327186706, + 14.684468288875792 + ], + [ + 91.12195355204176, + 14.684468288875792 + ], + [ + 91.737545415044, + 14.684468288875792 + ], + [ + 93.60944720254065, + 14.485253429420254 + ], + [ + 94.49783779200447, + 14.298601669209688 + ], + [ + 95.95695203288153, + 13.955808532669039 + ], + [ + 96.49716530041405, + 13.797872427875518 + ], + [ + 97.143267547297, + 13.605836482274185 + ], + [ + 97.45913975688404, + 13.499947502923987 + ], + [ + 98.31881469093105, + 13.173306922555412 + ], + [ + 98.72801278062343, + 13.026139188543198 + ], + [ + 99.0205535201842, + 12.889739825312404 + ], + [ + 99.29155751818234, + 12.634888383486441 + ], + [ + 99.56076678771683, + 12.48413119254701 + ], + [ + 99.63973484011365, + 12.354910743170535 + ], + [ + 99.90355992425748, + 12.015707063556988 + ], + [ + 100.65914060741761, + 11.11116391792109 + ], + [ + 100.89425003614451, + 10.874259760730638 + ], + [ + 101.34652160896246, + 10.368146334005814 + ], + [ + 101.49009988604757, + 10.150984189914652 + ], + [ + 101.77007752636337, + 9.70948098787801 + ], + [ + 101.90109270420362, + 9.576671081574318 + ], + [ + 102.10210229212271, + 9.250030501205856 + ], + [ + 102.18824925837373, + 9.16208880649117 + ], + [ + 102.25644893998913, + 9.092094396412222 + ], + [ + 102.34798009163092, + 9.057994555604523 + ], + [ + 102.55975805033131, + 8.90185317927444 + ], + [ + 102.61898408962895, + 8.840832411513361 + ], + [ + 102.69077322817145, + 8.813911484559867 + ], + [ + 102.74640981054199, + 8.813911484559867 + ], + [ + 102.86127243220994, + 8.813911484559867 + ], + [ + 102.86127243220994, + 8.813911484559867 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 262, + "versionNonce": 269599713, + "isDeleted": false, + "id": "cryProIzMmAIevMYReDdh", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 888.5255097616521, + "y": 791.9890915966655, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 190.14789125761672, + "height": 48.04308624112912, + "seed": 1314434063, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258954, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0017947284636647964, + 0.21177795870050886 + ], + [ + 0.001794728463664741, + 0.5796972937309874 + ], + [ + 0.0017947284636647964, + 0.8722380332918647 + ], + [ + 0.0017947284636649075, + 1.2904097653021154 + ], + [ + 0.0017947284636649075, + 1.5829505048629926 + ], + [ + 0.0017947284636647964, + 2.273920963334831 + ], + [ + 0.0017947284636647964, + 2.417499240419943 + ], + [ + 0.0017947284636654626, + 3.1443642681631454 + ], + [ + 0.0017947284636647964, + 3.3615264122543067 + ], + [ + 0.0017947284636647964, + 3.7438035749933087 + ], + [ + 0.0017947284636647964, + 4.011218116064242 + ], + [ + 0.0017947284636647964, + 4.345037610287022 + ], + [ + 0.0017947284636647964, + 4.720135859171819 + ], + [ + 0.0017947284636647964, + 5.070107909566673 + ], + [ + 0.0017947284636647964, + 5.513405840066866 + ], + [ + 0.10050479395954515, + 5.931577572077117 + ], + [ + 0.2028043163827542, + 6.209760483929472 + ], + [ + 0.40560863276539294, + 6.5561430773971106 + ], + [ + 0.6514864322735985, + 7.035335577168553 + ], + [ + 0.7860910670408416, + 7.333260502120083 + ], + [ + 1.0319688665490503, + 7.6024697716546825 + ], + [ + 1.3209201491827116, + 8.017052046737831 + ], + [ + 1.475266797049244, + 8.309592786298595 + ], + [ + 1.73550242426586, + 8.580596784296745 + ], + [ + 2.2846893341162513, + 9.165678263418386 + ], + [ + 2.4372412535192325, + 9.390019321363752 + ], + [ + 3.0743698580841965, + 9.917669489651416 + ], + [ + 3.6881669926228824, + 10.475830041819677 + ], + [ + 4.583736495941025, + 11.139879573338135 + ], + [ + 5.463153443087094, + 11.665735013162248 + ], + [ + 5.886709360488112, + 11.91879172652466 + ], + [ + 7.6509274401709035, + 12.774877203644337 + ], + [ + 9.187215004981226, + 13.39405852357379 + ], + [ + 10.904770144611348, + 14.036571313529521 + ], + [ + 13.033318102397516, + 14.677289375021587 + ], + [ + 13.880429937199551, + 14.867530592159369 + ], + [ + 16.15255617207083, + 15.506453925187884 + ], + [ + 17.730122491543057, + 15.96231495493305 + ], + [ + 19.112063408486847, + 16.197424383659836 + ], + [ + 20.51733579545703, + 16.396639243115374 + ], + [ + 22.949192863585495, + 16.71251145270253 + ], + [ + 23.631189679739578, + 16.80942678973497 + ], + [ + 25.32361862087987, + 16.827374074370596 + ], + [ + 27.364224883951465, + 16.827374074370596 + ], + [ + 28.100063554012422, + 16.827374074370596 + ], + [ + 29.279200154573573, + 16.827374074370596 + ], + [ + 32.28357560257882, + 16.827374074370596 + ], + [ + 33.8701155643688, + 16.827374074370596 + ], + [ + 35.370508559907876, + 16.827374074370596 + ], + [ + 37.082679514147344, + 16.827374074370596 + ], + [ + 38.931249831617606, + 16.827374074370596 + ], + [ + 40.81033053296858, + 16.913521040621617 + ], + [ + 42.69479541971009, + 17.134272641639882 + ], + [ + 44.48055024095572, + 17.331692772631868 + ], + [ + 48.42536340386812, + 17.791143259304135 + ], + [ + 50.209323496650086, + 18.171625693579585 + ], + [ + 52.16198806500711, + 18.433656049259866 + ], + [ + 53.92441141622635, + 18.82131739738952 + ], + [ + 55.923738924636154, + 19.07796356767915 + ], + [ + 57.6592413489019, + 19.289741526379657 + ], + [ + 59.649595214993724, + 19.715092172244113 + ], + [ + 61.367150354623845, + 19.944817415580246 + ], + [ + 62.815496224719595, + 20.147621731962886 + ], + [ + 64.84533411700977, + 20.66450352946913 + ], + [ + 65.54348348933604, + 20.861923660461116 + ], + [ + 67.65587889095013, + 21.26753229322651 + ], + [ + 68.33787570710422, + 21.46136296729128 + ], + [ + 69.02525670864907, + 21.621093800548465 + ], + [ + 71.85733822415204, + 22.5776840716278 + ], + [ + 73.11005869171936, + 23.07841331296197 + ], + [ + 74.50276797944457, + 23.561195269660516 + ], + [ + 75.92957710797748, + 24.1714029472721 + ], + [ + 77.65969534685257, + 24.91083107426016 + ], + [ + 78.67012747183878, + 25.343360633979046 + ], + [ + 79.84746934393638, + 25.883573901511568 + ], + [ + 81.67270819138037, + 26.888621841107124 + ], + [ + 82.5987880785791, + 27.37319852626922 + ], + [ + 83.63973058744591, + 27.893669780702567 + ], + [ + 84.54786319000891, + 28.42131994899023 + ], + [ + 85.41471703791012, + 28.939996474960026 + ], + [ + 86.29054452812898, + 29.464057186320588 + ], + [ + 87.22918751457269, + 30.128106717839046 + ], + [ + 87.65453816043726, + 30.382958159665122 + ], + [ + 88.19654615643344, + 30.844203374800827 + ], + [ + 89.86025944215669, + 32.41459078041885 + ], + [ + 90.11331615551921, + 32.838146697819866 + ], + [ + 90.96940163263889, + 33.977799272182665 + ], + [ + 91.71959813040837, + 35.34538236141793 + ], + [ + 92.28493759643084, + 36.47606129346286 + ], + [ + 92.67080421609694, + 37.38778335295308 + ], + [ + 92.93103984331367, + 38.25822665778128 + ], + [ + 93.39587451537659, + 39.652730673970154 + ], + [ + 93.57534736173295, + 40.46933212489148 + ], + [ + 93.75123075116221, + 41.44028022367934 + ], + [ + 93.97916126603468, + 42.73786890283566 + ], + [ + 94.15504465546394, + 43.65676987618008 + ], + [ + 94.31477548872112, + 44.33338250694351 + ], + [ + 94.3381069587474, + 45.16793124250046 + ], + [ + 94.3381069587474, + 45.83377550248247 + ], + [ + 94.3381069587474, + 46.2986101745455 + ], + [ + 94.3381069587474, + 46.86394964056785 + ], + [ + 94.3381069587474, + 47.08649597004978 + ], + [ + 94.3381069587474, + 47.48851514588796 + ], + [ + 94.3381069587474, + 47.60696722448324 + ], + [ + 94.3381069587474, + 47.72721403154196 + ], + [ + 94.3381069587474, + 47.84746083860068 + ], + [ + 94.3381069587474, + 47.96950237412295 + ], + [ + 94.3381069587474, + 48.028728413420595 + ], + [ + 94.39015408419073, + 48.04308624112912 + ], + [ + 94.44579066656115, + 48.04308624112912 + ], + [ + 94.50322197739524, + 48.04308624112912 + ], + [ + 94.61808459906331, + 48.04308624112912 + ], + [ + 94.73653667765848, + 48.04308624112912 + ], + [ + 94.88908859706135, + 48.04308624112912 + ], + [ + 95.00933540412007, + 48.04308624112912 + ], + [ + 95.13855585349665, + 47.82053991164719 + ], + [ + 95.28213413058177, + 47.40416290810049 + ], + [ + 95.30187614368094, + 47.10803271161251 + ], + [ + 95.43289132152108, + 46.67729788035729 + ], + [ + 95.4777595331102, + 46.12272678511624 + ], + [ + 95.5495486716527, + 45.887617356389455 + ], + [ + 95.58544324092395, + 45.55738731909378 + ], + [ + 95.72902151800906, + 44.72463331200038 + ], + [ + 95.77388972959807, + 44.143141289805726 + ], + [ + 95.98566768829869, + 43.559854539147636 + ], + [ + 96.33205028176644, + 42.68582177739222 + ], + [ + 96.74663255684959, + 41.80999428717337 + ], + [ + 97.37299279063313, + 40.81391998989568 + ], + [ + 98.30445686322264, + 39.45889999990527 + ], + [ + 99.4943618345651, + 37.93517553433992 + ], + [ + 101.35728997974388, + 35.9520005821023 + ], + [ + 102.67103121507239, + 34.753121968441974 + ], + [ + 105.36132918195392, + 32.567142699821716 + ], + [ + 108.74080287884385, + 30.011449367707428 + ], + [ + 109.79071903002841, + 29.30971053845417 + ], + [ + 111.93900900091376, + 27.87392776760339 + ], + [ + 113.19890838233528, + 27.351661784706494 + ], + [ + 114.94517917738256, + 26.296361448131165 + ], + [ + 117.69470318356173, + 25.01851478207402 + ], + [ + 119.2399643906898, + 24.532143368448374 + ], + [ + 121.36851234847609, + 23.99551955784284 + ], + [ + 122.2461345671586, + 23.774767956824576 + ], + [ + 124.91310106401374, + 23.397874979476228 + ], + [ + 126.83884470541739, + 23.17353392153086 + ], + [ + 128.6586993674706, + 23.03354510137285 + ], + [ + 130.5664957242385, + 23.03354510137285 + ], + [ + 132.37019783011976, + 23.03354510137285 + ], + [ + 134.31029929923181, + 23.03354510137285 + ], + [ + 135.9381180156838, + 23.03354510137285 + ], + [ + 138.6032897840755, + 23.03354510137285 + ], + [ + 140.20598230203768, + 22.922271936631887 + ], + [ + 142.1927467112023, + 22.810998771891036 + ], + [ + 144.0664432271626, + 22.810998771891036 + ], + [ + 146.51804230839002, + 22.810998771891036 + ], + [ + 148.40789138052241, + 22.810998771891036 + ], + [ + 149.39678676394578, + 22.810998771891036 + ], + [ + 150.72309109851926, + 22.810998771891036 + ], + [ + 152.74036589156458, + 22.810998771891036 + ], + [ + 154.17614866241524, + 22.810998771891036 + ], + [ + 155.18478605893802, + 22.810998771891036 + ], + [ + 155.9906191390778, + 22.810998771891036 + ], + [ + 157.29718146055188, + 22.810998771891036 + ], + [ + 159.19061998961138, + 22.810998771891036 + ], + [ + 160.14182607530006, + 22.810998771891036 + ], + [ + 161.0858532471343, + 22.868430082725013 + ], + [ + 161.78400261946058, + 23.026366187518647 + ], + [ + 162.60239879884534, + 23.202249576947906 + ], + [ + 163.15517516562295, + 23.356596224814325 + ], + [ + 163.7025673470098, + 23.537863799634238 + ], + [ + 164.0274131989147, + 23.645547507447986 + ], + [ + 164.45814803017015, + 23.7173366459906 + ], + [ + 165.0198980392654, + 23.7173366459906 + ], + [ + 165.4596065128385, + 23.7173366459906 + ], + [ + 165.89931498641158, + 23.7173366459906 + ], + [ + 166.51311212095015, + 23.7173366459906 + ], + [ + 167.15024072551523, + 23.7173366459906 + ], + [ + 167.79993242932505, + 23.7173366459906 + ], + [ + 169.05983181074657, + 23.7173366459906 + ], + [ + 169.4044196757509, + 23.7173366459906 + ], + [ + 169.9859116979453, + 23.7173366459906 + ], + [ + 171.29067929095595, + 23.665289520547276 + ], + [ + 172.02113377562614, + 23.453511561846767 + ], + [ + 172.23650119125386, + 23.308138556298104 + ], + [ + 172.5308366592783, + 23.182507563848617 + ], + [ + 173.27923842858422, + 22.963550691293904 + ], + [ + 173.48742693035751, + 22.91329829431413 + ], + [ + 173.60767373741623, + 22.841509155771632 + ], + [ + 173.7602256568191, + 22.841509155771632 + ], + [ + 173.84996207999734, + 22.841509155771632 + ], + [ + 174.04020329713512, + 22.841509155771632 + ], + [ + 174.37940697674867, + 22.841509155771632 + ], + [ + 174.6468215178195, + 22.841509155771632 + ], + [ + 175.40599165790695, + 22.841509155771632 + ], + [ + 176.07901483174305, + 22.841509155771632 + ], + [ + 176.909974110373, + 22.690751964832316 + ], + [ + 178.01014265853746, + 22.502305476158085 + ], + [ + 178.7011131170093, + 22.48615291998601 + ], + [ + 179.79410275131954, + 22.2707855043584 + ], + [ + 180.2230428541111, + 22.18463853810738 + ], + [ + 181.39858999774515, + 21.97286057940687 + ], + [ + 181.89752451061577, + 21.922608182427098 + ], + [ + 182.45389033432048, + 21.813129746149798 + ], + [ + 182.84693586784078, + 21.764672077633577 + ], + [ + 183.11255568044817, + 21.67852511138244 + ], + [ + 183.32253891068513, + 21.67852511138244 + ], + [ + 183.48406447240586, + 21.67852511138244 + ], + [ + 183.7819893973574, + 21.61391488669426 + ], + [ + 184.1140141631165, + 21.54571520507875 + ], + [ + 184.3078448371814, + 21.479310251927018 + ], + [ + 184.77985842309863, + 21.18318005543904 + ], + [ + 185.21777216820806, + 21.003707209082677 + ], + [ + 185.5192865500867, + 20.754239952647367 + ], + [ + 185.93207409670617, + 20.45990448462294 + ], + [ + 186.22102537934006, + 20.18710575816135 + ], + [ + 186.68406532293943, + 19.72047635763488 + ], + [ + 186.98916916174517, + 19.278973155598237 + ], + [ + 187.55989281315829, + 18.611334167152563 + ], + [ + 187.71782891795192, + 18.29546195756552 + ], + [ + 187.93678579050652, + 17.929337350998594 + ], + [ + 188.41059410488742, + 17.150425197811956 + ], + [ + 188.6044247789523, + 16.705332538848324 + ], + [ + 188.84850784999685, + 16.25844515142103 + ], + [ + 188.92029698853935, + 16.041283007329866 + ], + [ + 189.2020693573188, + 15.366465105029988 + ], + [ + 189.43179460065494, + 14.802920367471074 + ], + [ + 189.5879359769849, + 14.110155180535571 + ], + [ + 189.78715083644056, + 13.399442708964443 + ], + [ + 189.91457655735348, + 12.88435563992175 + ], + [ + 190.14789125761672, + 11.592151146156084 + ], + [ + 190.14789125761672, + 10.99091711086237 + ], + [ + 190.14789125761672, + 10.511724611090926 + ], + [ + 190.14789125761672, + 10.068426680590846 + ], + [ + 190.14789125761672, + 9.449245360661394 + ], + [ + 190.14789125761672, + 8.975437046280604 + ], + [ + 190.14789125761672, + 8.415481765648906 + ], + [ + 190.14789125761672, + 8.192935436166977 + ], + [ + 190.14789125761672, + 7.952441822049536 + ], + [ + 190.14789125761672, + 7.620417056290307 + ], + [ + 190.14789125761672, + 7.396075998344827 + ], + [ + 190.14789125761672, + 7.335055230583748 + ], + [ + 190.14789125761672, + 6.963546438626054 + ], + [ + 190.14789125761672, + 6.690747712164466 + ], + [ + 190.14789125761672, + 6.277960165544869 + ], + [ + 190.14789125761672, + 6.164892272340353 + ], + [ + 190.14789125761672, + 5.91004083051439 + ], + [ + 190.14789125761672, + 5.739541626475784 + ], + [ + 190.14789125761672, + 5.475716542332066 + ], + [ + 190.14789125761672, + 5.245991298995932 + ], + [ + 190.14789125761672, + 5.122155035009996 + ], + [ + 190.14789125761672, + 4.881661420892442 + ], + [ + 190.14789125761672, + 4.74885151458875 + ], + [ + 190.14789125761672, + 4.603478509040201 + ], + [ + 190.14789125761672, + 4.479642245054265 + ], + [ + 190.14789125761672, + 4.366574351849749 + ], + [ + 190.14789125761672, + 4.314527226406426 + ], + [ + 190.14789125761672, + 4.2570959155724495 + ], + [ + 190.14789125761672, + 4.2570959155724495 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 205, + "versionNonce": 96839343, + "isDeleted": false, + "id": "JTiYqsv4QgU3OfhWFSlOx", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1096.1253405989587, + "y": 790.319994125551, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 131.75281123865398, + "height": 40.8623776584119, + "seed": 814281327, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.502523969797835 + ], + [ + 0, + 0.6335391476378618 + ], + [ + 0, + 1.3119465068648424 + ], + [ + -1.1102230246251565e-16, + 1.6242292595248955 + ], + [ + -6.661338147750939e-16, + 2.654403397610281 + ], + [ + 0, + 3.422547180015499 + ], + [ + 0, + 5.190354716625393 + ], + [ + 1.3322676295501878e-15, + 5.676726130251041 + ], + [ + 4.440892098500626e-16, + 6.374875502577311 + ], + [ + 0, + 7.620417056290307 + ], + [ + 0, + 8.271903488563794 + ], + [ + 0, + 9.445655903734291 + ], + [ + 0, + 10.098937064471329 + ], + [ + 0, + 11.583177503838328 + ], + [ + 0, + 11.895460256498382 + ], + [ + 0, + 12.869997812213228 + ], + [ + 0, + 13.182280564873281 + ], + [ + 0, + 13.456874019798533 + ], + [ + 0.20639377330962816, + 14.228607259130854 + ], + [ + 0.3733035204211319, + 14.562426753353634 + ], + [ + 0.6281549622472085, + 14.978803756900334 + ], + [ + 0.6927651869355032, + 15.086487464714082 + ], + [ + 0.8812116756096202, + 15.364670376566437 + ], + [ + 1.042737237330357, + 15.477738269770953 + ], + [ + 1.561413763300152, + 15.901294187171857 + ], + [ + 1.9849696807011696, + 16.05923029196549 + ], + [ + 2.1357268716403723, + 16.18845074134208 + ], + [ + 2.598766815239742, + 16.436123269313725 + ], + [ + 2.92361266714488, + 16.543806977127588 + ], + [ + 3.646888237960866, + 16.79686369049 + ], + [ + 3.9717340898657767, + 16.888394842131788 + ], + [ + 4.1871015054935015, + 16.888394842131788 + ], + [ + 4.8511510370119595, + 16.888394842131788 + ], + [ + 5.066518452639457, + 16.888394842131788 + ], + [ + 5.355469735273118, + 16.888394842131788 + ], + [ + 5.779025652674136, + 16.888394842131788 + ], + [ + 6.712284453727079, + 16.888394842131788 + ], + [ + 7.184298039644318, + 16.888394842131788 + ], + [ + 7.92193143816894, + 16.97274707991926 + ], + [ + 8.842627139976912, + 17.05709931770673 + ], + [ + 9.874596006525735, + 17.209651237109597 + ], + [ + 11.173979414145833, + 17.209651237109597 + ], + [ + 11.62086680157313, + 17.209651237109597 + ], + [ + 12.485925921010676, + 17.209651237109597 + ], + [ + 13.796077699411853, + 17.209651237109597 + ], + [ + 14.61985806418761, + 17.209651237109597 + ], + [ + 14.933935545311215, + 17.209651237109597 + ], + [ + 16.1076879604816, + 17.209651237109597 + ], + [ + 16.419970713141538, + 17.209651237109597 + ], + [ + 17.23657216406309, + 17.209651237109597 + ], + [ + 18.54672394246427, + 17.209651237109597 + ], + [ + 20.91397078590444, + 17.478860506644082 + ], + [ + 21.459568238827615, + 17.56859692982232 + ], + [ + 22.58306825701834, + 17.753453961569335 + ], + [ + 24.06910342484889, + 18.220083362095806 + ], + [ + 25.932031570027675, + 18.61851308100688 + ], + [ + 28.071347898595377, + 19.21077347398284 + ], + [ + 28.852054780245453, + 19.463830187345252 + ], + [ + 30.343474133466543, + 19.867644091647094 + ], + [ + 32.04846617385192, + 20.291200009047998 + ], + [ + 33.755252942700736, + 20.79551870730927 + ], + [ + 36.01122662139983, + 21.527767920443125 + ], + [ + 36.52631369044252, + 21.718009137580907 + ], + [ + 38.71229295906278, + 22.592041899336323 + ], + [ + 39.86809808959765, + 23.108923696842567 + ], + [ + 41.09748708713869, + 23.67426316286503 + ], + [ + 42.09356138441626, + 24.12294527875588 + ], + [ + 43.06271475474068, + 24.607521963917975 + ], + [ + 43.88290566258911, + 25.081330278298765 + ], + [ + 45.05306862083239, + 25.795632206796995 + ], + [ + 46.07785857352701, + 26.339434931256733 + ], + [ + 47.43108383505387, + 27.313972486971693 + ], + [ + 47.92463416253395, + 27.708812748955665 + ], + [ + 48.73585142806451, + 28.342351896593414 + ], + [ + 49.48245846890677, + 29.123058778243603 + ], + [ + 49.75346246690492, + 29.508925397909707 + ], + [ + 50.13215017271682, + 29.959402242264105 + ], + [ + 50.49109586542954, + 30.695240912325175 + ], + [ + 50.64005832790531, + 31.17443341209662 + ], + [ + 50.783636604990306, + 31.608757700278943 + ], + [ + 50.86260465738724, + 31.847456585932832 + ], + [ + 50.936188524393174, + 32.601242540629414 + ], + [ + 50.936188524393174, + 33.292212999101366 + ], + [ + 50.97567255059175, + 33.91139431903082 + ], + [ + 51.05284587452479, + 34.23085598554508 + ], + [ + 51.090535172259706, + 35.40999258610623 + ], + [ + 51.090535172259706, + 36.04532646220764 + ], + [ + 51.13899284077593, + 36.68066033830917 + ], + [ + 51.243087091662574, + 37.31778894287413 + ], + [ + 51.300518402496664, + 37.96209646129341 + ], + [ + 51.397433739529106, + 38.595635608931275 + ], + [ + 51.4584545072903, + 39.16635926034439 + ], + [ + 51.607416969765836, + 39.61324664777169 + ], + [ + 51.682795565235665, + 40.03680256517271 + ], + [ + 51.74740578992396, + 40.23422269616469 + ], + [ + 51.87842096776399, + 40.496253051844974 + ], + [ + 52.00225723174981, + 40.68649426898264 + ], + [ + 52.158398608080006, + 40.842635645312726 + ], + [ + 52.278645415138726, + 40.8623776584119 + ], + [ + 52.398892222197446, + 40.8623776584119 + ], + [ + 52.519139029256166, + 40.8623776584119 + ], + [ + 52.77399047108224, + 40.616499858903694 + ], + [ + 53.10780996530502, + 40.026034194391286 + ], + [ + 53.195751660019596, + 39.69580415709561 + ], + [ + 53.391377062548145, + 39.22020111425138 + ], + [ + 53.80416460916763, + 38.02670668598171 + ], + [ + 54.14695774570828, + 37.16523702347126 + ], + [ + 54.5741031200364, + 36.12429451460446 + ], + [ + 55.14662149991295, + 34.905673887844955 + ], + [ + 55.77118700523306, + 33.909599590567154 + ], + [ + 56.64701449545191, + 32.71072097690683 + ], + [ + 57.65924134890179, + 31.578247316398347 + ], + [ + 58.919140730323306, + 30.521152251359467 + ], + [ + 60.53798580445755, + 29.255868684547295 + ], + [ + 61.550212657907196, + 28.602587523810143 + ], + [ + 63.43288281618538, + 27.523955717208537 + ], + [ + 65.078648817273, + 26.845548357981556 + ], + [ + 66.5287894158323, + 26.237135408833637 + ], + [ + 68.17635014538337, + 25.605390989659213 + ], + [ + 70.15593564069377, + 24.932367815823 + ], + [ + 72.2180786453282, + 24.243192085814712 + ], + [ + 74.28560583535318, + 23.34044366864225 + ], + [ + 77.18947648939888, + 22.435900523006353 + ], + [ + 78.2142664420935, + 22.093107386465704 + ], + [ + 81.60450850976486, + 20.80628707809069 + ], + [ + 83.63075694512804, + 20.196079400479107 + ], + [ + 84.62324178547851, + 19.97353307099729 + ], + [ + 87.47327058561723, + 19.311278267942384 + ], + [ + 89.8512857998387, + 18.92182219134918 + ], + [ + 92.42313168812507, + 18.77465445733685 + ], + [ + 95.22111336282046, + 18.77465445733685 + ], + [ + 98.433677312599, + 18.77465445733685 + ], + [ + 100.40428916559176, + 18.77465445733685 + ], + [ + 102.9276773853619, + 18.77465445733685 + ], + [ + 105.93923174722113, + 18.77465445733685 + ], + [ + 108.03188513573605, + 18.77465445733685 + ], + [ + 110.04377574339082, + 18.77465445733685 + ], + [ + 111.11343390767456, + 18.77465445733685 + ], + [ + 112.43435405685727, + 18.943358932911906 + ], + [ + 113.83603698690013, + 18.966690402938184 + ], + [ + 115.25028301618818, + 19.147957977758097 + ], + [ + 116.63401866159575, + 19.381272678021332 + ], + [ + 117.61394040270125, + 19.50869839893437 + ], + [ + 118.45746278057595, + 19.585871722867637 + ], + [ + 119.4212319655096, + 19.66304504680079 + ], + [ + 119.99195561692272, + 19.66304504680079 + ], + [ + 120.64523677765987, + 19.66304504680079 + ], + [ + 120.87855147792311, + 19.66304504680079 + ], + [ + 121.32005467995964, + 19.66304504680079 + ], + [ + 121.65028471725532, + 19.66304504680079 + ], + [ + 122.11511938931835, + 19.562540252841245 + ], + [ + 122.46868089664031, + 19.444088174246076 + ], + [ + 122.99812579339141, + 19.214362930909942 + ], + [ + 123.8129325158493, + 18.803370112753896 + ], + [ + 124.2903302871573, + 18.501855730875263 + ], + [ + 124.895153779378, + 18.31699869912825 + ], + [ + 125.53407711240652, + 18.019073774176718 + ], + [ + 126.00429596986032, + 17.84498511321101 + ], + [ + 126.44938862882395, + 17.55962328750445 + ], + [ + 127.0075491809921, + 17.360408428048913 + ], + [ + 127.23368496740113, + 17.28323510411576 + ], + [ + 127.47238385305513, + 17.18093558169255 + ], + [ + 127.9964445644157, + 16.93864723911156 + ], + [ + 128.43256358106146, + 16.744816565046676 + ], + [ + 128.81484074380046, + 16.50073349400202 + ], + [ + 129.14148132416904, + 16.310492276864352 + ], + [ + 129.5865739831329, + 16.066409205819696 + ], + [ + 129.95449331816326, + 15.870783803291374 + ], + [ + 130.25421297157845, + 15.615932361465298 + ], + [ + 130.6687952466616, + 15.292881238023824 + ], + [ + 131.00082001242072, + 14.957267015337493 + ], + [ + 131.20721378573057, + 14.677289375021587 + ], + [ + 131.45309158523878, + 14.251938729157132 + ], + [ + 131.59666986232378, + 13.842740639464637 + ], + [ + 131.72947976862747, + 13.544815714513106 + ], + [ + 131.75281123865398, + 13.126643982502856 + ], + [ + 131.75281123865398, + 12.66898822429414 + ], + [ + 131.75281123865398, + 11.782392363293866 + ], + [ + 131.75281123865398, + 11.536514563785659 + ], + [ + 131.75281123865398, + 10.843749376850155 + ], + [ + 131.75281123865398, + 10.456088028720501 + ], + [ + 131.75281123865398, + 10.186878759186015 + ], + [ + 131.75281123865398, + 9.781270126420623 + ], + [ + 131.75281123865398, + 9.6951231601696 + ], + [ + 131.75281123865398, + 9.571286896183778 + ], + [ + 131.75281123865398, + 9.445655903734291 + ], + [ + 131.75281123865398, + 9.320024911284804 + ], + [ + 131.75281123865398, + 9.259004143523725 + ], + [ + 131.75281123865398, + 9.154909892636965 + ], + [ + 131.75281123865398, + 9.092094396412335 + ], + [ + 131.7025588416741, + 9.038252542505347 + ], + [ + 131.64692225930366, + 9.038252542505347 + ], + [ + 131.63974334544946, + 8.977231774744268 + ], + [ + 131.63974334544946, + 8.865958610003304 + ], + [ + 131.5876962200059, + 8.751095988335237 + ], + [ + 131.51770180992708, + 8.623670267422199 + ], + [ + 131.51770180992708, + 8.56264949966112 + ], + [ + 131.51770180992708, + 8.503423460363479 + ], + [ + 131.51770180992708, + 8.38676611023186 + ], + [ + 131.51770180992708, + 8.38676611023186 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "arrow", + "version": 125, + "versionNonce": 407035386, + "isDeleted": false, + "id": "22GOzLcGtOpmZCr4MM6zq", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 808.7715322330005, + "y": 834.6423842597476, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 32.30702287288955, + "height": 43.707319290365604, + "seed": 171620879, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698914949004, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "WPMQvt-_FlUJV-gSJid2d", + "focus": 0.17211393830945956, + "gap": 7.077695571373624 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -32.30702287288955, + 43.707319290365604 + ] + ] + }, + { + "type": "arrow", + "version": 158, + "versionNonce": 1640040175, + "isDeleted": false, + "id": "Z7g4utHA4YL9MOFjxHTMP", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1184.5327584551928, + "y": 881.8513076790648, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 30.041501299478114, + "height": 37.28581074118108, + "seed": 1292194529, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "startBinding": { + "elementId": "6RmBCFJP86KO1SNRC5bd3", + "focus": -0.5979348353055244, + "gap": 14.601397462612113 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + -30.041501299478114, + -37.28581074118108 + ] + ] + }, + { + "type": "text", + "version": 220, + "versionNonce": 890586362, + "isDeleted": false, + "id": "hswmP42KbJeqTunxWESGj", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 766.269433907757, + "y": 1033.880237434097, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 361.49969482421875, + "height": 75, + "seed": 1223828719, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "mS5C-1z3i9fv7T-U7k2Sc", + "type": "arrow" + } + ], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Consume whole 2nd period\naccrued_reward += \n(end_ts_2 - prev_end_ts) * rps_2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Consume whole 2nd period\naccrued_reward += \n(end_ts_2 - prev_end_ts) * rps_2", + "lineHeight": 1.25, + "baseline": 68 + }, + { + "type": "arrow", + "version": 133, + "versionNonce": 2030063887, + "isDeleted": false, + "id": "mS5C-1z3i9fv7T-U7k2Sc", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 965.4426537811376, + "y": 1021.3413770461684, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 14.820035716235793, + "height": 168.3735509823532, + "seed": 184830223, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "startBinding": { + "elementId": "hswmP42KbJeqTunxWESGj", + "focus": 0.07616935956865926, + "gap": 12.53886038792848 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 14.820035716235793, + -168.3735509823532 + ] + ] + }, + { + "type": "line", + "version": 771, + "versionNonce": 1387547489, + "isDeleted": false, + "id": "jVKON0M9rk1FZQRwiKX-c", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 620, + "y": 1264.1292186621972, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 557.5626170211469, + "height": 3.772216281857254, + "seed": 1753932961, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": null, + "points": [ + [ + 0, + 0 + ], + [ + 557.5626170211469, + -3.772216281857254 + ] + ] + }, + { + "type": "ellipse", + "version": 1094, + "versionNonce": 437344047, + "isDeleted": false, + "id": "ooo0u1c28dp-bhEM0TpXz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1167.4074267067417, + "y": 1250.4992480885883, + "strokeColor": "#1e1e1e", + "backgroundColor": "#868e96", + "width": 20.60558426546811, + "height": 18.776800161189783, + "seed": 904374401, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 494, + "versionNonce": 1166614438, + "isDeleted": false, + "id": "4CdzvsgPszwMBEai_5J10", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 540, + "y": 1221.2070141671506, + "strokeColor": "#1e1e1e", + "backgroundColor": "#e86565", + "width": 32.90399169921875, + "height": 45, + "seed": 1723920481, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "4.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "4.", + "lineHeight": 1.25, + "baseline": 32 + }, + { + "type": "diamond", + "version": 852, + "versionNonce": 459053391, + "isDeleted": false, + "id": "0vv0rmJC6gCeTxynRXRcW", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 853.7484111377538, + "y": 1250.836200969974, + "strokeColor": "#1971c2", + "backgroundColor": "#15aabf", + "width": 27.8612515301719, + "height": 22.0703795400147, + "seed": 1625701441, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1273, + "versionNonce": 181835194, + "isDeleted": false, + "id": "KU3-eQ8zeBqruDJuNcAgc", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 810.8390012104276, + "y": 1367.9501660414, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 267.31976318359375, + "height": 50, + "seed": 1009334241, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "accrued_reward = \n(block_ts - last_ts) * rps", + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "accrued_reward = \n(block_ts - last_ts) * rps", + "lineHeight": 1.25, + "baseline": 43 + }, + { + "type": "arrow", + "version": 1328, + "versionNonce": 1441798913, + "isDeleted": false, + "id": "ICMEIUgISm6VGiefgSPU-", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 621.5270825019321, + "y": 1238.9164721839152, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 83.42188593108631, + "height": 0.15781612120517252, + "seed": 14259137, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "startBinding": { + "elementId": "HcQNGSgEYJiGpsu4gyhRo", + "focus": 2.1112328570440884, + "gap": 13.916472183915175 + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 83.42188593108631, + 0.15781612120517252 + ] + ] + }, + { + "type": "text", + "version": 613, + "versionNonce": 600980198, + "isDeleted": false, + "id": "HcQNGSgEYJiGpsu4gyhRo", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 624.0415943802759, + "y": 1200, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 29.319961547851562, + "height": 25, + "seed": 718500769, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "ICMEIUgISm6VGiefgSPU-", + "type": "arrow" + } + ], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "rps", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "rps", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "rectangle", + "version": 940, + "versionNonce": 1488730497, + "isDeleted": false, + "id": "vRWJNhPJ5uMCfrx6As8Tp", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1373.708099750514, + "y": 1246.1492551921694, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 21.623593161747976, + "height": 18.85612025559044, + "seed": 607587361, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [], + "updated": 1698854308212, + "link": null, + "locked": false + }, + { + "type": "freedraw", + "version": 216, + "versionNonce": 482396079, + "isDeleted": false, + "id": "VvvH0FQ06-tPESUQJVNI4", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 872.2248592318106, + "y": 1289.3083680766026, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 306.69522057518157, + "height": 45.467404673833926, + "seed": 260363471, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854258955, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0054839470117258315, + 1.0254980911840903 + ], + [ + 0.0054839470117256095, + 2.040028288344729 + ], + [ + 0.0054839470117256095, + 2.9010079691784085 + ], + [ + 0.0054839470117258315, + 5.873307249508798 + ], + [ + 0.0054839470117258315, + 6.931709022762789 + ], + [ + 0.005483947011724943, + 7.990110796017007 + ], + [ + 0.0054839470117258315, + 10.222077229770548 + ], + [ + 0.0054839470117258315, + 10.880150871171963 + ], + [ + 0.0054839470117258315, + 12.240169730068601 + ], + [ + 0.0054839470117258315, + 12.96953468262177 + ], + [ + 0.0054839470117258315, + 15.629248983286288 + ], + [ + 0.12064683425694511, + 16.523132346190096 + ], + [ + 0.5209749661095202, + 17.334756503918697 + ], + [ + 0.921303097962209, + 18.1463806616473 + ], + [ + 1.4642138521184052, + 18.820906144083665 + ], + [ + 2.303257744905409, + 19.396720580310102 + ], + [ + 3.0655263795288192, + 19.797048712162677 + ], + [ + 3.493274246439796, + 19.83543634124453 + ], + [ + 4.721678377056037, + 19.83543634124453 + ], + [ + 5.138458349943562, + 19.83543634124453 + ], + [ + 6.536864837921826, + 19.83543634124453 + ], + [ + 7.255261896451884, + 19.83543634124453 + ], + [ + 8.297211828670925, + 19.83543634124453 + ], + [ + 10.211109335747096, + 19.83543634124453 + ], + [ + 10.869182977148625, + 19.83543634124453 + ], + [ + 13.161472828030583, + 19.83543634124453 + ], + [ + 15.130209805223444, + 19.83543634124453 + ], + [ + 16.084416585255667, + 19.83543634124453 + ], + [ + 19.5612389906604, + 19.83543634124453 + ], + [ + 20.998033107720403, + 19.83543634124453 + ], + [ + 25.39067466407562, + 19.83543634124453 + ], + [ + 27.348443747245142, + 19.83543634124453 + ], + [ + 29.860091478594313, + 20.268668155167234 + ], + [ + 31.922055554985718, + 20.35092736034244 + ], + [ + 32.931101805134745, + 20.537381558739526 + ], + [ + 36.479215521691344, + 20.822546803346768 + ], + [ + 39.1498977163792, + 20.822546803346768 + ], + [ + 40.509916575275724, + 20.822546803346768 + ], + [ + 42.473169605456974, + 21.080292312895608 + ], + [ + 49.832626495130626, + 22.1332101391381 + ], + [ + 51.49974638668118, + 22.407407489722118 + ], + [ + 58.05306306563807, + 23.3232266406726 + ], + [ + 60.592130532045644, + 23.635811620338245 + ], + [ + 65.83478387521109, + 24.83679601589597 + ], + [ + 71.93841689921032, + 26.207782768815832 + ], + [ + 78.2175362275832, + 26.970051403439356 + ], + [ + 81.50790443459084, + 27.97361370657677 + ], + [ + 92.74451186152191, + 30.490745384937554 + ], + [ + 100.67429924041028, + 31.582050840261672 + ], + [ + 107.74310693846508, + 32.50883788523561 + ], + [ + 114.89965778870658, + 33.177879420660474 + ], + [ + 121.06909817684607, + 33.177879420660474 + ], + [ + 124.24978744362, + 33.177879420660474 + ], + [ + 125.28625342882754, + 33.177879420660474 + ], + [ + 126.73949938692238, + 33.177879420660474 + ], + [ + 128.65339689399855, + 33.177879420660474 + ], + [ + 130.56729440107472, + 33.177879420660474 + ], + [ + 133.8686305021057, + 33.177879420660474 + ], + [ + 135.2286493610021, + 33.177879420660474 + ], + [ + 136.56673243185185, + 33.177879420660474 + ], + [ + 137.2248060732535, + 33.177879420660474 + ], + [ + 138.5574051970915, + 33.177879420660474 + ], + [ + 139.45677250700703, + 33.177879420660474 + ], + [ + 140.37807560496924, + 33.177879420660474 + ], + [ + 141.05808503441733, + 33.177879420660474 + ], + [ + 141.5626081594918, + 33.282074413882356 + ], + [ + 141.93003260927446, + 33.6494988636648 + ], + [ + 142.47842731044227, + 34.19789356483284 + ], + [ + 142.85681965424817, + 34.57628590863874 + ], + [ + 143.20779226299567, + 34.927258517386235 + ], + [ + 143.39424646139275, + 35.11371271578332 + ], + [ + 143.77263880519865, + 35.49210505958922 + ], + [ + 144.23329035417976, + 35.9527566085701 + ], + [ + 144.50200375775205, + 36.22147001214239 + ], + [ + 145.31362791548065, + 36.71502524319362 + ], + [ + 146.53106415207333, + 37.31825941447846 + ], + [ + 147.4743030380822, + 37.784394910471065 + ], + [ + 148.18721614960066, + 38.29988592956897 + ], + [ + 148.74657874479192, + 38.49730802198951 + ], + [ + 149.11400319457437, + 38.70021406142155 + ], + [ + 149.29497344595973, + 38.78795721360848 + ], + [ + 149.30594133998318, + 38.96892746499384 + ], + [ + 149.30594133998318, + 39.19376929247278 + ], + [ + 149.30594133998318, + 39.36377164983469 + ], + [ + 149.30594133998318, + 39.687324523523785 + ], + [ + 149.30594133998318, + 40.076684761353135 + ], + [ + 149.30594133998318, + 40.42765737010063 + ], + [ + 149.1469068766445, + 40.43862526412386 + ], + [ + 148.48883323524285, + 40.789597872871354 + ], + [ + 148.29689508983427, + 40.79508181988308 + ], + [ + 148.02269773925025, + 40.97605207126844 + ], + [ + 147.3591401508371, + 41.266701262887636 + ], + [ + 147.1726859524398, + 41.447671514273 + ], + [ + 146.61880730426026, + 41.8589675401488 + ], + [ + 146.3829975827581, + 42.00155016245253 + ], + [ + 146.3829975827581, + 42.18252041383789 + ], + [ + 146.3829975827581, + 42.55542881063229 + ], + [ + 146.3829975827581, + 42.73091511500593 + ], + [ + 146.3829975827581, + 43.03801614766007 + ], + [ + 146.3829975827581, + 43.20801850502198 + ], + [ + 146.3829975827581, + 43.383504809395845 + ], + [ + 146.3829975827581, + 43.553507166757754 + ], + [ + 146.3829975827581, + 43.89351188148203 + ], + [ + 146.3829975827581, + 44.063514238843936 + ], + [ + 146.3829975827581, + 44.23351659620607 + ], + [ + 146.3829975827581, + 44.77642735036238 + ], + [ + 146.3829975827581, + 45.09998022405148 + ], + [ + 146.3829975827581, + 45.26998258141339 + ], + [ + 146.3829975827581, + 45.467404673833926 + ], + [ + 146.3829975827581, + 44.973849442782694 + ], + [ + 147.035587277148, + 43.89899582849375 + ], + [ + 147.3317204157787, + 43.39447270341907 + ], + [ + 147.73204854763128, + 42.58833249270219 + ], + [ + 148.2804432487991, + 41.135086534607126 + ], + [ + 149.0152921483642, + 40.20829948963342 + ], + [ + 149.70626947183575, + 38.95247562395889 + ], + [ + 149.9036915642563, + 38.54666354509459 + ], + [ + 150.92370570842866, + 37.29083967942006 + ], + [ + 151.32951778729296, + 36.45727973364478 + ], + [ + 151.95468774662413, + 35.59081610579938 + ], + [ + 152.74437611630606, + 34.79564378910595 + ], + [ + 154.31278496164646, + 33.34788177802238 + ], + [ + 155.64538408548447, + 32.44851446810708 + ], + [ + 157.65250869175907, + 31.093979556222166 + ], + [ + 159.5060827817067, + 30.11783698814338 + ], + [ + 161.46385186487623, + 29.130726526041144 + ], + [ + 164.052274854389, + 28.0668408057752 + ], + [ + 166.71747310206524, + 26.991987191486032 + ], + [ + 167.7978106633659, + 26.69585405285534 + ], + [ + 170.7426902086379, + 25.659388067648024 + ], + [ + 173.05143190055492, + 25.12196126050344 + ], + [ + 176.80793560355528, + 24.540662877265277 + ], + [ + 182.3960776084566, + 23.559036362174766 + ], + [ + 184.93514507486407, + 23.240967435497396 + ], + [ + 190.46296366263687, + 23.120320601240337 + ], + [ + 194.24140315368413, + 23.120320601240337 + ], + [ + 196.3253030181222, + 23.120320601240337 + ], + [ + 201.80925002980166, + 23.120320601240337 + ], + [ + 202.76345680983377, + 23.120320601240337 + ], + [ + 207.7812683255204, + 23.120320601240337 + ], + [ + 210.062590282379, + 23.120320601240337 + ], + [ + 217.41107927802955, + 23.120320601240337 + ], + [ + 223.7395541295075, + 24.090979222307624 + ], + [ + 228.0170327986175, + 25.308415458900527 + ], + [ + 230.22157949731263, + 25.93358541823204 + ], + [ + 234.85551472218162, + 26.926179827346004 + ], + [ + 240.10365201235868, + 27.847482925307986 + ], + [ + 249.46474956129555, + 29.96977041882792 + ], + [ + 252.60705119898785, + 30.33171092159887 + ], + [ + 255.43128391000278, + 30.759458788509846 + ], + [ + 262.1381511052866, + 31.784956879693937 + ], + [ + 266.39917793336144, + 32.20173685258146 + ], + [ + 270.1227779542917, + 32.20173685258146 + ], + [ + 272.771524360933, + 32.20173685258146 + ], + [ + 275.4422065556207, + 32.20173685258146 + ], + [ + 277.5480422081057, + 32.20173685258146 + ], + [ + 278.2061158495071, + 32.20173685258146 + ], + [ + 279.9116233701395, + 32.20173685258146 + ], + [ + 281.3429335401879, + 32.20173685258146 + ], + [ + 283.1800557891004, + 32.20173685258146 + ], + [ + 283.59683576198813, + 32.20173685258146 + ], + [ + 283.9971638938407, + 32.20173685258146 + ], + [ + 285.0500817200832, + 32.20173685258146 + ], + [ + 286.0042885001153, + 32.20173685258146 + ], + [ + 286.6513942474935, + 32.20173685258146 + ], + [ + 287.7865712789112, + 32.20173685258146 + ], + [ + 288.3788375561726, + 32.20173685258146 + ], + [ + 288.7956175290601, + 32.20173685258146 + ], + [ + 291.35113683650275, + 31.96044318406757 + ], + [ + 291.7514649683553, + 31.55463110520327 + ], + [ + 292.57405702010715, + 30.945912986906933 + ], + [ + 293.90117219693366, + 29.827187796524413 + ], + [ + 295.63958339963597, + 28.511040513721355 + ], + [ + 296.47314334541124, + 27.831031084273036 + ], + [ + 297.6028364298172, + 26.926179827346004 + ], + [ + 298.7599492492816, + 25.75809911385818 + ], + [ + 299.3686673675779, + 25.143897048550116 + ], + [ + 300.6354591272759, + 24.085495275295898 + ], + [ + 302.48903321722355, + 21.66707464314527 + ], + [ + 303.2951734279404, + 20.405766830459015 + ], + [ + 303.7229212948514, + 19.90124370538456 + ], + [ + 304.62777255177843, + 18.201220131764103 + ], + [ + 304.8361625382224, + 17.72411674174782 + ], + [ + 305.2693943521451, + 16.643779180447154 + ], + [ + 305.70811011307933, + 15.508602149029457 + ], + [ + 306.2071492911423, + 14.488588004857093 + ], + [ + 306.69522057518157, + 13.260183874240965 + ], + [ + 306.69522057518157, + 12.432107875477186 + ], + [ + 306.69522057518157, + 12.026295796613113 + ], + [ + 306.69522057518157, + 11.373706102223196 + ], + [ + 306.69522057518157, + 10.995313758417296 + ], + [ + 306.69522057518157, + 10.80885956002021 + ], + [ + 306.69522057518157, + 10.227561176782274 + ], + [ + 306.69522057518157, + 9.843684885964649 + ], + [ + 306.69522057518157, + 9.377549389971819 + ], + [ + 306.69522057518157, + 8.735927589605353 + ], + [ + 306.69522057518157, + 8.253340252577573 + ], + [ + 306.69522057518157, + 7.874947908771674 + ], + [ + 306.69522057518157, + 7.677525816351135 + ], + [ + 306.69522057518157, + 7.112679274148377 + ], + [ + 306.69522057518157, + 6.734286930342478 + ], + [ + 306.69522057518157, + 6.350410639524853 + ], + [ + 306.69522057518157, + 6.15847249411604 + ], + [ + 306.69522057518157, + 5.802015938356817 + ], + [ + 306.69522057518157, + 5.43459148857437 + ], + [ + 306.69522057518157, + 5.105554667873548 + ], + [ + 306.69522057518157, + 4.930068363499913 + ], + [ + 306.69522057518157, + 4.568127860728964 + ], + [ + 306.69522057518157, + 4.387157609343603 + ], + [ + 306.69522057518157, + 4.211671304969968 + ], + [ + 306.69522057518157, + 4.036185000596106 + ], + [ + 306.69522057518157, + 3.860698696222471 + ], + [ + 306.69522057518157, + 3.860698696222471 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "arrow", + "version": 1895, + "versionNonce": 1319831567, + "isDeleted": false, + "id": "Z8kP8JHjCIWHJeGsdUJSH", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1178.2499410993075, + "y": 1220.745164707162, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 83.80681739945408, + "height": 0.06654614351199939, + "seed": 208328623, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854348714, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 83.80681739945408, + 0.06654614351199939 + ] + ] + }, + { + "type": "text", + "version": 981, + "versionNonce": 1706020474, + "isDeleted": false, + "id": "OZz0L89Qzs2TMpE9tb1QK", + "fillStyle": "hachure", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1182.695857503313, + "y": 1178.0176658123721, + "strokeColor": "#e03131", + "backgroundColor": "#ff8787", + "width": 251.27976989746094, + "height": 25, + "seed": 2008156623, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "no rps / schedule finished", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no rps / schedule finished", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 2519, + "versionNonce": 1758850598, + "isDeleted": false, + "id": "2_jUYvR81KO47hOI6LMGy", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1183.0531712415532, + "y": 1444.2281154635627, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ff8787", + "width": 418.9996643066406, + "height": 50, + "seed": 1014988751, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698914940987, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "1. Remove reward info from pool info\n2. Move reward index to finished rewards ", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "1. Remove reward info from pool info\n2. Move reward index to finished rewards ", + "lineHeight": 1.25, + "baseline": 43 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/contracts/tokenomics/incentives/assets/schedules_flow.png b/contracts/tokenomics/incentives/assets/schedules_flow.png new file mode 100644 index 000000000..ef2abc56d Binary files /dev/null and b/contracts/tokenomics/incentives/assets/schedules_flow.png differ diff --git a/contracts/tokenomics/incentives/assets/withdraw.excalidraw b/contracts/tokenomics/incentives/assets/withdraw.excalidraw new file mode 100644 index 000000000..f8d5a7a7a --- /dev/null +++ b/contracts/tokenomics/incentives/assets/withdraw.excalidraw @@ -0,0 +1,549 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "elements": [ + { + "type": "rectangle", + "version": 153, + "versionNonce": 1737510956, + "isDeleted": false, + "id": "TwkcGGMlSbAi8GWmcZIFr", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 408.17578125, + "y": 220.55859375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 283.85546875, + "height": 156.61328125, + "seed": 729077199, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "NUKEGQRZEaHjygIM8A49i" + }, + { + "id": "5sm7b2vd4Bb1UqRdCv16p", + "type": "arrow" + } + ], + "updated": 1698854556011, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 48, + "versionNonce": 1883150740, + "isDeleted": false, + "id": "NUKEGQRZEaHjygIM8A49i", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 435.3036117553711, + "y": 248.865234375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 229.5998077392578, + "height": 100, + "seed": 2061261313, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854557790, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Claim all rewards: \n (pool_index - \nuser_index) * user LP \namount", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "TwkcGGMlSbAi8GWmcZIFr", + "originalText": "Claim all rewards: \n (pool_index - user_index) * user LP amount", + "lineHeight": 1.25, + "baseline": 93 + }, + { + "type": "rectangle", + "version": 275, + "versionNonce": 2136761388, + "isDeleted": false, + "id": "2PpEPpQOGK1OEtlbQGZm0", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 411.384765625, + "y": 473.654296875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 283.85546875, + "height": 156.61328125, + "seed": 1991116431, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "dfjPuh6OXw1qr7muxz6RF" + }, + { + "id": "DPLRSjwova9Pp4PQOIwu6", + "type": "arrow" + }, + { + "id": "5MvmcdzCpoc8jqpUc0AHx", + "type": "arrow" + }, + { + "id": "5sm7b2vd4Bb1UqRdCv16p", + "type": "arrow" + } + ], + "updated": 1698854558631, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 214, + "versionNonce": 2144557972, + "isDeleted": false, + "id": "dfjPuh6OXw1qr7muxz6RF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 441.0925750732422, + "y": 526.9609375, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 224.43984985351562, + "height": 50, + "seed": 1399610543, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854560177, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "user LP amount - \nwithdrawn amount > 0?", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2PpEPpQOGK1OEtlbQGZm0", + "originalText": "user LP amount - withdrawn amount > 0?", + "lineHeight": 1.25, + "baseline": 43 + }, + { + "type": "rectangle", + "version": 496, + "versionNonce": 1009844780, + "isDeleted": false, + "id": "4bWBwCdF09rjhuZXEx_dl", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 167.689453125, + "y": 710.150390625, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 283.85546875, + "height": 156.61328125, + "seed": 1282084559, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "0sDnDcbjWgWhKxNuWD1S4" + }, + { + "id": "DPLRSjwova9Pp4PQOIwu6", + "type": "arrow" + } + ], + "updated": 1698854562742, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 391, + "versionNonce": 214927252, + "isDeleted": false, + "id": "0sDnDcbjWgWhKxNuWD1S4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 188.50729370117188, + "y": 738.45703125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 242.21978759765625, + "height": 100, + "seed": 886988015, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854564349, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Sync user info with pool \nindexes:\n user_index = \npool_index", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "4bWBwCdF09rjhuZXEx_dl", + "originalText": "Sync user info with pool indexes:\n user_index = pool_index", + "lineHeight": 1.25, + "baseline": 93 + }, + { + "type": "rectangle", + "version": 424, + "versionNonce": 44766764, + "isDeleted": false, + "id": "VE1UEm_eYTfIOsZleMHCH", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 640.845703125, + "y": 709.173828125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 283.85546875, + "height": 156.61328125, + "seed": 1607102913, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "lO9AkDR0RL0LB_nyN7PRz" + }, + { + "id": "5MvmcdzCpoc8jqpUc0AHx", + "type": "arrow" + } + ], + "updated": 1698854565289, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 336, + "versionNonce": 2044703636, + "isDeleted": false, + "id": "lO9AkDR0RL0LB_nyN7PRz", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 699.8135147094727, + "y": 774.98046875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 165.9198455810547, + "height": 25, + "seed": 1945670049, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854567072, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Remove user info", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "VE1UEm_eYTfIOsZleMHCH", + "originalText": "Remove user info", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "arrow", + "version": 59, + "versionNonce": 1688419884, + "isDeleted": false, + "id": "DPLRSjwova9Pp4PQOIwu6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 430.80446634676343, + "y": 652.5703125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 32.33263293216248, + "height": 41.953125, + "seed": 269143316, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854575152, + "link": null, + "locked": false, + "startBinding": { + "elementId": "Lv_3amB0Mnx8PVnRphU-4", + "focus": 1.6910995524419348, + "gap": 10.72265625 + }, + "endBinding": { + "elementId": "4bWBwCdF09rjhuZXEx_dl", + "focus": 0.08138058861909432, + "gap": 15.626953125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -32.33263293216248, + 41.953125 + ] + ] + }, + { + "type": "arrow", + "version": 58, + "versionNonce": 17053100, + "isDeleted": false, + "id": "5MvmcdzCpoc8jqpUc0AHx", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 690.4098571878934, + "y": 649.203125, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 22.4866915448456, + "height": 39.5, + "seed": 420089871, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854565290, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2PpEPpQOGK1OEtlbQGZm0", + "focus": -0.43926295187102515, + "gap": 18.935546875 + }, + "endBinding": { + "elementId": "VE1UEm_eYTfIOsZleMHCH", + "focus": -0.07315884923211143, + "gap": 20.470703125 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 22.4866915448456, + 39.5 + ] + ] + }, + { + "type": "arrow", + "version": 71, + "versionNonce": 1504382380, + "isDeleted": false, + "id": "5sm7b2vd4Bb1UqRdCv16p", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 552.5951917344527, + "y": 395.796875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.174664197824427, + "height": 56.71875, + "seed": 2135117743, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1698854558631, + "link": null, + "locked": false, + "startBinding": { + "elementId": "TwkcGGMlSbAi8GWmcZIFr", + "focus": -0.02003584695187376, + "gap": 18.625 + }, + "endBinding": { + "elementId": "2PpEPpQOGK1OEtlbQGZm0", + "focus": -0.008428090792383302, + "gap": 21.138671875 + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -0.174664197824427, + 56.71875 + ] + ] + }, + { + "type": "text", + "version": 36, + "versionNonce": 1347240468, + "isDeleted": false, + "id": "Lv_3amB0Mnx8PVnRphU-4", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 439.97265625, + "y": 663.29296875, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.179962158203125, + "height": 25, + "seed": 1137293825, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "DPLRSjwova9Pp4PQOIwu6", + "type": "arrow" + } + ], + "updated": 1698854574130, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "yes", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "yes", + "lineHeight": 1.25, + "baseline": 18 + }, + { + "type": "text", + "version": 43, + "versionNonce": 691979540, + "isDeleted": false, + "id": "S4j0U3j4_g9pU7_tjIV4j", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 728.34765625, + "y": 658, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 20.41998291015625, + "height": 25, + "seed": 1187392335, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1698854550250, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "no", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "no", + "lineHeight": 1.25, + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/contracts/tokenomics/incentives/assets/withdraw.png b/contracts/tokenomics/incentives/assets/withdraw.png new file mode 100644 index 000000000..44c15ba37 Binary files /dev/null and b/contracts/tokenomics/incentives/assets/withdraw.png differ diff --git a/contracts/tokenomics/incentives/examples/serialization_cost.rs b/contracts/tokenomics/incentives/examples/serialization_cost.rs new file mode 100644 index 000000000..b53fb9b7e --- /dev/null +++ b/contracts/tokenomics/incentives/examples/serialization_cost.rs @@ -0,0 +1,63 @@ +use cosmwasm_std::{to_binary, Decimal}; + +use astroport::asset::AssetInfo; +use astroport::incentives::{RewardInfo, RewardType}; +use astroport_incentives::state::{PoolInfo, UserInfo}; + +fn main() { + // This example shows us a rough estimations of gas costs for storage operations charged by Cosmos SDK. + // It doesn't include costs charged within Wasm VM to serialize/deserialize data into Rust structures. + // Given that we allow 5 external rewards and 1 ASTRO reward per pool, we can estimate the following gas costs: + + let reward_info = RewardInfo { + reward: RewardType::Ext { + info: AssetInfo::native("test"), + next_update_ts: 0, + }, + rps: Default::default(), + index: Default::default(), + orphaned: Default::default(), + }; + let pool_info = PoolInfo::default(); + + // https://github.com/cosmos/cosmos-sdk/blob/47f46643affd7ec7978329c42bac47275ac7e1cc/store/types/gas.go#L199 + let reward_info_storage_bytes = to_binary(&reward_info).unwrap().len(); + println!("reward info storage bytes {reward_info_storage_bytes}"); + println!("sdk gas cost per read {}", reward_info_storage_bytes * 3); + println!("sdk gas cost per write {}", reward_info_storage_bytes * 30); + + let pool_info_storage_bytes = to_binary(&pool_info).unwrap().len(); + println!("pool info storage bytes {pool_info_storage_bytes}"); + println!("sdk gas cost per read {}", pool_info_storage_bytes * 3); + println!("sdk gas cost per write {}", pool_info_storage_bytes * 30); + + // Gas costs for a pool with 4 + 1 rewards + let pool_storage_bytes = pool_info_storage_bytes + 6 * reward_info_storage_bytes; + println!("pool with 5 + 1 rewards storage bytes {pool_storage_bytes}"); + println!("sdk gas cost per read {}", pool_storage_bytes * 3); + println!("sdk gas cost per write {}", pool_storage_bytes * 30); + + let user_info = UserInfo { + amount: Default::default(), + last_rewards_index: Default::default(), + last_claim_time: 0, + }; + let user_info_storage_bytes = to_binary(&user_info).unwrap().len(); + println!("user info storage bytes {user_info_storage_bytes}"); + println!("sdk gas cost per read {}", user_info_storage_bytes * 3); + println!("sdk gas cost per write {}", user_info_storage_bytes * 30); + + // Gas costs for a pool with 5 + 1 rewards + let reward_index_entry = ( + RewardType::Ext { + info: AssetInfo::native("test"), + next_update_ts: 0, + }, + Decimal::zero(), + ); + let reward_entry_storage_bytes = to_binary(&reward_index_entry).unwrap().len(); + let user_storage_bytes = user_info_storage_bytes + 6 * reward_entry_storage_bytes; + println!("user with 5 + 1 rewards storage bytes {user_storage_bytes}"); + println!("sdk gas cost per read {}", user_storage_bytes * 3); + println!("sdk gas cost per write {}", user_storage_bytes * 30); +} diff --git a/contracts/tokenomics/incentives/src/error.rs b/contracts/tokenomics/incentives/src/error.rs new file mode 100644 index 000000000..01e511841 --- /dev/null +++ b/contracts/tokenomics/incentives/src/error.rs @@ -0,0 +1,64 @@ +use astroport::factory::PairType; +use cosmwasm_std::{CheckedFromRatioError, OverflowError, StdError, Uint128}; +use cw_utils::PaymentError; +use thiserror::Error; + +use astroport::incentives::MAX_REWARD_TOKENS; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("{0}")] + CheckedFromRatioError(#[from] CheckedFromRatioError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Duplicated pool found")] + DuplicatedPoolFound {}, + + #[error("Amount to withdraw {withdraw_amount} exceeds balance {available}")] + AmountExceedsBalance { + available: Uint128, + withdraw_amount: Uint128, + }, + + #[error("User {user} doesn't have position in {lp_token}")] + PositionDoesntExist { user: String, lp_token: String }, + + #[error("Pool {pool} doesn't have {reward} reward")] + RewardNotFound { pool: String, reward: String }, + + #[error("Too many reward tokens in pool {lp_token}. Maximum allowed is {MAX_REWARD_TOKENS}")] + TooManyRewardTokens { lp_token: String }, + + #[error("Incentivization fee {fee} expected as you are trying to add new reward token {new_reward_token} for pool {lp_token}")] + IncentivizationFeeExpected { + fee: String, + lp_token: String, + new_reward_token: String, + }, + + #[error("Token {token} is blocked")] + BlockedToken { token: String }, + + #[error("Pair type {pair_type} is blocked")] + BlockedPairType { pair_type: PairType }, + + #[error("Failed to parse or process reply message")] + FailedToParseReply {}, + + #[error("No orphaned rewards to claim")] + NoOrphanedRewards {}, + + #[error("Failed to set 0 alloc point for pool {lp_token}")] + ZeroAllocPoint { lp_token: String }, +} diff --git a/contracts/tokenomics/incentives/src/execute.rs b/contracts/tokenomics/incentives/src/execute.rs new file mode 100644 index 000000000..e4bf152b4 --- /dev/null +++ b/contracts/tokenomics/incentives/src/execute.rs @@ -0,0 +1,535 @@ +use std::collections::HashSet; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, ensure, from_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + Uint128, +}; +use cw_utils::one_coin; +use itertools::Itertools; + +use astroport::asset::{ + addr_opt_validate, determine_asset_info, validate_native_denom, Asset, AssetInfo, AssetInfoExt, +}; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport::factory; +use astroport::factory::PairType; +use astroport::incentives::{Cw20Msg, ExecuteMsg, IncentivizationFeeInfo}; + +use crate::error::ContractError; +use crate::state::{ + Op, PoolInfo, UserInfo, ACTIVE_POOLS, BLOCKED_TOKENS, CONFIG, OWNERSHIP_PROPOSAL, +}; +use crate::utils::{ + asset_info_key, claim_orphaned_rewards, claim_rewards, deactivate_blocked_pools, + deactivate_pool, incentivize, is_pool_registered, query_pair_info, remove_reward_from_pool, +}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::SetupPools { pools } => setup_pools(deps, env, info, pools), + ExecuteMsg::ClaimRewards { lp_tokens } => { + // Check for duplicated pools + ensure!( + lp_tokens.iter().all_unique(), + ContractError::DuplicatedPoolFound {} + ); + + // Collect in-memory mutable objects + let mut tuples = lp_tokens + .into_iter() + .map(|lp_token| { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let pool_info = PoolInfo::load(deps.storage, &lp_asset)?; + let user_pos = UserInfo::load_position(deps.storage, &info.sender, &lp_asset)?; + Ok((lp_asset, pool_info, user_pos)) + }) + .collect::, ContractError>>()?; + + // Convert to mutable references + let mut_tuples = tuples + .iter_mut() + .map(|(lp_asset, pool_info, user_pos)| (&*lp_asset, pool_info, user_pos)) + .collect_vec(); + + // Compose response. Return early in case of error + let response = claim_rewards(deps.storage, None, env, &info.sender, mut_tuples)?; + + // Save updates in state + for (lp_asset, pool_info, user_pos) in tuples { + pool_info.save(deps.storage, &lp_asset)?; + user_pos.save(deps.storage, &info.sender, &lp_asset)?; + } + + Ok(response) + } + ExecuteMsg::Receive(cw20msg) => { + let maybe_lp = Asset::cw20(info.sender, cw20msg.amount); + let recipient = match from_binary(&cw20msg.msg)? { + Cw20Msg::Deposit { recipient } => recipient, + Cw20Msg::DepositFor(recipient) => Some(recipient), + }; + + deposit( + deps, + env, + maybe_lp, + Addr::unchecked(cw20msg.sender), + recipient, + ) + } + ExecuteMsg::Deposit { recipient } => { + let maybe_lp_coin = one_coin(&info)?; + let maybe_lp = Asset::native(maybe_lp_coin.denom, maybe_lp_coin.amount); + + deposit(deps, env, maybe_lp, info.sender, recipient) + } + ExecuteMsg::Withdraw { lp_token, amount } => withdraw(deps, env, info, lp_token, amount), + ExecuteMsg::SetTokensPerSecond { amount } => set_tokens_per_second(deps, env, info, amount), + ExecuteMsg::Incentivize { lp_token, schedule } => { + incentivize(deps, info, env, lp_token, schedule) + } + ExecuteMsg::RemoveRewardFromPool { + lp_token, + reward, + bypass_upcoming_schedules, + receiver, + } => remove_reward_from_pool( + deps, + info, + env, + lp_token, + reward, + bypass_upcoming_schedules, + receiver, + ), + ExecuteMsg::ClaimOrphanedRewards { limit, receiver } => { + claim_orphaned_rewards(deps, info, limit, receiver) + } + ExecuteMsg::UpdateConfig { + vesting_contract, + generator_controller, + guardian, + incentivization_fee_info, + } => update_config( + deps, + info, + vesting_contract, + generator_controller, + guardian, + incentivization_fee_info, + ), + ExecuteMsg::UpdateBlockedTokenslist { add, remove } => { + update_blocked_pool_tokens(deps, env, info, add, remove) + } + ExecuteMsg::DeactivatePool { lp_token } => deactivate_pool(deps, info, env, lp_token), + ExecuteMsg::DeactivateBlockedPools {} => deactivate_blocked_pools(deps, env), + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +fn deposit( + deps: DepsMut, + env: Env, + maybe_lp: Asset, + sender: Addr, + recipient: Option, +) -> Result { + let staker = addr_opt_validate(deps.api, &recipient)?.unwrap_or(sender); + + let pair_info = query_pair_info(deps.as_ref(), &maybe_lp.info)?; + let config = CONFIG.load(deps.storage)?; + is_pool_registered( + deps.querier, + &config, + &pair_info, + &maybe_lp.info.to_string(), + )?; + + let mut pool_info = PoolInfo::may_load(deps.storage, &maybe_lp.info)?.unwrap_or_default(); + let mut user_info = UserInfo::may_load_position(deps.storage, &staker, &maybe_lp.info)? + .unwrap_or_else(|| UserInfo::new(&env)); + + let response = claim_rewards( + deps.storage, + Some(config.vesting_contract), + env, + &staker, + vec![(&maybe_lp.info, &mut pool_info, &mut user_info)], + )?; + + user_info.update_and_sync_position(Op::Add(maybe_lp.amount), &mut pool_info); + pool_info.save(deps.storage, &maybe_lp.info)?; + user_info.save(deps.storage, &staker, &maybe_lp.info)?; + + Ok(response.add_attributes([ + attr("action", "deposit"), + attr("lp_token", maybe_lp.info.to_string()), + attr("user", staker.as_str()), + attr("amount", maybe_lp.amount), + ])) +} + +fn withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, + lp_token: String, + amount: Uint128, +) -> Result { + let lp_token_asset = determine_asset_info(&lp_token, deps.api)?; + + let mut user_info = UserInfo::load_position(deps.storage, &info.sender, &lp_token_asset)?; + + if user_info.amount < amount { + Err(ContractError::AmountExceedsBalance { + available: user_info.amount, + withdraw_amount: amount, + }) + } else { + let mut pool_info = PoolInfo::load(deps.storage, &lp_token_asset)?; + + let response = claim_rewards( + deps.storage, + None, + env, + &info.sender, + vec![(&lp_token_asset, &mut pool_info, &mut user_info)], + )?; + + user_info.update_and_sync_position(Op::Sub(amount), &mut pool_info); + pool_info.save(deps.storage, &lp_token_asset)?; + if user_info.amount.is_zero() { + // If user has withdrawn all LP tokens, we can remove his position + user_info.remove(deps.storage, &info.sender, &lp_token_asset); + } else { + user_info.save(deps.storage, &info.sender, &lp_token_asset)?; + } + + let transfer_msg = lp_token_asset.with_balance(amount).into_msg(info.sender)?; + + Ok(response.add_message(transfer_msg).add_attributes([ + attr("action", "withdraw"), + attr("lp_token", lp_token_asset.to_string()), + attr("amount", amount), + ])) + } +} + +pub fn setup_pools( + deps: DepsMut, + env: Env, + info: MessageInfo, + pools: Vec<(String, Uint128)>, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && Some(info.sender) != config.generator_controller { + return Err(ContractError::Unauthorized {}); + } + + let mut pools_set: HashSet<_> = Default::default(); + for (pool, alloc_points) in &pools { + if alloc_points.is_zero() { + return Err(ContractError::ZeroAllocPoint { + lp_token: pool.to_owned(), + }); + } + + if !pools_set.insert(pool) { + return Err(ContractError::DuplicatedPoolFound {}); + } + } + + let blacklisted_pair_types: Vec = deps + .querier + .query_wasm_smart(&config.factory, &factory::QueryMsg::BlacklistedPairTypes {})?; + + let setup_pools = pools + .into_iter() + .map(|(lp_token, alloc_point)| { + let maybe_lp = determine_asset_info(&lp_token, deps.api)?; + let pair_info = query_pair_info(deps.as_ref(), &maybe_lp)?; + + is_pool_registered(deps.querier, &config, &pair_info, &lp_token)?; + + // check if assets in the blocked list + for asset in &pair_info.asset_infos { + if BLOCKED_TOKENS.has(deps.storage, &asset_info_key(asset)) { + return Err(ContractError::BlockedToken { + token: asset.to_string(), + }); + } + } + + // check if pair type is blacklisted + if blacklisted_pair_types.contains(&pair_info.pair_type) { + return Err(ContractError::BlockedPairType { + pair_type: pair_info.pair_type, + }); + } + + Ok((maybe_lp, alloc_point)) + }) + .collect::, ContractError>>()?; + + // Update all reward indexes and remove astro rewards from old active pools + for (lp_token_asset, _) in ACTIVE_POOLS.load(deps.storage)? { + let mut pool_info = PoolInfo::load(deps.storage, &lp_token_asset)?; + pool_info.update_rewards(deps.storage, &env, &lp_token_asset)?; + pool_info.disable_astro_rewards(); + pool_info.save(deps.storage, &lp_token_asset)?; + } + + config.total_alloc_points = setup_pools.iter().map(|(_, alloc)| alloc).sum(); + + // Set astro rewards for new active pools + for (active_pool, alloc_points) in &setup_pools { + let mut pool_info = PoolInfo::may_load(deps.storage, active_pool)?.unwrap_or_default(); + pool_info.update_rewards(deps.storage, &env, active_pool)?; + pool_info.set_astro_rewards(&config, *alloc_points); + pool_info.save(deps.storage, active_pool)?; + } + + ACTIVE_POOLS.save(deps.storage, &setup_pools)?; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "setup_pools")) +} + +fn set_tokens_per_second( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let pool_infos = ACTIVE_POOLS + .load(deps.storage)? + .into_iter() + .map(|(lp_token, alloc_points)| { + let mut pool_info = PoolInfo::load(deps.storage, &lp_token)?; + pool_info.update_rewards(deps.storage, &env, &lp_token)?; + Ok((pool_info, lp_token, alloc_points)) + }) + .collect::>>()?; + + config.astro_per_second = amount; + + for (mut pool_info, lp_token, alloc_points) in pool_infos { + pool_info.set_astro_rewards(&config, alloc_points); + pool_info.save(deps.storage, &lp_token)?; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "set_tokens_per_second")) +} + +fn update_config( + deps: DepsMut, + info: MessageInfo, + vesting_contract: Option, + generator_controller: Option, + guardian: Option, + incentivization_fee_info: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let mut attrs = vec![attr("action", "update_config")]; + + if let Some(vesting_contract) = vesting_contract { + config.vesting_contract = deps.api.addr_validate(&vesting_contract)?; + attrs.push(attr("new_vesting_contract", vesting_contract)); + } + + if let Some(generator_controller) = generator_controller { + config.generator_controller = Some(deps.api.addr_validate(&generator_controller)?); + attrs.push(attr("new_generator_controller", generator_controller)); + } + + if let Some(guardian) = guardian { + config.guardian = Some(deps.api.addr_validate(guardian.as_str())?); + attrs.push(attr("new_guardian", guardian)); + } + + if let Some(new_info) = incentivization_fee_info { + deps.api.addr_validate(new_info.fee_receiver.as_str())?; + validate_native_denom(&new_info.fee.denom)?; + attrs.push(attr( + "new_incentivization_fee_receiver", + &new_info.fee_receiver, + )); + attrs.push(attr("new_incentivization_fee", new_info.fee.to_string())); + + config.incentivization_fee_info = Some(new_info); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes(attrs)) +} + +fn update_blocked_pool_tokens( + deps: DepsMut, + env: Env, + info: MessageInfo, + add: Vec, + remove: Vec, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner && Some(info.sender) != config.guardian { + return Err(ContractError::Unauthorized {}); + } + + // Checking for duplicates + ensure!( + remove.iter().chain(add.iter()).all_unique(), + StdError::generic_err("Duplicated tokens found") + ); + + // Remove tokens from blocklist + for asset_info in remove { + let asset_info_key = asset_info_key(&asset_info); + ensure!( + BLOCKED_TOKENS.has(deps.storage, &asset_info_key), + StdError::generic_err(format!( + "Token {asset_info} wasn't found in the blocked list", + )) + ); + + BLOCKED_TOKENS.remove(deps.storage, &asset_info_key); + } + + // Add tokens to blocklist + if !add.is_empty() { + let active_pools = ACTIVE_POOLS + .load(deps.storage)? + .into_iter() + .map(|(lp_asset, alloc_points)| { + let asset_infos = query_pair_info(deps.as_ref(), &lp_asset)?.asset_infos; + Ok((lp_asset, asset_infos, alloc_points)) + }) + .collect::>>()?; + + let mut to_disable = vec![]; + + for token_to_block in &add { + let asset_info_key = asset_info_key(token_to_block); + if !BLOCKED_TOKENS.has(deps.storage, &asset_info_key) { + if token_to_block.eq(&config.astro_token) { + return Err(StdError::generic_err(format!( + "Blocking ASTRO token {token_to_block} is prohibited", + )) + .into()); + } + + for (lp_asset, asset_infos, alloc_points) in &active_pools { + if asset_infos.contains(token_to_block) { + to_disable.push((lp_asset.clone(), alloc_points)); + } + } + + BLOCKED_TOKENS.save(deps.storage, &asset_info_key, &())?; + } else { + return Err(StdError::generic_err(format!( + "Token {token_to_block} is already in the blocked list", + )) + .into()); + } + } + + if !to_disable.is_empty() { + let mut reduce_total_alloc_points = Uint128::zero(); + + // Update all reward indexes and remove astro rewards from disabled pools + for (lp_token_asset, alloc_points) in &to_disable { + let mut pool_info = PoolInfo::load(deps.storage, lp_token_asset)?; + pool_info.update_rewards(deps.storage, &env, lp_token_asset)?; + pool_info.disable_astro_rewards(); + pool_info.save(deps.storage, lp_token_asset)?; + reduce_total_alloc_points += *alloc_points; + } + + let new_active_pools = active_pools + .iter() + .filter_map(|(lp_asset, _, alloc_points)| { + if to_disable + .iter() + .any(|(disable_lp, _)| disable_lp == lp_asset) + { + None + } else { + Some((lp_asset.clone(), *alloc_points)) + } + }) + .collect_vec(); + + config.total_alloc_points = config + .total_alloc_points + .checked_sub(reduce_total_alloc_points)?; + + for (lp_asset, alloc_points) in &new_active_pools { + let mut pool_info = PoolInfo::load(deps.storage, lp_asset)?; + pool_info.update_rewards(deps.storage, &env, lp_asset)?; + pool_info.set_astro_rewards(&config, *alloc_points); + pool_info.save(deps.storage, lp_asset)?; + } + + ACTIVE_POOLS.save(deps.storage, &new_active_pools)?; + } + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "update_tokens_blocklist")) +} diff --git a/contracts/tokenomics/incentives/src/instantiate.rs b/contracts/tokenomics/incentives/src/instantiate.rs new file mode 100644 index 000000000..d77200429 --- /dev/null +++ b/contracts/tokenomics/incentives/src/instantiate.rs @@ -0,0 +1,49 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint128}; + +use astroport::asset::{addr_opt_validate, validate_native_denom}; +use astroport::incentives::{Config, InstantiateMsg}; + +use crate::error::ContractError; +use crate::state::{ACTIVE_POOLS, CONFIG}; + +/// Contract name that is used for migration. +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +/// Contract version that is used for migration. +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + msg.astro_token.check(deps.api)?; + + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + if let Some(fee_info) = &msg.incentivization_fee_info { + deps.api.addr_validate(fee_info.fee_receiver.as_str())?; + validate_native_denom(&fee_info.fee.denom)?; + } + + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + factory: deps.api.addr_validate(&msg.factory)?, + generator_controller: None, + astro_token: msg.astro_token, + astro_per_second: Uint128::zero(), + total_alloc_points: Uint128::zero(), + vesting_contract: deps.api.addr_validate(&msg.vesting_contract)?, + guardian: addr_opt_validate(deps.api, &msg.guardian)?, + incentivization_fee_info: msg.incentivization_fee_info, + }, + )?; + ACTIVE_POOLS.save(deps.storage, &vec![])?; + + Ok(Response::new()) +} diff --git a/contracts/tokenomics/incentives/src/lib.rs b/contracts/tokenomics/incentives/src/lib.rs new file mode 100644 index 000000000..4ac6a50bd --- /dev/null +++ b/contracts/tokenomics/incentives/src/lib.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod execute; +pub mod instantiate; +pub mod query; +pub mod reply; +pub mod state; +pub mod traits; +pub mod utils; diff --git a/contracts/tokenomics/incentives/src/query.rs b/contracts/tokenomics/incentives/src/query.rs new file mode 100644 index 000000000..072abb37c --- /dev/null +++ b/contracts/tokenomics/incentives/src/query.rs @@ -0,0 +1,225 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ensure, to_binary, Binary, Deps, Env, Order, StdError, StdResult, Uint128}; +use cw_storage_plus::Bound; +use itertools::Itertools; + +use astroport::asset::{determine_asset_info, Asset, AssetInfo, AssetInfoExt}; +use astroport::incentives::{QueryMsg, RewardType, ScheduleResponse, MAX_PAGE_LIMIT}; + +use crate::error::ContractError; +use crate::state::{ + list_pool_stakers, PoolInfo, UserInfo, BLOCKED_TOKENS, CONFIG, EXTERNAL_REWARD_SCHEDULES, +}; +use crate::utils::{asset_info_key, from_key_to_asset_info}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::Config {} => Ok(to_binary(&CONFIG.load(deps.storage)?)?), + QueryMsg::Deposit { lp_token, user } => { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let user_addr = deps.api.addr_validate(&user)?; + let amount = UserInfo::load_position(deps.storage, &user_addr, &lp_asset)?.amount; + Ok(to_binary(&amount)?) + } + QueryMsg::PendingRewards { lp_token, user } => Ok(to_binary(&query_pending_rewards( + deps, env, user, lp_token, + )?)?), + QueryMsg::RewardInfo { lp_token } => { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let mut pool_info = PoolInfo::load(deps.storage, &lp_asset)?; + pool_info.update_rewards(deps.storage, &env, &lp_asset)?; + Ok(to_binary(&pool_info.rewards)?) + } + QueryMsg::BlockedTokensList { start_after, limit } => { + Ok(to_binary(&query_blocked_tokens(deps, start_after, limit)?)?) + } + QueryMsg::PoolInfo { lp_token } => { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + Ok(to_binary( + &PoolInfo::load(deps.storage, &lp_asset)?.into_response(), + )?) + } + QueryMsg::PoolStakers { + lp_token, + start_after, + limit, + } => { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let start_after = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + let stakers = list_pool_stakers(deps.storage, &lp_asset, start_after, limit)?; + Ok(to_binary(&stakers)?) + } + QueryMsg::IsFeeExpected { lp_token, reward } => { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let pool_info = PoolInfo::may_load(deps.storage, &lp_asset)?; + + let is_fee_expected = pool_info + .map(|mut x| -> StdResult<_> { + // update_rewards() removes finished schedules + x.update_rewards(deps.storage, &env, &lp_asset)?; + let reward_asset = determine_asset_info(&reward, deps.api)?; + + let expected = x + .rewards + .into_iter() + .filter(|x| x.reward.is_external()) + .all(|x| x.reward.asset_info() != &reward_asset); + + Ok(expected) + }) + .transpose()? + .unwrap_or(true); + + Ok(to_binary(&is_fee_expected)?) + } + QueryMsg::ExternalRewardSchedules { + reward, + lp_token, + start_after, + limit, + } => Ok(to_binary(&query_external_reward_schedules( + deps, + env, + reward, + lp_token, + start_after, + limit, + )?)?), + } +} + +fn query_blocked_tokens( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(MAX_PAGE_LIMIT) as usize; + if let Some(start_after) = start_after { + let asset_key = asset_info_key(&start_after); + BLOCKED_TOKENS.range( + deps.storage, + Some(Bound::exclusive(asset_key.as_slice())), + None, + Order::Ascending, + ) + } else { + BLOCKED_TOKENS.range(deps.storage, None, None, Order::Ascending) + } + .take(limit) + .map(|item| item.map(|(k, _)| from_key_to_asset_info(k))?) + .collect() +} + +pub fn query_pending_rewards( + deps: Deps, + env: Env, + user: String, + lp_token: String, +) -> Result, ContractError> { + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let user_addr = deps.api.addr_validate(&user)?; + + let mut pool_info = PoolInfo::load(deps.storage, &lp_asset)?; + pool_info.update_rewards(deps.storage, &env, &lp_asset)?; + + let mut pos = UserInfo::load_position(deps.storage, &user_addr, &lp_asset)?; + + let mut outstanding_rewards = + pos.claim_finished_rewards(deps.storage, &lp_asset, &pool_info)?; + + // Reset user reward index for all finished schedules + pos.reset_user_index(deps.storage, &lp_asset, &pool_info)?; + + let active_rewards = pool_info + .calculate_rewards(&mut pos) + .into_iter() + .map(|(_, asset)| asset); + + outstanding_rewards.extend(active_rewards); + + let aggregated = outstanding_rewards + .into_iter() + .group_by(|asset| asset.info.clone()) + .into_iter() + .map(|(info, assets)| { + let amount: Uint128 = assets.into_iter().map(|asset| asset.amount).sum(); + info.with_balance(amount) + }) + .collect(); + + Ok(aggregated) +} + +pub fn query_external_reward_schedules( + deps: Deps, + env: Env, + reward: String, + lp_token: String, + start_after: Option, + limit: Option, +) -> Result, ContractError> { + let mut limit = limit.unwrap_or(MAX_PAGE_LIMIT).min(MAX_PAGE_LIMIT); + ensure!(limit > 0, StdError::generic_err("limit must be > 0")); + + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let reward_asset = determine_asset_info(&reward, deps.api)?; + let mut pool_info = PoolInfo::load(deps.storage, &lp_asset)?; + pool_info.update_rewards(deps.storage, &env, &lp_asset)?; + + let (rps, end_ts) = pool_info + .rewards + .iter() + .find_map(|active| match &active.reward { + RewardType::Ext { + info, + next_update_ts, + } if info == &reward_asset => Some((active.rps, *next_update_ts)), + _ => None, + }) + .ok_or(ContractError::RewardNotFound { + pool: lp_token, + reward, + })?; + + let mut start_after = start_after.unwrap_or_else(|| env.block.time.seconds()); + let mut results = vec![]; + + if start_after < end_ts { + results.push(ScheduleResponse { + rps, + start_ts: env.block.time.seconds(), + end_ts, + }); + limit -= 1; + start_after = end_ts + } + let from_state = EXTERNAL_REWARD_SCHEDULES + .prefix((&lp_asset, &reward_asset)) + .range( + deps.storage, + Some(Bound::exclusive(start_after)), + None, + Order::Ascending, + ) + .take(limit as usize) + .collect::>>()? + .into_iter() + .map(|(next_update_ts, rps)| { + let resp = ScheduleResponse { + rps, + start_ts: start_after, + end_ts: next_update_ts, + }; + start_after = next_update_ts; + + resp + }); + + results.extend(from_state); + + Ok(results) +} diff --git a/contracts/tokenomics/incentives/src/reply.rs b/contracts/tokenomics/incentives/src/reply.rs new file mode 100644 index 000000000..9d7b574f8 --- /dev/null +++ b/contracts/tokenomics/incentives/src/reply.rs @@ -0,0 +1,22 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{DepsMut, Env, Reply, Response, SubMsgResult}; + +use crate::error::ContractError; + +pub const POST_TRANSFER_REPLY_ID: u64 = 1; + +/// The entry point to the contract for processing replies from submessages. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(_deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg { + // Caller context: either utils:claim_rewards() or utils:remove_reward_from_pool(). + // If cw20 token reverts the transfer, we bypass it silently. + // This can happen in abnormal situations when cw20 contract was tweaked and broken. + Reply { + id: POST_TRANSFER_REPLY_ID, + result: SubMsgResult::Err(err_msg), + } => Ok(Response::new().add_attribute("transfer_error", err_msg)), + _ => Err(ContractError::FailedToParseReply {}), + } +} diff --git a/contracts/tokenomics/incentives/src/state.rs b/contracts/tokenomics/incentives/src/state.rs new file mode 100644 index 000000000..b7ddfc202 --- /dev/null +++ b/contracts/tokenomics/incentives/src/state.rs @@ -0,0 +1,701 @@ +use std::collections::{HashMap, HashSet}; + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Decimal, Env, Order, StdError, StdResult, Storage, Uint128}; +use cw_storage_plus::{Bound, Item, Map}; +use itertools::Itertools; + +use astroport::asset::{Asset, AssetInfo, AssetInfoExt}; +use astroport::common::OwnershipProposal; +use astroport::incentives::{Config, IncentivesSchedule}; +use astroport::incentives::{PoolInfoResponse, RewardInfo, RewardType}; +use astroport::incentives::{MAX_PAGE_LIMIT, MAX_REWARD_TOKENS}; + +use crate::error::ContractError; +use crate::traits::RewardInfoExt; +use crate::utils::asset_info_key; + +/// General generator contract settings +pub const CONFIG: Item = Item::new("config"); + +/// Contains a proposal to change contract ownership. +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); +/// Pools which receive ASTRO emissions +pub const ACTIVE_POOLS: Item> = Item::new("active_pools"); +/// Prohibited tokens set. Key: binary representing [`AssetInfo`] converted with [`crate::utils::asset_info_key`]. +pub const BLOCKED_TOKENS: Map<&[u8], ()> = Map::new("blocked_tokens"); + +/// Contains reward indexes for finished rewards. They are removed from [`PoolInfo`] and stored here. +/// Next time user claims rewards they will be able to claim outstanding rewards from this index. +/// key: (LP token asset, deregistration timestamp), value: array of tuples (reward token asset, reward index). +pub const FINISHED_REWARD_INDEXES: Map<(&AssetInfo, u64), Vec<(AssetInfo, Decimal)>> = + Map::new("fin_rew_inds"); + +/// key: lp_token (either cw20 or native), value: pool info +pub const POOLS: Map<&AssetInfo, PoolInfo> = Map::new("pools"); +/// key: (lp_token, user_addr), value: user info +pub const USER_INFO: Map<(&AssetInfo, &Addr), UserInfo> = Map::new("user_info"); +/// key: (LP token asset, reward token asset, schedule end point), value: reward per second +pub const EXTERNAL_REWARD_SCHEDULES: Map<(&AssetInfo, &AssetInfo, u64), Decimal> = + Map::new("reward_schedules"); + +/// Accumulates all orphaned rewards i.e. those which were added to a pool +/// but this pool never received any LP tokens deposits. +/// key: Key: binary representing [`AssetInfo`] converted with [`asset_info_key`], +/// value: total amount of orphaned tokens +pub const ORPHANED_REWARDS: Map<&[u8], Uint128> = Map::new("orphaned_rewards"); + +impl RewardInfoExt for RewardInfo { + /// This function is tightly coupled with [`UserInfo`] structure. It iterates over all user's + /// reward indexes and tries to find the one that matches current reward info. If found, it + /// calculates the reward amount. + /// Otherwise it assumes user never claimed this particular reward and their reward index is 0. + /// Their position will be synced with pool indexes later on. + fn calculate_reward(&self, user_info: &UserInfo) -> Uint128 { + let user_index_opt = user_info + .last_rewards_index + .iter() + .find(|(reward_type, _)| reward_type.matches(&self.reward)); + + // In case reward was moved into finished state and then is incentivized again + // it might have self.reward_index == 0, but user index might be > 0 containing outstanding + // rewards from past schedules. + // Outstanding rewards from finished schedules are handled in claim_finished_rewards(). + // To account current active period properly we need to consider user index as 0. + match user_index_opt { + Some((_, user_reward_index)) if *user_reward_index > self.index => { + self.index * user_info.amount + } + None => self.index * user_info.amount, + Some((_, user_reward_index)) => (self.index - *user_reward_index) * user_info.amount, + } + } +} + +#[cw_serde] +#[derive(Default)] +pub struct PoolInfo { + /// Total amount of LP tokens staked in this pool + pub total_lp: Uint128, + /// Vector containing reward info for each reward token + pub rewards: Vec, + /// Last time when reward indexes were updated + pub last_update_ts: u64, + /// Rewards to remove; In-memory hash map to avoid unnecessary state writes; + /// Key: reward type, value: (reward index, orphaned rewards) + /// NOTE: this is not part of serialized structure in state! + #[serde(skip)] + pub rewards_to_remove: HashMap, +} + +impl PoolInfo { + /// Loop over all rewards and update their indexes according to the amount of LP tokens staked and rewards per second. + /// If multiple schedules for a specific reward passed since the last update, aggregate all rewards. + /// Move to the next schedule if it's time to do so or remove reward from pool info if there are no more schedules left. + pub fn update_rewards( + &mut self, + storage: &dyn Storage, + env: &Env, + lp_asset: &AssetInfo, + ) -> StdResult<()> { + let block_ts = env.block.time.seconds(); + let mut time_passed: Uint128 = block_ts.saturating_sub(self.last_update_ts).into(); + + if time_passed.is_zero() { + return Ok(()); + } + + for reward_info in self.rewards.iter_mut() { + let mut collected_rewards = Decimal::zero(); + + // Whether we need to remove this reward from pool info. Only applicable for finished external rewards. + let mut need_remove = false; + + if let RewardType::Ext { + info, + next_update_ts, + } = &reward_info.reward + { + let mut next_update_ts = *next_update_ts; + // Time to move to the next schedule? + if next_update_ts <= block_ts { + // Schedule ended. Collect leftovers from the last update time + collected_rewards += reward_info.rps + * Decimal::from_ratio(next_update_ts - self.last_update_ts, 1u8); + + // Find which passed schedules should be processed (can be multiple ones) + let schedules = EXTERNAL_REWARD_SCHEDULES.prefix((lp_asset, info)).range( + storage, + Some(Bound::exclusive(next_update_ts)), + None, + Order::Ascending, + ); + + for period in schedules { + let (update_ts, period_reward_per_sec) = period?; + // We found a schedule which should be active atm + if update_ts > block_ts { + reward_info.rps = period_reward_per_sec; + reward_info.reward = RewardType::Ext { + info: info.clone(), + next_update_ts: update_ts, + }; + time_passed = (block_ts - next_update_ts).into(); + next_update_ts = update_ts; + break; + } + + // Process schedules one by one and collect rewards + collected_rewards += period_reward_per_sec + * Decimal::from_ratio(update_ts - next_update_ts, 1u8); + next_update_ts = update_ts; + } + + // Check there are neither active nor upcoming schedules left + if next_update_ts <= block_ts { + // Remove reward from pool info + need_remove = true; + reward_info.rps = Decimal::zero(); + } + } + } + + collected_rewards += reward_info.rps * Decimal::from_ratio(time_passed, 1u8); + + if self.total_lp.is_zero() { + reward_info.orphaned += collected_rewards; + } else { + // Allowing the first depositor to claim orphaned rewards + reward_info.index += (reward_info.orphaned + collected_rewards) + / Decimal::from_ratio(self.total_lp, 1u8); + reward_info.orphaned = Decimal::zero(); + } + + if need_remove { + self.rewards_to_remove.insert( + reward_info.reward.clone(), + (reward_info.index, reward_info.orphaned), + ); + } + } + + // Remove finished rewards. Only external rewards can be removed from PoolInfo. + self.rewards + .retain(|r| !self.rewards_to_remove.contains_key(&r.reward)); + + self.last_update_ts = env.block.time.seconds(); + + Ok(()) + } + + /// This function calculates all rewards for a specific user position. + /// Converts them to [`Asset`]. Returns array of tuples (is_external_reward, Asset). + pub fn calculate_rewards(&self, user_info: &mut UserInfo) -> Vec<(bool, Asset)> { + self.rewards + .iter() + .map(|reward_info| { + let amount = reward_info.calculate_reward(user_info); + ( + reward_info.reward.is_external(), + reward_info.reward.asset_info().with_balance(amount), + ) + }) + .collect() + } + + /// Set astro per second for this pool according to alloc points and general astro per second value + pub fn set_astro_rewards(&mut self, config: &Config, alloc_points: Uint128) { + if let Some(astro_reward_info) = self.rewards.iter_mut().find(|r| !r.reward.is_external()) { + astro_reward_info.rps = Decimal::from_ratio( + config.astro_per_second * alloc_points, + config.total_alloc_points, + ); + } else { + self.rewards.push(RewardInfo { + reward: RewardType::Int(config.astro_token.clone()), + rps: Decimal::from_ratio( + config.astro_per_second * alloc_points, + config.total_alloc_points, + ), + index: Decimal::zero(), + orphaned: Default::default(), + }); + } + } + + /// Check whether this pools receiving ASTRO emissions + pub fn is_active_pool(&self) -> bool { + self.rewards + .iter() + .any(|r| !r.reward.is_external() && !r.rps.is_zero()) + } + + /// This function disables ASTRO rewards in a specific pool. + /// We must keep ASTRO schedule even tho reward per second becomes zero + /// because users still should be able to claim outstanding rewards according to indexes. + pub fn disable_astro_rewards(&mut self) { + if let Some(astro_reward_info) = self.rewards.iter_mut().find(|r| !r.reward.is_external()) { + astro_reward_info.rps = Decimal::zero(); + } + } + + /// Add external reward to a pool. If reward already exists, update its schedule. + /// Complexity O(n + m) = O(m) where n - number of rewards in pool (constant), + /// m - number of schedules that new schedule intersects. + /// The idea is to walk through all schedules and increase them by new reward per second. + /// + /// ## Algorithm description + /// New schedule (start_x, end_x, rps_x). + /// Schedule always takes effect from the current block. + /// rps - rewards per second + /// + /// There are several possible cases. + /// 1. This is a new reward (i.e. no entries matching this reward in PoolInfo.rewards) + /// - Add External(end_x, rps_x) to PoolInfo.rewards array + /// 2. Reward in PoolInfo contains schedule (start_s, rps_s). start_s is point when the next schedule should be picked up. + /// - Add rps_x to current active rps_s; + /// - Fetch all schedules from EXTERNAL_REWARD_SCHEDULES (array of pairs (end_s, rps_s)) where end_s > start_x; + /// - If end_s >= end_x then new schedule is fully covered by the first one. Set point (end_x, rps_s + rps_x); + /// - Otherwise loop over all schedules and update them until end_s >= end_x or until all schedules passed. + pub fn incentivize( + &mut self, + storage: &mut dyn Storage, + lp_asset: &AssetInfo, + schedule: &IncentivesSchedule, + ) -> Result<(), ContractError> { + let ext_rewards_len = self + .rewards + .iter() + .filter(|r| r.reward.is_external()) + .count(); + + let maybe_active_schedule = self.rewards.iter_mut().find( + |r| matches!(&r.reward, RewardType::Ext { info, .. } if info == &schedule.reward_info), + ); + + // Check that we don't exceed the maximum number of reward tokens per pool + if ext_rewards_len == MAX_REWARD_TOKENS as usize && maybe_active_schedule.is_none() { + return Err(ContractError::TooManyRewardTokens { + lp_token: lp_asset.to_string(), + }); + } + + if let Some(active_schedule) = maybe_active_schedule { + let next_update_ts = match &active_schedule.reward { + RewardType::Ext { next_update_ts, .. } => *next_update_ts, + RewardType::Int(_) => { + unreachable!("Only external rewards can be deregistered") + } + }; + + let mut to_save = vec![]; + + if next_update_ts >= schedule.end_ts { + // Newly added schedule is fully covered by the first schedule. + // Set a new break in schedule only if its end is greater + if next_update_ts > schedule.end_ts { + to_save.push((next_update_ts, active_schedule.rps)); + } + + active_schedule.reward = RewardType::Ext { + info: schedule.reward_info.clone(), + next_update_ts: schedule.end_ts, + }; + } else { + // Create iterator starting from schedule.start_ts till the end + let mut overlapping_schedules = EXTERNAL_REWARD_SCHEDULES + .prefix((lp_asset, &schedule.reward_info)) + .range( + storage, + Some(Bound::exclusive(schedule.next_epoch_start_ts)), + None, + Order::Ascending, + ); + + // Add rps to next overlapping schedules. + loop { + if let Some((end_ts, rps_state)) = overlapping_schedules.next().transpose()? { + if end_ts >= schedule.end_ts { + to_save.push((schedule.end_ts, rps_state + schedule.rps)); + break; + } else { + to_save.push((end_ts, rps_state + schedule.rps)); + } + } else { + to_save.push((schedule.end_ts, schedule.rps)); + break; + } + } + }; + + // Update state + for (update_ts, rps) in to_save { + EXTERNAL_REWARD_SCHEDULES.save( + storage, + (lp_asset, &schedule.reward_info, update_ts), + &rps, + )?; + } + + // New schedule anyway hits an active one + active_schedule.rps += schedule.rps; + } else { + self.rewards.push(RewardInfo { + reward: RewardType::Ext { + info: schedule.reward_info.clone(), + next_update_ts: schedule.end_ts, + }, + rps: schedule.rps, + index: Decimal::zero(), + orphaned: Default::default(), + }); + } + + Ok(()) + } + + /// Deregister specific reward from pool. Calculate accrued rewards at this point. Calculate remaining rewards + /// (with those which didn't start yet) and remove upcoming schedules. + /// Complexity is either O(1) or O(m) depending on bypass_upcoming_schedules toggle, + /// where m - number of upcoming schedules. + pub fn deregister_reward( + &mut self, + storage: &mut dyn Storage, + lp_asset: &AssetInfo, + reward_asset: &AssetInfo, + bypass_upcoming_schedules: bool, + ) -> Result { + let (pos, reward_info) = self + .rewards + .iter() + .find_position(|reward| matches!(&reward.reward, RewardType::Ext { info, .. } if info == reward_asset)) + .ok_or_else(|| ContractError::RewardNotFound { pool: lp_asset.to_string(), reward: reward_asset.to_string() })?; + self.rewards_to_remove.insert( + reward_info.reward.clone(), + (reward_info.index, reward_info.orphaned), + ); + let reward_info = self.rewards.remove(pos); + + let next_update_ts = match &reward_info.reward { + RewardType::Ext { next_update_ts, .. } => *next_update_ts, + RewardType::Int(_) => unreachable!("Only external rewards can be deregistered"), + }; + + // Assume update_rewards() was called before + let mut remaining = reward_info.rps + * Decimal::from_ratio(next_update_ts.saturating_sub(self.last_update_ts), 1u8); + + // Remove active schedule from state + EXTERNAL_REWARD_SCHEDULES.remove(storage, (lp_asset, reward_asset, next_update_ts)); + + // If there is too much spam in the state, we can bypass upcoming schedules + if !bypass_upcoming_schedules { + let schedules = EXTERNAL_REWARD_SCHEDULES + .prefix((lp_asset, reward_asset)) + .range( + storage, + Some(Bound::exclusive(next_update_ts)), + None, + Order::Ascending, + ) + .collect::>>()?; + + // Collect future rewards and remove future schedules from state + let mut prev_time = next_update_ts; + schedules + .into_iter() + .for_each(|(update_ts, period_reward_per_sec)| { + if update_ts > next_update_ts { + remaining += + period_reward_per_sec * Decimal::from_ratio(update_ts - prev_time, 1u8); + prev_time = update_ts; + } + + EXTERNAL_REWARD_SCHEDULES.remove(storage, (lp_asset, reward_asset, update_ts)); + }) + } + + // Take orphaned rewards as well + remaining += reward_info.orphaned; + + Ok(remaining.to_uint_floor()) + } + + pub fn load(storage: &dyn Storage, lp_token: &AssetInfo) -> StdResult { + POOLS.load(storage, lp_token) + } + + pub fn may_load(storage: &dyn Storage, lp_token: &AssetInfo) -> StdResult> { + POOLS.may_load(storage, lp_token) + } + + /// Reflect changes to pool info in state. Save finished rewards indexes from in-memory hash map. + /// If reward schedule has orphaned rewards accumulate them in ORPHANED_REWARDS. + /// This function consumes self just to make sure it becomes unusable after calling save(). + pub fn save(self, storage: &mut dyn Storage, lp_token: &AssetInfo) -> StdResult<()> { + if !self.rewards_to_remove.is_empty() { + self.rewards_to_remove + .iter() + .map(|(reward, index)| (reward.asset_info().clone(), *index)) + .group_by(|(_, (_, orphaned_amount))| orphaned_amount.is_zero()) + .into_iter() + .try_for_each(|(is_zero, group)| { + if is_zero { + let finished_indexes = group + .map(|(reward_asset_info, (index, _))| (reward_asset_info, index)) + .collect_vec(); + FINISHED_REWARD_INDEXES.save( + storage, + (lp_token, self.last_update_ts), + &finished_indexes, + ) + } else { + // Processing finished schedules with orphaned rewards + for (reward, (_, orphaned_amount)) in group { + ORPHANED_REWARDS.update::<_, StdError>( + storage, + &asset_info_key(&reward), + |amount| { + Ok(amount.unwrap_or_default() + orphaned_amount.to_uint_floor()) + }, + )?; + } + + Ok(()) + } + })?; + } + + POOLS.save(storage, lp_token, &self) + } + + pub fn into_response(self) -> PoolInfoResponse { + PoolInfoResponse { + total_lp: self.total_lp, + rewards: self.rewards, + last_update_ts: self.last_update_ts, + } + } +} + +/// List all stakers of a specific pool. +pub fn list_pool_stakers( + storage: &dyn Storage, + lp_token: &AssetInfo, + start_after: Option, + limit: Option, +) -> StdResult> { + let start = start_after.as_ref().map(Bound::exclusive); + let limit = limit.unwrap_or(MAX_PAGE_LIMIT).max(MAX_PAGE_LIMIT); + USER_INFO + .prefix(lp_token) + .range(storage, start, None, Order::Ascending) + .take(limit as usize) + .map(|item| item.map(|(user, user_info)| (user, user_info.amount))) + .collect() +} + +/// This structure is for internal use only. +/// Used to add/subtract LP tokens from user position and pool. +pub enum Op { + Add(T), + Sub(T), + Noop, +} + +#[cw_serde] +/// This structure stores user position in a specific pool. +pub struct UserInfo { + /// Amount of LP tokens staked + pub amount: Uint128, + /// Last rewards indexes per reward token + pub last_rewards_index: Vec<(RewardType, Decimal)>, + /// The last time user claimed rewards + pub last_claim_time: u64, +} + +impl UserInfo { + /// Create empty user position with last claim time set to current block time. + pub fn new(env: &Env) -> Self { + Self { + amount: Uint128::zero(), + last_rewards_index: vec![], + last_claim_time: env.block.time.seconds(), + } + } + + /// Loads user position from state. If position doesn't exist returns an error. + /// Can be used in context where position must exist. + pub fn load_position( + storage: &dyn Storage, + user: &Addr, + lp_token: &AssetInfo, + ) -> Result { + Self::may_load_position(storage, user, lp_token)?.ok_or_else(|| { + ContractError::PositionDoesntExist { + user: user.to_string(), + lp_token: lp_token.to_string(), + } + }) + } + + /// Tries to load user position from state. If position doesn't exist returns None. + /// Can be used in context where position may or may not exist. For example, in deposit context. + pub fn may_load_position( + storage: &dyn Storage, + user: &Addr, + lp_token: &AssetInfo, + ) -> StdResult> { + USER_INFO.may_load(storage, (lp_token, user)) + } + + /// Reset user index for all finished rewards. + /// This function is called after processing finished schedules and before processing active + /// schedules for a specific user. + /// The idea is as follows: + /// - get all finished rewards from FINISHED_REWARDS_INDEXES which finished after last time when user claimed rewards + /// - merge them with rewards_to_remove + /// - iterate over all finished rewards and set user index to 0. + pub fn reset_user_index( + &mut self, + storage: &dyn Storage, + lp_token: &AssetInfo, + pool_info: &PoolInfo, + ) -> StdResult<()> { + let mut finished: HashSet<_> = FINISHED_REWARD_INDEXES + .prefix(lp_token) + .range( + storage, + Some(Bound::exclusive(self.last_claim_time)), + None, + Order::Ascending, + ) + .map(|res| res.map(|(_, indexes)| indexes)) + .collect::>>()? + .into_iter() + .flatten() + .map(|(reward_asset, _)| reward_asset) + .collect(); + + finished.extend( + pool_info + .rewards_to_remove + .keys() + .map(|reward| reward.asset_info().clone()), + ); + + for (reward, index) in self.last_rewards_index.iter_mut() { + if reward.is_external() && finished.contains(reward.asset_info()) { + *index = Decimal::zero(); + } + } + + Ok(()) + } + + /// This function calculates all outstanding rewards from finished schedules for a specific user position. + /// The idea is as follows: + /// - get all finished rewards from FINISHED_REWARDS_INDEXES which were deregistered after last claim time + /// - merge them with rewards_to_remove + /// - iterate over all user indexes and find differences. If user doesn't have index for deregistered reward then + /// they never claimed it and their index defaults to 0. + pub fn claim_finished_rewards( + &self, + storage: &dyn Storage, + lp_token: &AssetInfo, + pool_info: &PoolInfo, + ) -> StdResult> { + let finished_iter = FINISHED_REWARD_INDEXES + .prefix(lp_token) + .range( + storage, + Some(Bound::exclusive(self.last_claim_time)), + None, + Order::Ascending, + ) + .map(|res| res.map(|(_, indexes)| indexes)) + .collect::>>()? + .into_iter() + .flatten(); + + let to_remove_iter = pool_info + .rewards_to_remove + .iter() + .map(|(reward, (index, _))| (reward.asset_info().clone(), *index)); + + finished_iter + .chain(to_remove_iter) + .into_group_map_by(|(reward_info, _)| reward_info.clone()) + .into_values() + .flat_map(|indexes_group| { + indexes_group + .into_iter() + .enumerate() + .map(|(i, (reward_info, finished_index))| { + // User could have claimed this reward from the first schedule before it was finished + let amount = if i == 0 { + let user_reward_index = self + .last_rewards_index + .iter() + .filter(|(reward_type, _)| reward_type.is_external()) + .find_map(|(reward_type, index)| { + if reward_type.asset_info() == &reward_info { + Some(*index) + } else { + None + } + }) + .unwrap_or_default(); + + (finished_index - user_reward_index) * self.amount + } else { + // Subsequent finished schedules consider user never claimed rewards + // thus their index was 0 + finished_index * self.amount + }; + + Ok(reward_info.with_balance(amount)) + }) + }) + .collect() + } + + /// Add/remove LP tokens from user position and pool info. + /// Sync reward indexes and set last claim time. + pub fn update_and_sync_position(&mut self, operation: Op, pool_info: &mut PoolInfo) { + match operation { + Op::Add(amount) => { + self.amount += amount; + pool_info.total_lp += amount; + } + Op::Sub(amount) => { + self.amount -= amount; + pool_info.total_lp -= amount; + } + Op::Noop => {} + } + + self.last_rewards_index = pool_info + .rewards + .iter() + .map(|reward_info| (reward_info.reward.clone(), reward_info.index)) + .collect(); + self.last_claim_time = pool_info.last_update_ts; + } + + /// Save user position to state. + /// This function consumes self just to make sure it becomes unusable after calling save(). + pub fn save( + self, + storage: &mut dyn Storage, + user: &Addr, + lp_token: &AssetInfo, + ) -> StdResult<()> { + USER_INFO.save(storage, (lp_token, user), &self) + } + + /// Remove user position from state. + pub fn remove(self, storage: &mut dyn Storage, user: &Addr, lp_token: &AssetInfo) { + USER_INFO.remove(storage, (lp_token, user)) + } +} diff --git a/contracts/tokenomics/incentives/src/traits.rs b/contracts/tokenomics/incentives/src/traits.rs new file mode 100644 index 000000000..1bdfaf87f --- /dev/null +++ b/contracts/tokenomics/incentives/src/traits.rs @@ -0,0 +1,8 @@ +use cosmwasm_std::Uint128; + +use crate::state::UserInfo; + +/// This trait is meant to extend [`astroport::incentives::RewardInfo`]. +pub trait RewardInfoExt { + fn calculate_reward(&self, user_info: &UserInfo) -> Uint128; +} diff --git a/contracts/tokenomics/incentives/src/utils.rs b/contracts/tokenomics/incentives/src/utils.rs new file mode 100644 index 000000000..6ed7d933a --- /dev/null +++ b/contracts/tokenomics/incentives/src/utils.rs @@ -0,0 +1,519 @@ +use cosmwasm_std::{ + attr, ensure, wasm_execute, Addr, Deps, DepsMut, Env, MessageInfo, Order, QuerierWrapper, + ReplyOn, Response, StdError, StdResult, Storage, SubMsg, Uint128, +}; +use itertools::Itertools; + +use astroport::asset::{ + determine_asset_info, pair_info_by_pool, AssetInfo, AssetInfoExt, CoinsExt, PairInfo, +}; +use astroport::factory::PairType; +use astroport::incentives::{Config, IncentivesSchedule, InputSchedule, MAX_ORPHANED_REWARD_LIMIT}; +use astroport::{factory, pair, vesting}; + +use crate::error::ContractError; +use crate::reply::POST_TRANSFER_REPLY_ID; +use crate::state::{ + Op, PoolInfo, UserInfo, ACTIVE_POOLS, BLOCKED_TOKENS, CONFIG, ORPHANED_REWARDS, +}; + +/// Claim all rewards and compose [`Response`] object containing all attributes and messages. +/// This function doesn't mutate the state but mutates in-memory objects. +/// Function caller is responsible for updating the state. +/// If vesting_contract is None this function reads config from state and gets vesting address. +pub fn claim_rewards( + storage: &dyn Storage, + vesting_contract: Option, + env: Env, + user: &Addr, + pool_tuples: Vec<(&AssetInfo, &mut PoolInfo, &mut UserInfo)>, +) -> Result { + let mut attrs = vec![attr("action", "claim_rewards"), attr("user", user)]; + let mut external_rewards = vec![]; + let mut protocol_reward_amount = Uint128::zero(); + for (lp_token_asset, pool_info, pos) in pool_tuples { + attrs.push(attr("claimed_position", lp_token_asset.to_string())); + + pool_info.update_rewards(storage, &env, lp_token_asset)?; + + // Claim outstanding rewards from finished schedules + for finished_reward in pos.claim_finished_rewards(storage, lp_token_asset, pool_info)? { + if !finished_reward.amount.is_zero() { + attrs.push(attr("claimed_finished_reward", finished_reward.to_string())); + external_rewards.push(finished_reward); + } + } + + // Reset user reward index for all finished schedules + pos.reset_user_index(storage, lp_token_asset, pool_info)?; + + for (is_external, reward_asset) in pool_info.calculate_rewards(pos) { + attrs.push(attr("claimed_reward", reward_asset.to_string())); + + if !reward_asset.amount.is_zero() { + if is_external { + external_rewards.push(reward_asset); + } else { + protocol_reward_amount += reward_asset.amount; + } + } + } + + // Sync user index with pool index. It removes all finished schedules from user info. + pos.update_and_sync_position(Op::Noop, pool_info); + } + + // Aggregating rewards by asset info. + // This allows to reduce number of output messages thus reducing total gas cost. + let mut messages = external_rewards + .into_iter() + .group_by(|asset| asset.info.clone()) + .into_iter() + .map(|(info, assets)| { + let amount: Uint128 = assets.into_iter().map(|asset| asset.amount).sum(); + info.with_balance(amount) + .into_submsg(user, Some((ReplyOn::Error, POST_TRANSFER_REPLY_ID))) + }) + .collect::>>()?; + + // Claim Astroport rewards + if !protocol_reward_amount.is_zero() { + let vesting_contract = if let Some(vesting_contract) = vesting_contract { + vesting_contract + } else { + CONFIG.load(storage)?.vesting_contract + }; + messages.push(SubMsg::new(wasm_execute( + vesting_contract, + &vesting::ExecuteMsg::Claim { + recipient: Some(user.to_string()), + amount: Some(protocol_reward_amount), + }, + vec![], + )?)); + } + + Ok(Response::new() + .add_attributes(attrs) + .add_submessages(messages)) +} + +/// Only factory can set the allocation points to zero for the specified pool. +/// Called from deregistration context in factory. +pub fn deactivate_pool( + deps: DepsMut, + info: MessageInfo, + env: Env, + lp_token: String, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.factory { + return Err(ContractError::Unauthorized {}); + } + + let lp_token_asset = determine_asset_info(&lp_token, deps.api)?; + + match PoolInfo::may_load(deps.storage, &lp_token_asset)? { + Some(mut pool_info) if pool_info.is_active_pool() => { + let mut active_pools = ACTIVE_POOLS.load(deps.storage)?; + + let (ind, _) = active_pools + .iter() + .find_position(|(lp_asset, _)| lp_asset == &lp_token_asset) + .unwrap(); + let (_, alloc_points) = active_pools.swap_remove(ind); + + pool_info.update_rewards(deps.storage, &env, &lp_token_asset)?; + pool_info.disable_astro_rewards(); + pool_info.save(deps.storage, &lp_token_asset)?; + + config.total_alloc_points = config.total_alloc_points.checked_sub(alloc_points)?; + + for (lp_asset, alloc_points) in &active_pools { + let mut pool_info = PoolInfo::load(deps.storage, lp_asset)?; + pool_info.update_rewards(deps.storage, &env, lp_asset)?; + pool_info.set_astro_rewards(&config, *alloc_points); + pool_info.save(deps.storage, lp_asset)?; + } + + ACTIVE_POOLS.save(deps.storage, &active_pools)?; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attributes([ + attr("action", "deactivate_pool"), + attr("lp_token", lp_token), + ])) + } + _ => Ok(Response::new()), + } +} + +/// Removes pools from active pools if their pair type is blocked. +pub fn deactivate_blocked_pools(deps: DepsMut, env: Env) -> Result { + let mut response = Response::new(); + let mut active_pools = ACTIVE_POOLS.load(deps.storage)?; + let mut config = CONFIG.load(deps.storage)?; + + let blocked_pair_types: Vec = deps + .querier + .query_wasm_smart(&config.factory, &factory::QueryMsg::BlacklistedPairTypes {})?; + + let mut to_remove = vec![]; + + for (lp_token_asset, alloc_points) in &active_pools { + let mut pool_info = PoolInfo::load(deps.storage, lp_token_asset)?; + + let pair_info = query_pair_info(deps.as_ref(), lp_token_asset)?; + + // check if pair type is blocked + if blocked_pair_types.contains(&pair_info.pair_type) { + pool_info.update_rewards(deps.storage, &env, lp_token_asset)?; + pool_info.disable_astro_rewards(); + pool_info.save(deps.storage, lp_token_asset)?; + + config.total_alloc_points = config.total_alloc_points.checked_sub(*alloc_points)?; + + to_remove.push(lp_token_asset.clone()); + + response.attributes.extend([ + attr("action", "deactivate_pool"), + attr("lp_token", lp_token_asset.to_string()), + ]); + } + } + + if !to_remove.is_empty() { + active_pools.retain(|(lp_token_asset, _)| !to_remove.contains(lp_token_asset)); + + for (lp_asset, alloc_points) in &active_pools { + let mut pool_info = PoolInfo::load(deps.storage, lp_asset)?; + pool_info.update_rewards(deps.storage, &env, lp_asset)?; + pool_info.set_astro_rewards(&config, *alloc_points); + pool_info.save(deps.storage, lp_asset)?; + } + + ACTIVE_POOLS.save(deps.storage, &active_pools)?; + CONFIG.save(deps.storage, &config)?; + } + + Ok(response) +} + +pub fn incentivize( + deps: DepsMut, + info: MessageInfo, + env: Env, + lp_token: String, + input: InputSchedule, +) -> Result { + let schedule = IncentivesSchedule::from_input(&env, &input)?; + + let mut response = Response::new().add_attributes([ + attr("action", "incentivize"), + attr("lp_token", lp_token.clone()), + attr("start_ts", env.block.time.seconds().to_string()), + attr("end_ts", schedule.end_ts.to_string()), + attr("reward", schedule.reward_info.to_string()), + ]); + + let lp_token_asset = determine_asset_info(&lp_token, deps.api)?; + + // Prohibit reward schedules with blocked token + if BLOCKED_TOKENS.has(deps.storage, &asset_info_key(&schedule.reward_info)) { + return Err(ContractError::BlockedToken { + token: schedule.reward_info.to_string(), + }); + } + + let pair_info = query_pair_info(deps.as_ref(), &lp_token_asset)?; + let config = CONFIG.load(deps.storage)?; + is_pool_registered(deps.querier, &config, &pair_info, &lp_token)?; + + let mut pool_info = PoolInfo::may_load(deps.storage, &lp_token_asset)?.unwrap_or_default(); + pool_info.update_rewards(deps.storage, &env, &lp_token_asset)?; + + let rewards_number_before = pool_info.rewards.len(); + pool_info.incentivize(deps.storage, &lp_token_asset, &schedule)?; + + let mut funds = info.funds.clone(); + + // Check whether this is a new external reward token. + // 3rd parties are encouraged to keep endless schedules without breaks even with the small rewards. + // Otherwise, reward token will be removed from the pool info and go to outstanding rewards. + // Next schedules with the same token will be considered as "new". + if rewards_number_before < pool_info.rewards.len() { + // If fee set we expect to receive it + if let Some(incentivization_fee_info) = &config.incentivization_fee_info { + let fee_coin_pos = funds + .iter() + .find_position(|coin| coin.denom == incentivization_fee_info.fee.denom); + if let Some((ind, fee_coin)) = fee_coin_pos { + // Mutate funds array so we can assert below that reward coins properly sent + funds[ind].amount = fee_coin + .amount + .checked_sub(incentivization_fee_info.fee.amount) + .map_err(|_| ContractError::IncentivizationFeeExpected { + fee: incentivization_fee_info.fee.to_string(), + lp_token, + new_reward_token: schedule.reward_info.to_string(), + })?; + if funds[ind].amount.is_zero() { + funds.remove(ind); + } + } else { + return Err(ContractError::IncentivizationFeeExpected { + fee: incentivization_fee_info.fee.to_string(), + lp_token, + new_reward_token: schedule.reward_info.to_string(), + }); + } + } + } + + // Assert that we received reward tokens + match &schedule.reward_info { + AssetInfo::Token { contract_addr } => { + response = response.add_message(wasm_execute( + contract_addr, + &cw20::Cw20ExecuteMsg::TransferFrom { + owner: info.sender.to_string(), + recipient: env.contract.address.to_string(), + amount: input.reward.amount, + }, + vec![], + )?); + } + AssetInfo::NativeToken { .. } => { + funds.assert_coins_properly_sent(&[input.reward], &[schedule.reward_info.clone()])? + } + } + + pool_info.save(deps.storage, &lp_token_asset)?; + + Ok(response) +} + +pub fn remove_reward_from_pool( + deps: DepsMut, + info: MessageInfo, + env: Env, + lp_token: String, + reward: String, + bypass_upcoming_schedules: bool, + receiver: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let lp_asset = determine_asset_info(&lp_token, deps.api)?; + let reward_asset = determine_asset_info(&reward, deps.api)?; + + let mut pool_info = PoolInfo::load(deps.storage, &lp_asset)?; + pool_info.update_rewards(deps.storage, &env, &lp_asset)?; + let unclaimed = pool_info.deregister_reward( + deps.storage, + &lp_asset, + &reward_asset, + bypass_upcoming_schedules, + )?; + + pool_info.save(deps.storage, &lp_asset)?; + + let mut response = Response::new(); + + // Send unclaimed rewards + if !unclaimed.is_zero() { + deps.api.addr_validate(&receiver)?; + let transfer_msg = reward_asset + .with_balance(unclaimed) + .into_submsg(receiver, Some((ReplyOn::Error, POST_TRANSFER_REPLY_ID)))?; + response = response.add_submessage(transfer_msg); + } + + Ok(response.add_attributes([ + attr("action", "remove_reward_from_pool"), + attr("lp_token", lp_token), + attr("reward", reward), + ])) +} + +/// Queries pair info corresponding to given LP token. +/// Handles both native and cw20 tokens. If the token is native it must follow the following format: +/// factory/{lp_minter}/{token_name} where lp_minter is a valid bech32 address on the current chain. +pub fn query_pair_info(deps: Deps, lp_asset: &AssetInfo) -> StdResult { + match lp_asset { + AssetInfo::Token { contract_addr } => pair_info_by_pool(&deps.querier, contract_addr), + AssetInfo::NativeToken { denom } => { + let parts = denom.split('/').collect_vec(); + if denom.starts_with("factory") && parts.len() >= 3 { + let lp_minter = parts[1]; + deps.api.addr_validate(lp_minter)?; + deps.querier + .query_wasm_smart(lp_minter, &pair::QueryMsg::Pair {}) + } else { + Err(StdError::generic_err(format!( + "LP token {denom} doesn't follow token factory format: factory/{{lp_minter}}/{{token_name}}", + ))) + } + } + } +} + +/// Checks if the pool with the following asset infos is registered in the factory contract and +/// LP tokens address/denom matches the one registered in the factory. +pub fn is_pool_registered( + querier: QuerierWrapper, + config: &Config, + pair_info: &PairInfo, + lp_token_addr: &str, +) -> StdResult<()> { + querier + .query_wasm_smart::( + &config.factory, + &factory::QueryMsg::Pair { + asset_infos: pair_info.asset_infos.to_vec(), + }, + ) + .map_err(|_| { + StdError::generic_err(format!( + "The pair is not registered: {}-{}", + pair_info.asset_infos[0], pair_info.asset_infos[1] + )) + }) + .map(|resp| { + // Eventually resp.liquidity_token will become just a String once token factory LP tokens are implemented + if resp.liquidity_token.as_str() == lp_token_addr { + Ok(()) + } else { + Err(StdError::generic_err(format!( + "LP token {lp_token_addr} doesn't match LP token registered in factory {}", + resp.liquidity_token + ))) + } + })? +} + +pub fn claim_orphaned_rewards( + deps: DepsMut, + info: MessageInfo, + limit: Option, + receiver: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); + + let receiver = deps.api.addr_validate(&receiver)?; + let limit = limit + .unwrap_or(MAX_ORPHANED_REWARD_LIMIT) + .min(MAX_ORPHANED_REWARD_LIMIT); + + let orphaned_rewards = ORPHANED_REWARDS + .range(deps.storage, None, None, Order::Ascending) + .take(limit as usize) + .collect::>>()?; + + if orphaned_rewards.is_empty() { + return Err(ContractError::NoOrphanedRewards {}); + } + + let mut messages = vec![]; + let mut attrs = vec![ + attr("action", "claim_orphaned_rewards"), + attr("receiver", &receiver), + ]; + + for (reward_info_binary, amount) in orphaned_rewards { + // Send orphaned rewards + if !amount.is_zero() { + ORPHANED_REWARDS.remove(deps.storage, &reward_info_binary); + + let reward_info = from_key_to_asset_info(reward_info_binary)?; + let reward_asset = reward_info.with_balance(amount); + + attrs.push(attr("claimed_orphaned_reward", reward_asset.to_string())); + + let transfer_msg = reward_asset + .into_submsg(&receiver, Some((ReplyOn::Error, POST_TRANSFER_REPLY_ID)))?; + messages.push(transfer_msg); + } + } + + Ok(Response::new().add_submessages(messages)) +} + +pub fn asset_info_key(asset_info: &AssetInfo) -> Vec { + let mut bytes = vec![]; + match asset_info { + AssetInfo::NativeToken { denom } => { + bytes.push(0); + bytes.extend_from_slice(denom.as_bytes()); + } + AssetInfo::Token { contract_addr } => { + bytes.push(1); + bytes.extend_from_slice(contract_addr.as_bytes()); + } + } + + bytes +} + +pub fn from_key_to_asset_info(bytes: Vec) -> StdResult { + match bytes[0] { + 0 => String::from_utf8(bytes[1..].to_vec()) + .map_err(StdError::invalid_utf8) + .map(AssetInfo::native), + 1 => String::from_utf8(bytes[1..].to_vec()) + .map_err(StdError::invalid_utf8) + .map(AssetInfo::cw20_unchecked), + _ => Err(StdError::generic_err( + "Failed to deserialize asset info key", + )), + } +} + +#[cfg(test)] +mod unit_tests { + use astroport::asset::AssetInfo; + + use super::*; + + #[test] + fn test_asset_info_binary_key() { + let asset_infos = vec![ + AssetInfo::native("uusd"), + AssetInfo::cw20_unchecked("wasm1contractxxx"), + ]; + + for asset_info in asset_infos { + let key = asset_info_key(&asset_info); + assert_eq!(from_key_to_asset_info(key).unwrap(), asset_info); + } + } + + #[test] + fn test_deserialize_asset_info_from_malformed_data() { + let asset_infos = vec![ + AssetInfo::native("uusd"), + AssetInfo::cw20_unchecked("wasm1contractxxx"), + ]; + + for asset_info in asset_infos { + let mut key = asset_info_key(&asset_info); + key[0] = 2; + + assert_eq!( + from_key_to_asset_info(key).unwrap_err(), + StdError::generic_err("Failed to deserialize asset info key") + ); + } + + let key = vec![0, u8::MAX]; + assert_eq!( + from_key_to_asset_info(key).unwrap_err().to_string(), + "Cannot decode UTF8 bytes into string: invalid utf-8 sequence of 1 bytes from index 0" + ); + } +} diff --git a/contracts/tokenomics/incentives/tests/helper/broken_cw20.rs b/contracts/tokenomics/incentives/tests/helper/broken_cw20.rs new file mode 100644 index 000000000..db39eecbc --- /dev/null +++ b/contracts/tokenomics/incentives/tests/helper/broken_cw20.rs @@ -0,0 +1,60 @@ +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdError}; +use cw20_base::allowances::{ + execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, + execute_transfer_from, +}; +use cw20_base::contract::{ + execute_burn, execute_mint, execute_send, execute_update_marketing, execute_update_minter, + execute_upload_logo, +}; +use cw20_base::msg::ExecuteMsg; +use cw20_base::ContractError; + +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Transfer { .. } => return Err(StdError::generic_err("Haha").into()), + ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount), + ExecuteMsg::Send { + contract, + amount, + msg, + } => execute_send(deps, env, info, contract, amount, msg), + ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount), + ExecuteMsg::IncreaseAllowance { + spender, + amount, + expires, + } => execute_increase_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::DecreaseAllowance { + spender, + amount, + expires, + } => execute_decrease_allowance(deps, env, info, spender, amount, expires), + ExecuteMsg::TransferFrom { + owner, + recipient, + amount, + } => execute_transfer_from(deps, env, info, owner, recipient, amount), + ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount), + ExecuteMsg::SendFrom { + owner, + contract, + amount, + msg, + } => execute_send_from(deps, env, info, owner, contract, amount, msg), + ExecuteMsg::UpdateMarketing { + project, + description, + marketing, + } => execute_update_marketing(deps, env, info, project, description, marketing), + ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo), + ExecuteMsg::UpdateMinter { new_minter } => { + execute_update_minter(deps, env, info, new_minter) + } + } +} diff --git a/contracts/tokenomics/incentives/tests/helper/helper.rs b/contracts/tokenomics/incentives/tests/helper/helper.rs new file mode 100644 index 000000000..86cd244bb --- /dev/null +++ b/contracts/tokenomics/incentives/tests/helper/helper.rs @@ -0,0 +1,1020 @@ +#![allow(dead_code)] + +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +use anyhow::Result as AnyResult; +use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; +use cosmwasm_std::{ + to_binary, Addr, Api, BlockInfo, CanonicalAddr, Coin, Empty, Env, IbcMsg, IbcQuery, + RecoverPubkeyError, StdError, StdResult, Storage, Timestamp, Uint128, VerificationError, +}; +use cw20::MinterResponse; +use cw_multi_test::{ + AddressGenerator, App, AppBuilder, AppResponse, BankKeeper, Contract, ContractWrapper, + DistributionKeeper, Executor, FailingModule, StakeKeeper, WasmKeeper, +}; +use itertools::Itertools; + +use crate::helper::broken_cw20; +use astroport::asset::{Asset, AssetInfo, AssetInfoExt, PairInfo}; +use astroport::factory::{PairConfig, PairType}; +use astroport::incentives::{ + Config, ExecuteMsg, IncentivesSchedule, IncentivizationFeeInfo, InputSchedule, + PoolInfoResponse, QueryMsg, RewardInfo, ScheduleResponse, +}; +use astroport::pair::StablePoolParams; +use astroport::vesting::{VestingAccount, VestingSchedule, VestingSchedulePoint}; +use astroport::{factory, native_coin_registry, pair, vesting}; + +fn factory_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ) +} + +fn pair_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ) +} + +fn pair_stable_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_pair_stable::contract::execute, + astroport_pair_stable::contract::instantiate, + astroport_pair_stable::contract::query, + ) + .with_reply_empty(astroport_pair_stable::contract::reply), + ) +} + +fn coin_registry_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + astroport_native_coin_registry::contract::execute, + astroport_native_coin_registry::contract::instantiate, + astroport_native_coin_registry::contract::query, + )) +} + +fn vesting_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + astroport_vesting::contract::execute, + astroport_vesting::contract::instantiate, + astroport_vesting::contract::query, + )) +} + +fn token_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )) +} + +fn broken_token_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + broken_cw20::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + )) +} + +fn generator_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_incentives::execute::execute, + astroport_incentives::instantiate::instantiate, + astroport_incentives::query::query, + ) + .with_reply_empty(astroport_incentives::reply::reply), + ) +} + +pub struct TestApi { + mock_api: MockApi, +} + +impl TestApi { + pub fn new() -> Self { + Self { + mock_api: MockApi::default(), + } + } +} + +impl Api for TestApi { + fn addr_validate(&self, input: &str) -> StdResult { + if input.starts_with(TestAddr::ADDR_PREFIX) { + self.mock_api.addr_validate(input) + } else { + Err(StdError::generic_err(format!( + "TestApi: address {input} does not start with {}", + TestAddr::ADDR_PREFIX + ))) + } + } + + fn addr_canonicalize(&self, human: &str) -> StdResult { + self.mock_api.addr_canonicalize(human) + } + + fn addr_humanize(&self, canonical: &CanonicalAddr) -> StdResult { + self.mock_api.addr_humanize(canonical) + } + + fn secp256k1_verify( + &self, + message_hash: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.mock_api + .secp256k1_verify(message_hash, signature, public_key) + } + + fn secp256k1_recover_pubkey( + &self, + message_hash: &[u8], + signature: &[u8], + recovery_param: u8, + ) -> Result, RecoverPubkeyError> { + self.mock_api + .secp256k1_recover_pubkey(message_hash, signature, recovery_param) + } + + fn ed25519_verify( + &self, + message: &[u8], + signature: &[u8], + public_key: &[u8], + ) -> Result { + self.mock_api.ed25519_verify(message, signature, public_key) + } + + fn ed25519_batch_verify( + &self, + messages: &[&[u8]], + signatures: &[&[u8]], + public_keys: &[&[u8]], + ) -> Result { + self.mock_api + .ed25519_batch_verify(messages, signatures, public_keys) + } + + fn debug(&self, message: &str) { + self.mock_api.debug(message) + } +} + +pub struct TestAddr; + +impl TestAddr { + pub const ADDR_PREFIX: &'static str = "wasm1"; + pub const COUNT_KEY: &'static [u8] = b"address_count"; + + pub fn new(seed: &str) -> Addr { + Addr::unchecked(format!("{}_{seed}", Self::ADDR_PREFIX)) + } +} + +impl AddressGenerator for TestAddr { + fn next_address(&self, storage: &mut dyn Storage) -> Addr { + let count = if let Some(next) = storage.get(Self::COUNT_KEY) { + u64::from_be_bytes(next.as_slice().try_into().unwrap()) + 1 + } else { + 1u64 + }; + storage.set(Self::COUNT_KEY, &count.to_be_bytes()); + + Addr::unchecked(format!("{}_contract{count}", Self::ADDR_PREFIX)) + } +} + +pub type TestApp = App< + BankKeeper, + TestApi, + MockStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, +>; + +pub struct Helper { + pub app: TestApp, + pub owner: Addr, + pub factory: Addr, + pub vesting: Addr, + pub generator: Addr, + pub coin_registry: Addr, + pub token_code_id: u64, + pub incentivization_fee: Coin, +} + +impl Helper { + pub fn new(owner: &str, astro: &AssetInfo) -> AnyResult { + let mut app = AppBuilder::new() + .with_wasm::, WasmKeeper<_, _>>( + WasmKeeper::new_with_custom_address_generator(TestAddr), + ) + .with_api(TestApi::new()) + .with_block(BlockInfo { + height: 1, + time: Timestamp::from_seconds(1696810000), + chain_id: "cw-multitest-1".to_string(), + }) + .build(|_, _, _| {}); + let owner = TestAddr::new(owner); + + let vesting_code = app.store_code(vesting_contract()); + let vesting = app + .instantiate_contract( + vesting_code, + owner.clone(), + &vesting::InstantiateMsg { + owner: owner.to_string(), + vesting_token: astro.clone(), + }, + &[], + "Astroport Vesting", + None, + ) + .unwrap(); + + let coin_registry_address_code = app.store_code(coin_registry_contract()); + let coin_registry_address = app + .instantiate_contract( + coin_registry_address_code, + owner.clone(), + &native_coin_registry::InstantiateMsg { + owner: owner.to_string(), + }, + &[], + "Astroport Coin Registry", + None, + ) + .unwrap(); + + let factory_code = app.store_code(factory_contract()); + let token_code_id = app.store_code(token_contract()); + let pair_code = app.store_code(pair_contract()); + let pair_stable_code = app.store_code(pair_stable_contract()); + let factory = app + .instantiate_contract( + factory_code, + owner.clone(), + &factory::InstantiateMsg { + pair_configs: vec![ + PairConfig { + code_id: pair_code, + pair_type: PairType::Xyk {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + }, + PairConfig { + code_id: pair_stable_code, + pair_type: PairType::Stable {}, + total_fee_bps: 0, + maker_fee_bps: 0, + is_disabled: false, + is_generator_disabled: false, + }, + ], + token_code_id, + fee_address: None, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id: 0, + coin_registry_address: coin_registry_address.to_string(), + }, + &[], + "Astroport Factory", + None, + ) + .unwrap(); + + let incentivization_fee = astro + .with_balance(10_000_000000u128) + .as_coin() + .expect("Test suite supports only native ASTRO"); + + let generator_code = app.store_code(generator_contract()); + let generator = app + .instantiate_contract( + generator_code, + owner.clone(), + &astroport::incentives::InstantiateMsg { + owner: owner.to_string(), + factory: factory.to_string(), + astro_token: astro.clone(), + vesting_contract: vesting.to_string(), + incentivization_fee_info: Some(IncentivizationFeeInfo { + fee_receiver: TestAddr::new("maker"), + fee: incentivization_fee.clone(), + }), + guardian: Some(TestAddr::new("guardian").to_string()), + }, + &[], + "Astroport Generator", + None, + ) + .unwrap(); + + app.execute_contract( + owner.clone(), + factory.clone(), + &factory::ExecuteMsg::UpdateConfig { + token_code_id: None, + fee_address: None, + generator_address: Some(generator.to_string()), + whitelist_code_id: None, + coin_registry_address: None, + }, + &[], + ) + .unwrap(); + + let astro_for_vesting = astro.with_balance(u128::MAX).as_coin().unwrap(); + app.init_modules(|router, _, storage| { + router + .bank + .init_balance(storage, &owner, vec![astro_for_vesting.clone()]) + }) + .unwrap(); + app.execute_contract( + owner.clone(), + vesting.clone(), + &vesting::ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: generator.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: app.block_info().time.seconds(), + amount: astro_for_vesting.amount, + }, + end_point: None, + }], + }], + }, + &[astro_for_vesting], + ) + .unwrap(); + + Ok(Self { + app, + owner, + factory, + vesting, + generator, + coin_registry: coin_registry_address, + token_code_id, + incentivization_fee, + }) + } + + pub fn stake(&mut self, from: &Addr, lp_asset: Asset) -> AnyResult { + match &lp_asset.info { + AssetInfo::Token { contract_addr } => self.app.execute_contract( + from.clone(), + contract_addr.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: self.generator.to_string(), + amount: lp_asset.amount, + msg: to_binary(&ExecuteMsg::Deposit { recipient: None }).unwrap(), + }, + &[], + ), + AssetInfo::NativeToken { .. } => self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::Deposit { recipient: None }, + &[lp_asset.as_coin().unwrap()], + ), + } + } + + pub fn unstake( + &mut self, + from: &Addr, + lp_token: &str, + amount: impl Into, + ) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::Withdraw { + lp_token: lp_token.to_string(), + amount: amount.into(), + }, + &[], + ) + } + + pub fn setup_pools(&mut self, pools: Vec<(String, u128)>) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.generator.clone(), + &ExecuteMsg::SetupPools { + pools: pools + .into_iter() + .map(|(pool, amount)| (pool, amount.into())) + .collect(), + }, + &[], + ) + } + + pub fn deactivate_pool(&mut self, from: &Addr, lp_token: &str) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::DeactivatePool { + lp_token: lp_token.to_string(), + }, + &[], + ) + } + + pub fn deactivate_pool_full_flow( + &mut self, + asset_infos: &[AssetInfo], + ) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.factory.clone(), + &factory::ExecuteMsg::Deregister { + asset_infos: asset_infos.to_vec(), + }, + &[], + ) + } + + pub fn block_tokens(&mut self, from: &Addr, tokens: &[AssetInfo]) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::UpdateBlockedTokenslist { + add: tokens.to_vec(), + remove: vec![], + }, + &[], + ) + } + + pub fn unblock_tokens(&mut self, from: &Addr, tokens: &[AssetInfo]) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::UpdateBlockedTokenslist { + add: vec![], + remove: tokens.to_vec(), + }, + &[], + ) + } + + pub fn update_blocklist( + &mut self, + from: &Addr, + add: &[AssetInfo], + remove: &[AssetInfo], + ) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::UpdateBlockedTokenslist { + add: add.to_vec(), + remove: remove.to_vec(), + }, + &[], + ) + } + + pub fn block_pair_type(&mut self, from: &Addr, pair_type: PairType) -> AnyResult { + let pair_config = self + .app + .wrap() + .query_wasm_smart::( + &self.factory, + &factory::QueryMsg::Config {}, + ) + .unwrap() + .pair_configs + .into_iter() + .find(|c| c.pair_type == pair_type) + .unwrap(); + + self.app.execute_contract( + from.clone(), + self.factory.clone(), + &factory::ExecuteMsg::UpdatePairConfig { + config: PairConfig { + is_generator_disabled: true, + ..pair_config + }, + }, + &[], + ) + } + + pub fn deactivate_blocked(&mut self) -> AnyResult { + self.app.execute_contract( + Addr::unchecked("permissionless"), + self.generator.clone(), + &ExecuteMsg::DeactivateBlockedPools {}, + &[], + ) + } + + pub fn set_tokens_per_second(&mut self, amount: u128) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.generator.clone(), + &ExecuteMsg::SetTokensPerSecond { + amount: amount.into(), + }, + &[], + ) + } + + pub fn create_schedule( + &self, + asset: &Asset, + duration_periods: u64, + ) -> AnyResult<(InputSchedule, IncentivesSchedule)> { + let env = Env { + block: self.app.block_info(), + ..mock_env() + }; + + let input = InputSchedule { + reward: asset.clone(), + duration_periods, + }; + let sch = IncentivesSchedule::from_input(&env, &input)?; + + Ok((input, sch)) + } + + pub fn init_cw20(&mut self, name: &str, decimals: Option) -> Addr { + self.app + .instantiate_contract( + self.token_code_id, + self.owner.clone(), + &cw20_base::msg::InstantiateMsg { + name: name.to_string(), + symbol: name.to_string(), + decimals: decimals.unwrap_or(6), + initial_balances: vec![], + mint: Some(MinterResponse { + minter: self.owner.to_string(), + cap: None, + }), + marketing: None, + }, + &[], + name, + None, + ) + .unwrap() + } + + pub fn init_broken_cw20(&mut self, name: &str, decimals: Option) -> Addr { + let broken_cw20_code = self.app.store_code(broken_token_contract()); + self.app + .instantiate_contract( + broken_cw20_code, + self.owner.clone(), + &cw20_base::msg::InstantiateMsg { + name: name.to_string(), + symbol: name.to_string(), + decimals: decimals.unwrap_or(6), + initial_balances: vec![], + mint: Some(MinterResponse { + minter: self.owner.to_string(), + cap: None, + }), + marketing: None, + }, + &[], + name, + None, + ) + .unwrap() + } + + pub fn incentivize( + &mut self, + from: &Addr, + lp_token: &str, + schedule: InputSchedule, + attach_funds: &[Coin], + ) -> AnyResult { + let mut funds = HashMap::new(); + match &schedule.reward.info { + AssetInfo::Token { contract_addr } => { + self.app + .execute_contract( + from.clone(), + contract_addr.clone(), + &cw20::Cw20ExecuteMsg::IncreaseAllowance { + spender: self.generator.to_string(), + amount: schedule.reward.amount, + expires: None, + }, + &[], + ) + .unwrap(); + } + AssetInfo::NativeToken { .. } => { + let coin = schedule.reward.as_coin().unwrap(); + funds.insert(coin.denom.clone(), coin); + } + } + for coin in attach_funds { + match funds.entry(coin.denom.clone()) { + Entry::Occupied(mut entry) => { + entry.get_mut().amount += coin.amount; + } + Entry::Vacant(entry) => { + entry.insert(coin.clone()); + } + } + } + let funds = funds.values().cloned().collect_vec(); + + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::Incentivize { + lp_token: lp_token.to_string(), + schedule, + }, + &funds, + ) + } + + pub fn remove_reward( + &mut self, + from: &Addr, + lp_token: &str, + reward: &str, + bypass_upcoming_schedules: bool, + receiver: &Addr, + ) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::RemoveRewardFromPool { + lp_token: lp_token.to_string(), + reward: reward.to_string(), + bypass_upcoming_schedules, + receiver: receiver.to_string(), + }, + &[], + ) + } + + pub fn claim_orphaned_rewards( + &mut self, + limit: Option, + receiver: impl Into, + ) -> AnyResult { + self.app.execute_contract( + self.owner.clone(), + self.generator.clone(), + &ExecuteMsg::ClaimOrphanedRewards { + limit, + receiver: receiver.into(), + }, + &[], + ) + } + + pub fn claim_rewards(&mut self, from: &Addr, lp_tokens: Vec) -> AnyResult { + self.app.execute_contract( + from.clone(), + self.generator.clone(), + &ExecuteMsg::ClaimRewards { lp_tokens }, + &[], + ) + } + + pub fn next_block(&mut self, plus_seconds: u64) { + self.app.update_block(|block| { + block.time = block.time.plus_seconds(plus_seconds); + block.height += 1 + }) + } + + pub fn mint_assets(&mut self, to: &Addr, assets: &[Asset]) { + for asset in assets { + match &asset.info { + AssetInfo::Token { contract_addr } => { + self.app + .execute_contract( + self.owner.clone(), + contract_addr.clone(), + &cw20::Cw20ExecuteMsg::Mint { + recipient: to.to_string(), + amount: asset.amount, + }, + &[], + ) + .unwrap(); + } + AssetInfo::NativeToken { .. } => { + self.mint_coin(to, &asset.as_coin().unwrap()); + } + } + } + } + + pub fn mint_coin(&mut self, to: &Addr, coin: &Coin) { + // .init_balance() erases previous balance thus I use such hack and create intermediate "denom admin" + let denom_admin = Addr::unchecked(format!("{}_admin", &coin.denom)); + self.app + .init_modules(|router, _, storage| { + router + .bank + .init_balance(storage, &denom_admin, vec![coin.clone()]) + }) + .unwrap(); + + self.app + .send_tokens(denom_admin, to.clone(), &[coin.clone()]) + .unwrap(); + } + + pub fn query_pair_info(&self, asset_infos: &[AssetInfo]) -> PairInfo { + self.app + .wrap() + .query_wasm_smart( + &self.factory, + &factory::QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + ) + .unwrap() + } + + pub fn query_pending_rewards(&self, user: &Addr, lp_token: &str) -> Vec { + self.app + .wrap() + .query_wasm_smart( + &self.generator, + &QueryMsg::PendingRewards { + lp_token: lp_token.to_string(), + user: user.to_string(), + }, + ) + .unwrap() + } + + pub fn query_config(&self) -> Config { + self.app + .wrap() + .query_wasm_smart(&self.generator, &QueryMsg::Config {}) + .unwrap() + } + + pub fn query_deposit(&self, lp_token: &str, user: &Addr) -> StdResult { + self.app + .wrap() + .query_wasm_smart::( + &self.generator, + &QueryMsg::Deposit { + lp_token: lp_token.to_string(), + user: user.to_string(), + }, + ) + .map(|x| x.u128()) + } + + pub fn is_fee_needed(&self, lp_token: &str, reward: &AssetInfo) -> bool { + self.app + .wrap() + .query_wasm_smart::( + &self.generator, + &QueryMsg::IsFeeExpected { + lp_token: lp_token.to_string(), + reward: reward.to_string(), + }, + ) + .unwrap() + } + + pub fn query_ext_reward_schedules( + &self, + lp_token: &str, + reward: &AssetInfo, + start_after: Option, + limit: Option, + ) -> StdResult> { + self.app.wrap().query_wasm_smart( + &self.generator, + &QueryMsg::ExternalRewardSchedules { + reward: reward.to_string(), + lp_token: lp_token.to_string(), + start_after, + limit, + }, + ) + } + + pub fn blocked_tokens(&self) -> Vec { + self.app + .wrap() + .query_wasm_smart( + &self.generator, + &QueryMsg::BlockedTokensList { + start_after: None, + limit: None, + }, + ) + .unwrap() + } + + pub fn pool_info(&self, lp_token: &str) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.generator, + &QueryMsg::PoolInfo { + lp_token: lp_token.to_string(), + }, + ) + } + + pub fn pool_stakers( + &self, + lp_token: &str, + start_after: Option<&Addr>, + limit: Option, + ) -> Vec<(String, Uint128)> { + self.app + .wrap() + .query_wasm_smart( + &self.generator, + &QueryMsg::PoolStakers { + lp_token: lp_token.to_string(), + start_after: start_after.map(ToString::to_string), + limit, + }, + ) + .unwrap() + } + + pub fn query_reward_info(&self, lp_token: &str) -> Vec { + self.app + .wrap() + .query_wasm_smart( + &self.generator, + &QueryMsg::RewardInfo { + lp_token: lp_token.to_string(), + }, + ) + .unwrap() + } + + pub fn create_pair(&mut self, asset_infos: &[AssetInfo]) -> AnyResult { + let asset_infos = asset_infos.to_vec(); + self.app + .execute_contract( + Addr::unchecked("permissionless"), + self.factory.clone(), + &factory::ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.clone(), + init_params: None, + }, + &[], + ) + .map(|_| self.query_pair_info(&asset_infos)) + } + + pub fn create_stable_pair(&mut self, asset_infos: &[AssetInfo]) -> PairInfo { + for x in asset_infos { + if let AssetInfo::NativeToken { denom } = x { + self.app + .execute_contract( + self.owner.clone(), + self.coin_registry.clone(), + &native_coin_registry::ExecuteMsg::Add { + native_coins: vec![(denom.to_string(), 6)], + }, + &[], + ) + .unwrap(); + } + } + + let asset_infos = asset_infos.to_vec(); + self.app + .execute_contract( + Addr::unchecked("permissionless"), + self.factory.clone(), + &factory::ExecuteMsg::CreatePair { + pair_type: PairType::Stable {}, + asset_infos: asset_infos.clone(), + init_params: Some( + to_binary(&StablePoolParams { + amp: 10, + owner: None, + }) + .unwrap(), + ), + }, + &[], + ) + .unwrap(); + + self.query_pair_info(&asset_infos) + } + + /// Supports only native coins + pub fn provide_liquidity( + &mut self, + sender: &Addr, + assets: &[Asset], + pair_addr: &Addr, + auto_stake: bool, + ) -> AnyResult { + // We don't test pair contract here thus we top up user's balance implicitly + let funds = assets.iter().map(|a| a.as_coin().unwrap()).collect_vec(); + self.mint_assets(&sender, assets); + + let msg = pair::ExecuteMsg::ProvideLiquidity { + assets: assets.to_vec(), + slippage_tolerance: None, + auto_stake: Some(auto_stake), + receiver: None, + }; + + self.app + .execute_contract(sender.clone(), pair_addr.clone(), &msg, &funds) + } + + pub fn snapshot_balances(&self, user: &Addr, pending_rewards: &[Asset]) -> Vec { + pending_rewards + .iter() + .map(|asset| { + let balance = match &asset.info { + AssetInfo::Token { contract_addr } => { + self.app + .wrap() + .query_wasm_smart::( + contract_addr, + &cw20::Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap() + .balance + } + AssetInfo::NativeToken { denom } => { + self.app.wrap().query_balance(user, denom).unwrap().amount + } + }; + + asset.info.with_balance(balance) + }) + .collect_vec() + } +} + +pub fn assert_rewards(bal_before: &[Asset], bal_after: &[Asset], pending_rewards: &[Asset]) { + let sort_closure = |a: &&Asset, b: &&Asset| a.info.to_string().cmp(&b.info.to_string()); + + let expected = bal_before + .iter() + .sorted_by(sort_closure) + .zip(pending_rewards.iter().sorted_by(sort_closure)) + .fold(vec![], |mut acc, (before, pending)| { + let amount = before.amount + pending.amount; + acc.push(before.info.with_balance(amount)); + acc + }); + + let bal_after = bal_after + .iter() + .sorted_by(sort_closure) + .cloned() + .collect_vec(); + + assert_eq!(bal_after, expected); +} diff --git a/contracts/tokenomics/incentives/tests/helper/mod.rs b/contracts/tokenomics/incentives/tests/helper/mod.rs new file mode 100644 index 000000000..455ba16f1 --- /dev/null +++ b/contracts/tokenomics/incentives/tests/helper/mod.rs @@ -0,0 +1,5 @@ +#![cfg(not(tarpaulin_include))] +pub mod broken_cw20; +mod helper; + +pub use helper::*; diff --git a/contracts/tokenomics/incentives/tests/incentives_integration_tests.rs b/contracts/tokenomics/incentives/tests/incentives_integration_tests.rs new file mode 100644 index 000000000..1890abcd5 --- /dev/null +++ b/contracts/tokenomics/incentives/tests/incentives_integration_tests.rs @@ -0,0 +1,2203 @@ +use std::str::FromStr; + +use cosmwasm_std::{coin, coins, Decimal, Timestamp, Uint128}; +use cw_multi_test::Executor; + +use astroport::asset::{native_asset_info, AssetInfo, AssetInfoExt}; +use astroport::incentives::{ + ExecuteMsg, IncentivizationFeeInfo, ScheduleResponse, EPOCHS_START, EPOCH_LENGTH, + MAX_REWARD_TOKENS, +}; +use astroport_incentives::error::ContractError; + +use crate::helper::{assert_rewards, Helper, TestAddr}; + +mod helper; + +#[test] +fn test_stake_unstake() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + + let user = TestAddr::new("user"); + + // ##### Check native LPs + // TODO: build token factory based pair and test the lines below + + // let native_lp = native_asset_info("lp_token".to_string()).with_balance(1000u16); + // helper.mint_coins(&user, vec![native_lp.as_coin().unwrap()]); + // + // helper.stake(&user, native_lp).unwrap(); + // + // helper.unstake(&user, "lp_token", 500).unwrap(); + // + // // Unstake more than staked + // let err = helper.unstake(&user, "lp_token", 10000).unwrap_err(); + // assert_eq!( + // err.downcast::().unwrap(), + // ContractError::AmountExceedsBalance { + // available: 500u16.into(), + // withdraw_amount: 10000u16.into() + // } + // ); + // + // // Unstake non-existing LP token + // let err = helper + // .unstake(&user, "non_existing_lp_token", 10000) + // .unwrap_err(); + // assert_eq!( + // err.downcast::().unwrap(), + // ContractError::PositionDoesntExist { + // user: user.to_string(), + // lp_token: "non_existing_lp_token".to_string() + // } + // ); + // + // helper.unstake(&user, "lp_token", 500).unwrap(); + + // ##### Check cw20 LPs + + let asset_infos = [AssetInfo::native("uusd"), AssetInfo::native("ueur")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, false) + .unwrap(); + + let cw20_lp = AssetInfo::cw20(pair_info.liquidity_token.clone()); + let initial_lp_balance = cw20_lp.query_pool(&helper.app.wrap(), &user).unwrap(); + helper + .stake(&user, cw20_lp.with_balance(initial_lp_balance)) + .unwrap(); + let lp_balance = cw20_lp.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(lp_balance.u128(), 0); + + // Unstake more than staked + let err = helper + .unstake( + &user, + pair_info.liquidity_token.as_str(), + initial_lp_balance + Uint128::one(), + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::AmountExceedsBalance { + available: initial_lp_balance, + withdraw_amount: initial_lp_balance + Uint128::one() + } + ); + + // Unstake half + helper + .unstake( + &user, + pair_info.liquidity_token.as_str(), + initial_lp_balance.u128() / 2, + ) + .unwrap(); + let lp_balance = cw20_lp.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(lp_balance.u128(), initial_lp_balance.u128() / 2); + + // Unstake the rest + helper + .unstake( + &user, + pair_info.liquidity_token.as_str(), + initial_lp_balance.u128() / 2, + ) + .unwrap(); + let lp_balance = cw20_lp.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(lp_balance, initial_lp_balance); +} + +#[test] +fn test_claim_rewards() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + + let mut pools = vec![ + ("uusd", "eur", "".to_string(), vec!["user1", "user2"], 100), + ("uusd", "tokenA", "".to_string(), vec!["user1"], 50), + ("uusd", "tokenB", "".to_string(), vec!["user2"], 50), + ]; + + let mut active_pools = vec![]; + for (token1, token2, lp_token, stakers, alloc_points) in pools.iter_mut() { + let asset_infos = [AssetInfo::native(*token1), AssetInfo::native(*token2)]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + *lp_token = pair_info.liquidity_token.to_string(); + active_pools.push((pair_info.liquidity_token.to_string(), *alloc_points)); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + for staker in stakers { + let staker_addr = TestAddr::new(staker); + + // Pool doesn't exist in Generator yet + let astro_before = astro.query_pool(&helper.app.wrap(), &staker_addr).unwrap(); + helper + .claim_rewards(&staker_addr, vec![pair_info.liquidity_token.to_string()]) + .unwrap_err(); + let astro_after = astro.query_pool(&helper.app.wrap(), &staker_addr).unwrap(); + assert_eq!((astro_after - astro_before).u128(), 0); + + helper + .provide_liquidity( + &staker_addr, + &provide_assets, + &pair_info.contract_addr, + true, + ) + .unwrap(); + } + } + + // Invalid active pools set + let err = helper + .setup_pools(vec![ + (TestAddr::new("pool1").to_string(), 1), + (TestAddr::new("pool1").to_string(), 1), + ]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::DuplicatedPoolFound {} + ); + + // Can't set 0 alloc point + let err = helper + .setup_pools(vec![ + (TestAddr::new("pool1").to_string(), 0), + (TestAddr::new("pool2").to_string(), 1), + ]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ZeroAllocPoint { + lp_token: TestAddr::new("pool1").to_string() + } + ); + + // Only owner can execute operations below + let err = helper + .app + .execute_contract( + TestAddr::new("not_owner"), + helper.generator.clone(), + &ExecuteMsg::SetTokensPerSecond { + amount: 1u128.into(), + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + let err = helper + .app + .execute_contract( + TestAddr::new("not_owner"), + helper.generator.clone(), + &ExecuteMsg::SetupPools { + pools: active_pools + .iter() + .map(|(pool, amount)| (pool.clone(), (*amount).into())) + .collect(), + }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + helper.setup_pools(active_pools).unwrap(); + helper.set_tokens_per_second(1_000000).unwrap(); + + // Block time still the same thus no rewards collected + for (_, _, lp_token, stakers, _) in &pools { + for staker in stakers { + let staker_addr = TestAddr::new(staker); + + let pending = helper.query_pending_rewards(&staker_addr, &lp_token); + let bal_before = helper.snapshot_balances(&staker_addr, &pending); + + helper + .claim_rewards(&staker_addr, vec![lp_token.clone()]) + .unwrap(); + + let bal_after = helper.snapshot_balances(&staker_addr, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + } + } + + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(5)); + + let user1 = TestAddr::new("user1"); + let astro_before = astro.query_pool(&helper.app.wrap(), &user1).unwrap(); + let err = helper + .claim_rewards( + &user1, + vec![ + pools[0].2.to_string(), + pools[1].2.to_string(), + pools[2].2.to_string(), // user1 doesn't have position in this pool and it should fail transaction + ], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::PositionDoesntExist { + user: user1.to_string(), + lp_token: pools[2].2.to_string() + } + ); + + helper + .claim_rewards(&user1, vec![pools[0].2.to_string(), pools[1].2.to_string()]) + .unwrap(); + let astro_after = astro.query_pool(&helper.app.wrap(), &user1).unwrap(); + assert_eq!((astro_after - astro_before).u128(), 2_500000); + + let user2 = TestAddr::new("user2"); + let astro_before = astro.query_pool(&helper.app.wrap(), &user2).unwrap(); + helper + .claim_rewards(&user2, vec![pools[0].2.to_string(), pools[2].2.to_string()]) + .unwrap(); + let astro_after = astro.query_pool(&helper.app.wrap(), &user2).unwrap(); + assert_eq!((astro_after - astro_before).u128(), 2_500000); +} + +#[test] +fn test_incentives() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + let reward_asset_info = AssetInfo::native("reward"); + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.mint_assets(&bank, &[reward.clone()]); + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_coin(&bank, &incentivization_fee); + + // Check general validation + let err = helper + .incentivize(&bank, &lp_token, schedule.clone(), &[]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::IncentivizationFeeExpected { + fee: incentivization_fee.to_string(), + lp_token: lp_token.clone(), + new_reward_token: reward_asset_info.to_string(), + } + ); + let err = helper + .incentivize(&bank, &lp_token, schedule.clone(), &coins(1, "astro")) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::IncentivizationFeeExpected { + fee: incentivization_fee.to_string(), + lp_token: lp_token.clone(), + new_reward_token: reward_asset_info.to_string(), + } + ); + let additional_random_funds = coin(1000u128, "uusd"); + helper.mint_coin(&bank, &additional_random_funds); + let err = helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[additional_random_funds, incentivization_fee.clone()], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Supplied coins contain uusd that is not in the input asset vector" + ); + + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(internal_sch.next_epoch_start_ts) + }); + + // Iterate over 2 weeks by 1 day and claim rewards + loop { + if helper.app.block_info().time.seconds() > internal_sch.end_ts { + break; + } + + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + } + + let reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + // A small amount of reward is lost due to rounding + assert_eq!(reward_balance.u128(), 999_999986); + + // Claim after schedule ended doesn't do anything + for _ in 0..5 { + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let new_reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(new_reward_balance, reward_balance); + } +} + +#[test] +fn test_cw20_incentives() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + let reward_cw20 = helper.init_cw20("reward", None); + let reward_asset_info = AssetInfo::cw20(reward_cw20); + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.mint_assets(&bank, &[reward.clone()]); + + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(internal_sch.next_epoch_start_ts) + }); + + // Iterate over 2 weeks by 1 day and claim rewards + loop { + if helper.app.block_info().time.seconds() > internal_sch.end_ts { + break; + } + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + } + + let reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + // A small amount of reward is lost due to rounding + assert_eq!(reward_balance.u128(), 999_999986); + + // Claiming after schedule ended doesn't do anything + for _ in 0..5 { + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + let new_reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(new_reward_balance, reward_balance); + } +} + +#[test] +fn test_multiple_schedules_same_reward() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + // Incentivize with ASTRO + helper.setup_pools(vec![(lp_token.clone(), 100)]).unwrap(); + helper.set_tokens_per_second(100).unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + let reward_asset_info = AssetInfo::native("reward"); + let reward = reward_asset_info.with_balance(1000_000000u128); + + // Create multiple overlapping schedules with the same reward token starting right away + let schedules: Vec<_> = (1..=5) + .into_iter() + .map(|i| helper.create_schedule(&reward, i).unwrap()) + .collect(); + for (ind, (schedule, _)) in schedules.iter().enumerate() { + helper.mint_assets(&bank, &[reward.clone()]); + if ind == 0 { + // attach incentivization fee on the first schedule + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + } else { + helper + .incentivize(&bank, &lp_token, schedule.clone(), &[]) + .unwrap(); + } + } + + let time_before_claims = helper.app.block_info().time.seconds(); + + // Rewards started right away + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(reward_balance.u128(), 207_189296); + // And received ASTRO rewards + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(astro_reward_balance.u128(), 86400 * 100); + + // Iterate till the end of the longest schedule by 1 day and claim rewards + loop { + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + if helper.app.block_info().time.seconds() > schedules.last().cloned().unwrap().1.end_ts { + break; + } else { + helper.next_block(86400) + } + } + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + // A small amount of reward is lost due to rounding + assert_eq!(reward_balance.u128(), 4999_999972); + + let time_now = helper.app.block_info().time.seconds(); + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!( + astro_reward_balance.u128(), + u128::from(time_now - time_before_claims) * 100 + ); +} + +#[test] +fn test_multiple_schedules_different_reward() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + // Incentivize with ASTRO + helper.setup_pools(vec![(lp_token.clone(), 100)]).unwrap(); + helper.set_tokens_per_second(100).unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + + let schedules: Vec<_> = (1..=MAX_REWARD_TOKENS) + .into_iter() + .map(|i| { + let reward_asset_info = AssetInfo::native(format!("reward{i}")); + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.create_schedule(&reward, 2).unwrap() + }) + .collect(); + // Create multiple schedules with different rewards (starts on the next week) + for (schedule, _) in &schedules { + helper.mint_assets(&bank, &[schedule.reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + } + + // Can't incentivize with one more reward token + let reward_asset_info = AssetInfo::native(format!("reward{}", MAX_REWARD_TOKENS + 1)); + let reward = reward_asset_info.with_balance(1000_000000u128); + let (schedule, _) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_assets(&bank, &[schedule.reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + let err = helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::TooManyRewardTokens { + lp_token: lp_token.clone() + } + ); + + let time_before_claims = helper.app.block_info().time.seconds(); + + // Rewards started right away + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + for (schedule, _) in &schedules { + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let reward_balance = schedule + .reward + .info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(reward_balance.u128(), 47_629547); + } + // And received ASTRO rewards + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(astro_reward_balance.u128(), 86400 * 100); + + // Iterate till the end of the longest schedule by 1 day and claim rewards + loop { + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + if helper.app.block_info().time.seconds() > schedules.last().cloned().unwrap().1.end_ts { + break; + } else { + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + } + } + + for (schedule, _) in &schedules { + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let reward_balance = schedule + .reward + .info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + // Total amount is a bit off because of rounding due to Decimal type + assert_eq!( + reward_balance.u128(), + 999_999980, + "Balance for {} is wrong", + schedule.reward.info + ); + } + + let time_now = helper.app.block_info().time.seconds(); + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!( + astro_reward_balance.u128(), + u128::from(time_now - time_before_claims) * 100 + ); +} + +#[test] +fn test_astro_external_reward() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(EPOCHS_START + EPOCH_LENGTH)); + + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + // Incentivize with ASTRO + helper.setup_pools(vec![(lp_token.clone(), 100)]).unwrap(); + helper.set_tokens_per_second(100).unwrap(); + + // Setup external rewards: 2 equal external ASTRO rewards that must be summed up + let bank = TestAddr::new("bank"); + let reward = astro.with_balance(2u128 * 7 * 86400 * 25); // 25 uastro per second + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_assets(&bank, &[reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + // 2nd schedule doesn't require incentivization fee + helper.mint_assets(&bank, &[reward.clone()]); + helper + .incentivize(&bank, &lp_token, schedule.clone(), &[]) + .unwrap(); + + // Prepare user's liquidity + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let time_before_claims = helper.app.block_info().time.seconds(); + + helper.next_block(86400); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!(astro_reward_balance.u128(), 86400 * (100 + 50)); + + // Iterate till the end of the schedule by 1 day and claim rewards + loop { + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + if helper.app.block_info().time.seconds() > internal_sch.end_ts { + break; + } else { + helper.next_block(86400); + } + } + + let time_now = helper.app.block_info().time.seconds(); + let astro_reward_balance = astro.query_pool(&helper.app.wrap(), &user).unwrap(); + assert_eq!( + astro_reward_balance.u128(), + u128::from(time_now - time_before_claims) * 100 // protocol rewards + + u128::from(internal_sch.end_ts - internal_sch.next_epoch_start_ts) * 50 // external rewards + ); +} + +#[test] +fn test_blocked_tokens() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let guardian = TestAddr::new("guardian"); + + let tokens = [ + AssetInfo::native("usd"), + AssetInfo::native("foo"), + AssetInfo::native("blk"), + AssetInfo::native("bar"), + ]; + let norm_pair1_info = helper + .create_pair(&[tokens[0].clone(), tokens[1].clone()]) + .unwrap(); + let norm_pair2_info = helper + .create_pair(&[tokens[0].clone(), tokens[3].clone()]) + .unwrap(); + + // Check general validation + let err = helper + .block_tokens(&guardian, &[astro.clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + format!( + "Generic error: Blocking ASTRO token {} is prohibited", + &astro + ) + ); + let err = helper + .block_tokens(&TestAddr::new("random"), &[tokens[2].clone()]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + let err = helper + .unblock_tokens(&owner, &[tokens[2].clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + format!( + "Generic error: Token {} wasn't found in the blocked list", + &tokens[2] + ) + ); + + let err = helper + .update_blocklist(&owner, &[tokens[2].clone(), tokens[2].clone()], &[]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Duplicated tokens found" + ); + + let err = helper + .update_blocklist(&owner, &[], &[tokens[0].clone(), tokens[0].clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Duplicated tokens found" + ); + + let err = helper + .update_blocklist( + &owner, + &[tokens[0].clone()], + &[tokens[0].clone(), tokens[1].clone()], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Duplicated tokens found" + ); + + // Block 'blk' token + helper.block_tokens(&owner, &[tokens[2].clone()]).unwrap(); + + let blocked = helper.blocked_tokens(); + assert_eq!(blocked[0], tokens[2]); + + let err = helper + .block_tokens(&owner, &[tokens[2].clone()]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + format!( + "Generic error: Token {} is already in the blocked list", + &tokens[2] + ) + ); + + // Create pair with blocked token 'blk' and stake in Generator. + // Generator should allow it. + let blk_pair_info = helper + .create_pair(&[tokens[0].clone(), tokens[2].clone()]) + .unwrap(); + + // Try to add ASTRO emissions to the 'blk' pair + let err = helper + .setup_pools(vec![ + (norm_pair1_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + (blk_pair_info.liquidity_token.to_string(), 1), + ]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::BlockedToken { + token: tokens[2].to_string() + } + ); + + // Activate allowed pairs + helper + .setup_pools(vec![ + (norm_pair1_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + ]) + .unwrap(); + helper.set_tokens_per_second(100).unwrap(); + + helper.next_block(1000); + + // Unblock 'blk' token and remove norm_pair1 from active set + helper.unblock_tokens(&owner, &[tokens[2].clone()]).unwrap(); + helper + .setup_pools(vec![ + (blk_pair_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + ]) + .unwrap(); + + // For simplicity we have no stakers in this test. However, all rewards are accrued in 'orphaned_rewards' + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // 50 astro * 1000 passed seconds + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 0); // This pair was just incentivized in this block + + helper.next_block(1000); + + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // deactivated pool didn't get anything + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 2000); + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); + + // Block poor 'blk' token again. It should automatically deactivate blk_pair + helper + .block_tokens(&guardian, &[tokens[2].clone()]) + .unwrap(); + + helper.next_block(1000); + + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // deactivated pool didn't get anything + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!( + reward_info[0].orphaned.to_uint_floor().u128(), + 50 * 2000 + 100 * 1000 + ); // this pools is the only active atm + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // deactivated blk pair didn't get anything +} + +#[test] +fn test_blocked_pair_types() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + + let tokens = [ + AssetInfo::native("usd"), + AssetInfo::native("foo"), + AssetInfo::native("bar"), + ]; + let norm_pair1_info = helper + .create_pair(&[tokens[0].clone(), tokens[1].clone()]) + .unwrap(); + let norm_pair2_info = helper + .create_pair(&[tokens[0].clone(), tokens[2].clone()]) + .unwrap(); + let blk_pair_info = helper.create_stable_pair(&[tokens[1].clone(), tokens[2].clone()]); + + // Activate all pairs. blk pair is not blocked yet + helper + .setup_pools(vec![ + (norm_pair1_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + (blk_pair_info.liquidity_token.to_string(), 1), + ]) + .unwrap(); + helper.set_tokens_per_second(150).unwrap(); + + helper.next_block(1000); + + // Block 'blk' pair + helper + .block_pair_type(&owner, blk_pair_info.pair_type.clone()) + .unwrap(); + + // For simplicity we have no stakers in this test. However, all rewards are accrued in 'orphaned_rewards' + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // 50 astro * 1000 passed seconds + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); + // Although this pair is blocked, it still gets rewards until manually deactivated + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); + + // Deactivate 'blk' pair + helper.deactivate_blocked().unwrap(); + + helper.next_block(1000); + + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!( + reward_info[0].orphaned.to_uint_floor().u128(), + 50 * 1000 + 75 * 1000 + ); + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!( + reward_info[0].orphaned.to_uint_floor().u128(), + 50 * 1000 + 75 * 1000 + ); + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // deactivated blk pair didn't get anything + + // Next time setup pool won't allow to activate 'blk' pair + let err = helper + .setup_pools(vec![ + (norm_pair1_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + (blk_pair_info.liquidity_token.to_string(), 1), + ]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::BlockedPairType { + pair_type: blk_pair_info.pair_type.clone() + } + ); + + // All subsequent deactivate_blocked calls will do nothing + helper.deactivate_blocked().unwrap(); + + // Lets check factory deactivation logic + // Only factory can deactivate pair + let err = helper + .deactivate_pool(&owner, norm_pair1_info.liquidity_token.as_str()) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + // Deactivate norm_pair1_info by its asset infos + helper + .deactivate_pool_full_flow(&[tokens[0].clone(), tokens[1].clone()]) + .unwrap(); + + let err = helper + .setup_pools(vec![ + (norm_pair1_info.liquidity_token.to_string(), 1), + (norm_pair2_info.liquidity_token.to_string(), 1), + ]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + format!( + "Generic error: The pair is not registered: {}-{}", + &tokens[0], &tokens[1] + ) + ); + + helper.next_block(1000); + + let reward_info = helper.query_reward_info(norm_pair1_info.liquidity_token.as_str()); + assert_eq!( + reward_info[0].orphaned.to_uint_floor().u128(), + 50 * 1000 + 75 * 1000 // deactivated pool gets nothing + ); + let reward_info = helper.query_reward_info(norm_pair2_info.liquidity_token.as_str()); + assert_eq!( + reward_info[0].orphaned.to_uint_floor().u128(), + 50 * 1000 + 75 * 1000 + 150 * 1000 + ); + let reward_info = helper.query_reward_info(blk_pair_info.liquidity_token.as_str()); + assert_eq!(reward_info[0].orphaned.to_uint_floor().u128(), 50 * 1000); // deactivated blk pair still gets nothing +} + +#[test] +fn test_incentives_with_blocked() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + + // Try to incentivize with blocked token + let bank = TestAddr::new("bank"); + let blocked_token = AssetInfo::native("blocked_reward"); + helper + .block_tokens(&owner, &[blocked_token.clone()]) + .unwrap(); + let reward = blocked_token.with_balance(1000_000000u128); + helper.mint_assets(&bank, &[reward.clone()]); + + let (schedule, _) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_coin(&bank, &incentivization_fee); + let err = helper + .incentivize( + &bank, + pair_info.liquidity_token.as_str(), + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::BlockedToken { + token: blocked_token.to_string() + } + ); +} + +#[test] +fn test_remove_rewards() { + let astro = native_asset_info("astro".to_string()); + + let mut helper = Helper::new("owner", &astro).unwrap(); + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(EPOCHS_START + EPOCH_LENGTH)); + + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + let reward_asset_info = AssetInfo::native("reward"); + let reward = reward_asset_info.with_balance(1000_000000u128); + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + + helper.mint_assets(&bank, &[reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(internal_sch.next_epoch_start_ts) + }); + + // 5 days + for _ in 0..5 { + helper.next_block(86400); + + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + } + + // Assume 1 day passed and then reward gets deregistered + helper.next_block(86400); + + let receiver = TestAddr::new("receiver"); + + // Only owner is able to remove reward + let err = helper + .remove_reward( + &TestAddr::new("random"), + &lp_token, + &reward_asset_info.to_string(), + false, + &receiver, + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + helper + .remove_reward( + &owner, + &lp_token, + &reward_asset_info.to_string(), + false, + &receiver, + ) + .unwrap(); + + // User must be allowed to claim rewards for the last 1 day + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let outstanding = reward_asset_info + .query_pool(&helper.app.wrap(), &receiver) + .unwrap() + .u128(); + let claimed = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap() + .u128(); + assert_eq!(outstanding, 571_428571); // ~ 8 * 71428571 (8 days left) + assert_eq!(claimed, 428_571426); // // claimed 6 days in a row ~ 6 * 71428571 + assert_eq!(outstanding + claimed, 999_999997); // ~ initial reward amount i.e. 1000_000000 +} + +#[test] +fn test_long_unclaimed_rewards() { + let astro = native_asset_info("astro".to_string()); + + let mut helper = Helper::new("owner", &astro).unwrap(); + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(EPOCHS_START + EPOCH_LENGTH)); + + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + + let schedules: Vec<_> = (1..=2) + .into_iter() + .map(|i| { + let reward_asset_info = AssetInfo::native(format!("reward{i}")); + let reward = reward_asset_info.with_balance(50_000_000000u128); + // Create 2 schedules with different duration + [ + helper.create_schedule(&reward, 20).unwrap(), + helper.create_schedule(&reward, 10).unwrap(), + ] + }) + .flatten() + .collect(); + let max_end = schedules.iter().map(|(_, sch)| sch.end_ts).max().unwrap(); + // Create multiple schedules with different rewards (starts on the next week) + for (ind, (schedule, _)) in schedules.iter().enumerate() { + helper.mint_assets(&bank, &[schedule.reward.clone()]); + let mut attach_funds = vec![]; + if ind % 2 == 0 { + helper.mint_coin(&bank, &incentivization_fee); + attach_funds = vec![incentivization_fee.clone()] + } + helper + .incentivize(&bank, &lp_token, schedule.clone(), &attach_funds) + .unwrap(); + } + + // Start from the starting point and jump over 5 weeks + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(schedules[0].1.next_epoch_start_ts + 86400 * 7 * 5); + }); + + // Deregister reward1 + let receiver = TestAddr::new("receiver"); + helper + .remove_reward( + &owner, + &lp_token, + &schedules[0].0.reward.info.to_string(), + false, + &receiver, + ) + .unwrap(); + let deregister_amount = schedules[0] + .0 + .reward + .info + .query_pool(&helper.app.wrap(), &receiver) + .unwrap() + .u128(); + assert_eq!(deregister_amount, 62499_999999); + + // Iterate till the end of the longest schedule by 1 day and claim rewards + loop { + let pending = helper.query_pending_rewards(&user, &lp_token); + let bal_before = helper.snapshot_balances(&user, &pending); + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + let bal_after = helper.snapshot_balances(&user, &pending); + assert_rewards(&bal_before, &bal_after, &pending); + + if helper.app.block_info().time.seconds() > max_end { + break; + } else { + helper.next_block(86400); + } + } + + let claimed_reward1 = schedules[0] + .0 + .reward + .info + .query_pool(&helper.app.wrap(), &user) + .unwrap() + .u128(); + assert_eq!(deregister_amount + claimed_reward1, 99999_999998); + + for (schedule, _) in schedules.iter().skip(2) { + let bal = schedule + .reward + .info + .query_pool(&helper.app.wrap(), &user) + .unwrap() + .u128(); + assert_eq!(bal, 99_999_999974); // All rewards are claimed + } +} + +#[test] +fn test_queries() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // doesnt stake + ) + .unwrap(); + + for i in 0..10 { + let user = TestAddr::new(&format!("user_{i}")); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + } + + let user = TestAddr::new("user_1"); + + assert_eq!(helper.query_deposit(&lp_token, &user).unwrap(), 100000); + + let random = TestAddr::new("random"); + let err = helper.query_deposit(&lp_token, &random).unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Generic error: Querier contract error: User {} doesn't have position in {}", + random.as_str(), + &lp_token + ) + ); + + let err = helper.query_deposit(random.as_str(), &user).unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Generic error: Querier contract error: User {} doesn't have position in {}", + user.as_str(), + random.as_str() + ) + ); + + // This Lp doesn't exist + helper.pool_info(random.as_str()).unwrap_err(); + + let pool_info = helper.pool_info(&lp_token).unwrap(); + assert_eq!(pool_info.rewards, []); + assert_eq!(pool_info.total_lp.u128(), 1_000_000); // 100_000 per user + assert_eq!( + pool_info.last_update_ts, + helper.app.block_info().time.seconds() + ); + + let stakers = helper.pool_stakers(&lp_token, None, None); + assert_eq!( + stakers[5], + ("wasm1_user_5".to_string(), Uint128::from(100_000u128)) + ); + let total = stakers + .iter() + .fold(Uint128::zero(), |acc, (_, bal)| acc + bal); + assert_eq!(total, pool_info.total_lp); + + let bank = TestAddr::new("bank"); + let reward_asset_info = AssetInfo::native("reward"); + let reward = reward_asset_info.with_balance(1000_000000u128); + + // Create multiple overlapping schedules with the same reward token starting right away + let schedules: Vec<_> = (1..=5) + .into_iter() + .map(|i| helper.create_schedule(&reward, i).unwrap()) + .collect(); + for (ind, (schedule, _)) in schedules.iter().enumerate() { + helper.mint_assets(&bank, &[reward.clone()]); + if ind == 0 { + // attach incentivization fee on the first schedule + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + } else { + helper + .incentivize(&bank, &lp_token, schedule.clone(), &[]) + .unwrap(); + } + } + + let res = helper + .query_ext_reward_schedules(&lp_token, &reward_asset_info, None, None) + .unwrap(); + assert_eq!( + res, + [ + ScheduleResponse { + rps: Decimal::from_str("2398.02426572720408957").unwrap(), + start_ts: 1696810000, + end_ts: 1698019200, + }, + ScheduleResponse { + rps: Decimal::from_str("1571.031212468851459733").unwrap(), + start_ts: 1698019200, + end_ts: 1698624000, + }, + ScheduleResponse { + rps: Decimal::from_str("1019.76329626157472324").unwrap(), + start_ts: 1698624000, + end_ts: 1699228800, + }, + ScheduleResponse { + rps: Decimal::from_str("606.335150073382231096").unwrap(), + start_ts: 1699228800, + end_ts: 1699833600, + }, + ScheduleResponse { + rps: Decimal::from_str("275.603571822290816888").unwrap(), + start_ts: 1699833600, + end_ts: 1700438400, + }, + ] + ); + let res = helper + .query_ext_reward_schedules(&lp_token, &reward_asset_info, None, Some(1)) + .unwrap(); + assert_eq!(res.len(), 1); + let res = helper + .query_ext_reward_schedules(&lp_token, &reward_asset_info, Some(1699228800), None) + .unwrap(); + assert_eq!(res.len(), 2); +} + +#[test] +fn test_update_config() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + + let new_vesting = TestAddr::new("new_vesting"); + let new_generator_controller = TestAddr::new("new_generator_controller"); + let new_guardian = TestAddr::new("new_guardian"); + let new_incentivization_fee_info = IncentivizationFeeInfo { + fee_receiver: TestAddr::new("new_fee_receiver"), + fee: coin(1000, "uusd"), + }; + + let msg = ExecuteMsg::UpdateConfig { + vesting_contract: Some(new_vesting.to_string()), + generator_controller: Some(new_generator_controller.to_string()), + guardian: Some(new_guardian.to_string()), + incentivization_fee_info: Some(new_incentivization_fee_info.clone()), + }; + + let err = helper + .app + .execute_contract(TestAddr::new("random"), helper.generator.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + helper + .app + .execute_contract(helper.owner.clone(), helper.generator.clone(), &msg, &[]) + .unwrap(); + + let config = helper.query_config(); + assert_eq!(config.vesting_contract, new_vesting); + assert_eq!( + config.generator_controller.unwrap(), + new_generator_controller + ); + assert_eq!(config.guardian.unwrap(), new_guardian); + assert_eq!( + config.incentivization_fee_info.unwrap(), + new_incentivization_fee_info + ); +} + +#[test] +fn test_change_ownership() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + + let new_owner = TestAddr::new("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + owner: new_owner.to_string(), + expires_in: 100, // seconds + }; + + // Unauthorized check + let err = helper + .app + .execute_contract( + TestAddr::new("not_owner"), + helper.generator.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = helper + .app + .execute_contract( + new_owner.clone(), + helper.generator.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + helper + .app + .execute_contract(helper.owner.clone(), helper.generator.clone(), &msg, &[]) + .unwrap(); + + // Claim from invalid addr + let err = helper + .app + .execute_contract( + TestAddr::new("invalid_addr"), + helper.generator.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Drop ownership proposal + helper + .app + .execute_contract( + helper.owner.clone(), + helper.generator.clone(), + &ExecuteMsg::DropOwnershipProposal {}, + &[], + ) + .unwrap(); + + // Claim ownership + let err = helper + .app + .execute_contract( + new_owner.clone(), + helper.generator.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner again + helper + .app + .execute_contract(helper.owner.clone(), helper.generator.clone(), &msg, &[]) + .unwrap(); + helper + .app + .execute_contract( + new_owner.clone(), + helper.generator.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + assert_eq!(helper.query_config().owner.to_string(), new_owner) +} + +#[test] +fn test_incentive_without_funds() { + let astro = native_asset_info("astro".to_string()); + let usdc = native_asset_info("usdc".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + let bank = TestAddr::new("bank"); + let reward_asset_info = usdc.clone(); + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.mint_assets(&bank, &[reward.clone()]); + let (schedule, _) = helper.create_schedule(&reward, 2).unwrap(); + let incentivization_fee = helper.incentivization_fee.clone(); + helper.mint_coin(&bank, &incentivization_fee); + // add reward + let err = helper + .app + .execute_contract( + bank.clone(), + helper.generator.clone(), + &ExecuteMsg::Incentivize { + lp_token: lp_token.to_string(), + schedule, + }, + &[incentivization_fee], // only send incentivization fee without reward + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Generic error: Native token balance mismatch between the argument (1000000000usdc) and the transferred (0usdc)") +} + +#[test] +fn test_claim_excess_rewards() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let mut pools = vec![ + ("uusd", "eur", "".to_string(), vec!["user1", "user2"], 100), + ("uusd", "tokenA", "".to_string(), vec!["user1"], 50), + ("uusd", "tokenB", "".to_string(), vec!["user2"], 50), + ]; + let mut active_pools = vec![]; + for (token1, token2, lp_token, stakers, alloc_points) in pools.iter_mut() { + let asset_infos = [AssetInfo::native(*token1), AssetInfo::native(*token2)]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + *lp_token = pair_info.liquidity_token.to_string(); + active_pools.push((pair_info.liquidity_token.to_string(), *alloc_points)); + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + for staker in stakers { + let staker_addr = TestAddr::new(staker); + // Pool doesn't exist in Generator yet + let astro_before = astro.query_pool(&helper.app.wrap(), &staker_addr).unwrap(); + helper + .claim_rewards( + &staker_addr, + vec![ + pair_info.liquidity_token.to_string(), + pair_info.liquidity_token.to_string(), + ], + ) + .unwrap_err(); + let astro_after = astro.query_pool(&helper.app.wrap(), &staker_addr).unwrap(); + assert_eq!((astro_after - astro_before).u128(), 0); + + helper + .provide_liquidity( + &staker_addr, + &provide_assets, + &pair_info.contract_addr, + true, + ) + .unwrap(); + } + } + + helper.setup_pools(active_pools).unwrap(); + helper.set_tokens_per_second(1_000000).unwrap(); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(5)); + let user1 = TestAddr::new("user1"); + let astro_before = astro.query_pool(&helper.app.wrap(), &user1).unwrap(); + let err = helper + .claim_rewards( + &user1, + vec![ + pools[0].2.to_string(), + pools[1].2.to_string(), + pools[0].2.to_string(), + pools[1].2.to_string(), + ], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::DuplicatedPoolFound {} + ); + + helper + .claim_rewards(&user1, vec![pools[0].2.to_string(), pools[1].2.to_string()]) + .unwrap(); + let astro_after = astro.query_pool(&helper.app.wrap(), &user1).unwrap(); + assert_eq!((astro_after - astro_before).u128(), 2_500000); +} + +#[test] +fn test_user_claim_less() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + let reward_asset_info = AssetInfo::native("reward"); + let reward = reward_asset_info.with_balance(1000_000000u128); + + // create reward schedule + helper.mint_assets(&bank, &[reward.clone()]); + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(internal_sch.next_epoch_start_ts) + }); + + // user claim, sets user index + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + // finish 1st schedule, reward goes to FINISHED_REWARD_INDEXES + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(internal_sch.end_ts + 1)); + + // create reward schedule again + helper.mint_assets(&bank, &[reward.clone()]); + let (schedule, internal_sch) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + + // few seconds before schedule finishes + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(internal_sch.end_ts - 1)); + + // user claim rewards as (global index - user index), which is incorrect + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + // finish 2nd schedule + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(internal_sch.end_ts + 1)); + + // user claim all rewards + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + // check user rewards + let new_reward_balance = reward_asset_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + + assert_eq!( + new_reward_balance.u128(), + (reward.amount + reward.amount).u128() - 2 // rounding error + ); +} + +#[test] +fn test_broken_cw20_incentives() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + // Owner provides liquidity first just make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + let bank = TestAddr::new("bank"); + + let schedules: Vec<_> = (1..=2) + .into_iter() + .map(|i| { + let reward_asset_info = if i == 1 { + AssetInfo::native(format!("reward{i}")) + } else { + let reward_cw20 = helper.init_broken_cw20("reward", None); + AssetInfo::cw20(reward_cw20) + }; + + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.create_schedule(&reward, 1).unwrap() + }) + .collect(); + + // Create multiple schedules with different rewards + for (schedule, _) in &schedules { + helper.mint_assets(&bank, &[schedule.reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + } + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(schedules[0].1.next_epoch_start_ts) + }); + + // Iterate by 1 day and claim rewards + loop { + if helper.app.block_info().time.seconds() > schedules[0].1.end_ts { + break; + } + + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(86400)); + } + + // Valid native coin reward was accrued properly + let valid_reward_balance = schedules[0] + .1 + .reward_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(valid_reward_balance.u128(), 999_999994); + + // Broken cw20 reward was not accrued because incentives contract simply ignores it + let broken_reward_balance = schedules[1] + .1 + .reward_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(broken_reward_balance.u128(), 0); +} + +#[test] +fn test_factory_deregisters_any_pool() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let asset_infos = &[AssetInfo::native("usd"), AssetInfo::native("foo")]; + + // factory contract create pair + helper.create_pair(asset_infos).unwrap(); + // ensure pair created + let pair_info = helper.query_pair_info(asset_infos); + assert_eq!(pair_info.asset_infos, asset_infos); + + // Incentives contract doesn't have such pool yet but it doesn't block deregistration + helper.deactivate_pool_full_flow(asset_infos).unwrap(); +} + +#[test] +fn test_orphaned_rewards() { + let astro = native_asset_info("astro".to_string()); + let mut helper = Helper::new("owner", &astro).unwrap(); + let incentivization_fee = helper.incentivization_fee.clone(); + + let asset_infos = [AssetInfo::native("foo"), AssetInfo::native("bar")]; + let pair_info = helper.create_pair(&asset_infos).unwrap(); + let lp_token = pair_info.liquidity_token.to_string(); + + let bank = TestAddr::new("bank"); + + let schedules: Vec<_> = (1..=(MAX_REWARD_TOKENS - 1)) + .into_iter() + .map(|i| { + let reward_asset_info = AssetInfo::native(format!("reward{i}")); + let reward = reward_asset_info.with_balance(1000_000000u128); + helper.create_schedule(&reward, 2).unwrap() + }) + .collect(); + // Create multiple schedules with different rewards + for (schedule, _) in &schedules { + helper.mint_assets(&bank, &[schedule.reward.clone()]); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + schedule.clone(), + &[incentivization_fee.clone()], + ) + .unwrap(); + } + + // Timing out all schedules. nobody stakes LP tokens, rewards become orphaned + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(schedules.last().cloned().unwrap().1.end_ts + 1) + }); + + let orph_receiver = TestAddr::new("orphaned_rewards_receiver"); + + // Check that there are still no orphaned rewards to claim + let err = helper + .claim_orphaned_rewards(None, &orph_receiver) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoOrphanedRewards {} + ); + + // Add one more reward thus triggering finished rewards cleanup + let reward = + AssetInfo::native(format!("reward{MAX_REWARD_TOKENS}")).with_balance(1000_000000u128); + let (new_schedule, int_new_schedule) = helper.create_schedule(&reward, 2).unwrap(); + helper.mint_assets(&bank, &[reward]); + helper.mint_coin(&bank, &incentivization_fee); + helper + .incentivize( + &bank, + &lp_token, + new_schedule, + &[incentivization_fee.clone()], + ) + .unwrap(); + + // Provide to check that user is only eligible for the last added reward + let provide_assets = [ + asset_infos[0].with_balance(100000u64), + asset_infos[1].with_balance(100000u64), + ]; + let user = TestAddr::new("user"); + helper + .provide_liquidity(&user, &provide_assets, &pair_info.contract_addr, true) + .unwrap(); + + helper + .app + .update_block(|block| block.time = Timestamp::from_seconds(int_new_schedule.end_ts + 1)); + + // Claim rewards and assert user only gets reward5 + helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + + for (schedule, _) in &schedules { + let reward_balance = schedule + .reward + .info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(reward_balance.u128(), 0); + } + + let reward_balance = int_new_schedule + .reward_info + .query_pool(&helper.app.wrap(), &user) + .unwrap(); + assert_eq!(reward_balance.u128(), 999_999999); + + // Owner claims first orphaned rewards + helper + .claim_orphaned_rewards(Some(1), &orph_receiver) + .unwrap(); + + // Owner claims all orphaned rewards + helper.claim_orphaned_rewards(None, &orph_receiver).unwrap(); + + for (schedule, _) in &schedules { + let reward_balance = schedule + .reward + .info + .query_pool(&helper.app.wrap(), &orph_receiver) + .unwrap(); + assert_eq!(reward_balance.u128(), 999999999); + } + + // Try to claim again + let err = helper + .claim_orphaned_rewards(None, &orph_receiver) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoOrphanedRewards {} + ); +} diff --git a/contracts/tokenomics/incentives/tests/incentives_simulations.rs b/contracts/tokenomics/incentives/tests/incentives_simulations.rs new file mode 100644 index 000000000..f1aa01513 --- /dev/null +++ b/contracts/tokenomics/incentives/tests/incentives_simulations.rs @@ -0,0 +1,425 @@ +#![allow(dead_code)] +extern crate core; + +use std::collections::{HashMap, HashSet}; + +use cosmwasm_std::{Addr, StdError, Timestamp}; +use itertools::Itertools; +use proptest::prelude::*; + +use astroport::asset::{AssetInfo, AssetInfoExt}; +use astroport::incentives::{MAX_PERIODS, MAX_REWARD_TOKENS}; +use astroport_incentives::error::ContractError; +use Event::*; + +use crate::helper::{Helper, TestAddr}; + +mod helper; +const MAX_EVENTS: usize = 100; +const MAX_POOLS: u8 = 3; +const MAX_USERS: u8 = 3; + +#[derive(Debug)] +enum Event { + Deposit { + sender_id: u8, + lp_token_id: u8, + amount: u8, + }, + Withdraw { + sender_id: u8, + lp_token_id: u8, + amount: u8, + }, + Claim { + sender_id: u8, + }, + Incentivize { + lp_token_id: u8, + reward_id: u8, + amount: u128, + duration: u64, + }, + SetupPools { + pools: Vec<(u8, u8)>, + tokens_per_second: u128, + }, + RemoveReward { + lp_token_id: u8, + reward_id: u8, + }, +} + +fn get_transfer_amount(events: &[cosmwasm_std::Event], denom: &str) -> u128 { + events + .iter() + .find(|event| event.ty == "transfer") + .and_then(|event| { + event + .attributes + .iter() + .find(|attr| attr.key == "amount" && attr.value.ends_with(denom)) + .map(|attr| { + attr.value + .strip_suffix(denom) + .unwrap() + .parse::() + .unwrap() + }) + }) + .unwrap_or(0) +} + +#[derive(Default, Debug)] +struct RewardData { + pub left: u128, + pub total: u128, +} + +type RewardsAccounting = HashMap>; + +fn update_total_rewards( + events: &[cosmwasm_std::Event], + lp_token: &str, + rewards_left: &mut RewardsAccounting, +) { + if let Some(lp_map) = rewards_left.get_mut(lp_token) { + let rewards = lp_map.keys().cloned().collect_vec(); + for reward in rewards { + let claimed_amount = get_transfer_amount(events, &reward); + if claimed_amount > 0 { + println!("Claimed {claimed_amount} {reward} for {lp_token}"); + lp_map + .entry(reward.clone()) + .and_modify(|v| { + v.left = v.left.checked_sub(claimed_amount).expect(&format!( + "Tried to claim more than available: {v:?} - {claimed_amount}" + )) + }) + .or_insert_with(|| { + panic!("Reward {reward} not found in {lp_token} rewards map"); + }); + } + } + } +} + +fn simulate_case(events: Vec<(Event, u64)>) { + let astro = AssetInfo::native("astro"); + let mut helper = Helper::new("owner", &astro).unwrap(); + let owner = helper.owner.clone(); + let incentivization_fee = helper.incentivization_fee.clone(); + + // How many tokens we need to produce MAX_POOLS unique pairs? + // solve n(n-1) / 2 = MAX_POOLS against n, select only positive result and round it up + let tokens_number = ((1.0 + (1.0 + 8.0 * MAX_POOLS as f64).sqrt()) / 2.0).ceil() as usize; + + let users = (0..MAX_USERS) + .into_iter() + .map(|i| TestAddr::new(&format!("user{i}"))) + .collect_vec(); + let mut user_positions = HashMap::new(); + + // Create MAX_POOLS pairs and provide liquidity for them for each user + let lp_tokens = (1..=tokens_number) + .cartesian_product(1..=tokens_number) + .filter(|(a, b)| a != b) + .filter_map(|(a, b)| { + let asset_infos = [ + AssetInfo::native(format!("token{a}")), + AssetInfo::native(format!("token{b}")), + ]; + let pair_info = helper.create_pair(&asset_infos).ok()?; + + let provide_assets = [ + asset_infos[0].with_balance(100_000_00000u64), + asset_infos[1].with_balance(100_000_00000u64), + ]; + // Owner provides liquidity first just to make following calculations easier + // since first depositor gets small cut of LP tokens + helper + .provide_liquidity( + &owner, + &provide_assets, + &pair_info.contract_addr, + false, // Owner doesn't stake in generator + ) + .unwrap(); + + for user in &users { + helper + .provide_liquidity( + &user, + &provide_assets, + &pair_info.contract_addr, + false, // Do not stake on test initialization + ) + .unwrap(); + } + + Some(pair_info.liquidity_token.to_string()) + }) + .take(MAX_POOLS as usize) + .collect_vec(); + + let bank = TestAddr::new("bank"); + let dereg_rewards_receiver = TestAddr::new("dereg_rewards_receiver"); + + let mut rewards: RewardsAccounting = HashMap::new(); + let mut longest_schedule_end = helper.app.block_info().time.seconds(); + + for (i, (event, shift_time)) in events.into_iter().enumerate() { + println!( + "Event {i} at {}: {event:?}", + helper.app.block_info().time.seconds() + ); + match event { + Deposit { + sender_id, + lp_token_id, + amount, + } => { + let user = &users[sender_id as usize]; + let lp_token = &lp_tokens[lp_token_id as usize]; + let lp_asset_info = AssetInfo::cw20(Addr::unchecked(lp_token)); + let total_amount = lp_asset_info.query_pool(&helper.app.wrap(), user).unwrap(); + let part = total_amount.u128() * amount as u128 / 100; + + let resp = helper + .stake(user, lp_asset_info.with_balance(part)) + .unwrap(); + + update_total_rewards(&resp.events, lp_token, &mut rewards); + + user_positions + .entry(user) + .or_insert(HashSet::new()) + .insert(lp_token.clone()); + } + Withdraw { + sender_id, + lp_token_id, + amount, + } => { + let user = &users[sender_id as usize]; + let lp_token = &lp_tokens[lp_token_id as usize]; + if let Ok(total_amount) = helper.query_deposit(lp_token, user) { + let part = total_amount * amount as u128 / 100; + let resp = helper.unstake(user, lp_token, part).unwrap(); + + update_total_rewards(&resp.events, lp_token, &mut rewards); + + if amount == 100 { + user_positions + .entry(user) + .or_insert(HashSet::new()) + .remove(lp_token); + } + } + } + Claim { sender_id } => { + let user = &users[sender_id as usize]; + for lp_token in user_positions.get(user).unwrap_or(&HashSet::new()) { + println!("{user} claims rewards for {lp_token}"); + let resp = helper.claim_rewards(user, vec![lp_token.clone()]).unwrap(); + + update_total_rewards(&resp.events, lp_token, &mut rewards); + } + } + Incentivize { + lp_token_id, + reward_id, + amount, + duration, + } => { + let lp_token = &lp_tokens[lp_token_id as usize]; + let reward_token = AssetInfo::native(format!("reward{reward_id}")); + // ignore invalid schedules + if let Ok((schedule, int_sch)) = + helper.create_schedule(&reward_token.with_balance(amount), duration) + { + longest_schedule_end = longest_schedule_end.max(int_sch.end_ts); + + let mut attach_funds = vec![]; + if helper.is_fee_needed(lp_token, &reward_token) { + helper.mint_coin(&bank, &incentivization_fee); + attach_funds.push(incentivization_fee.clone()); + } + helper.mint_assets(&bank, &[schedule.reward.clone()]); + helper + .incentivize(&bank, &lp_token, schedule, &attach_funds) + .unwrap(); + + let r = rewards + .entry(lp_token.to_string()) + .or_insert_with(HashMap::new) + .entry(reward_token.to_string()) + .or_insert(RewardData::default()); + r.left += amount; + r.total += amount; + } + } + SetupPools { + pools, + tokens_per_second, + } => { + let pools = pools + .into_iter() + .map(|(lp_token_id, alloc_points)| { + let lp_token = &lp_tokens[lp_token_id as usize]; + (lp_token.clone(), alloc_points as u128) + }) + .sorted_by(|a, b| a.0.cmp(&b.0)) + .dedup_by(|a, b| a.0 == b.0) + .collect_vec(); + + helper.setup_pools(pools).unwrap(); + helper.set_tokens_per_second(tokens_per_second).unwrap(); + } + RemoveReward { + lp_token_id, + reward_id, + } => { + let lp_token = &lp_tokens[lp_token_id as usize]; + let reward = format!("reward{reward_id}"); + let res = + helper.remove_reward(&owner, lp_token, &reward, false, &dereg_rewards_receiver); + + match res { + Ok(resp) => { + let removed_amount = get_transfer_amount(&resp.events, &reward); + rewards + .get_mut(lp_token) + .unwrap() + .entry(reward) + .and_modify(|v| { + v.left = v.left.checked_sub(removed_amount).expect(&format!( + "Tried to remove more than available: {v:?} - {removed_amount}" + )) + }); + } + Err(err) => { + let err = err.downcast::().unwrap(); + match err { + ContractError::Std(StdError::NotFound { .. }) + | ContractError::RewardNotFound { .. } => { + // ignore + } + unexpected_err => panic!("Unexpected error: {unexpected_err:?}"), + } + } + } + } + } + + helper.next_block(shift_time) + } + + // Collect all rewards till the end of the longest schedule + + helper.app.update_block(|block| { + block.time = Timestamp::from_seconds(longest_schedule_end + 1); + block.height += 1 + }); + + for (user, lp_tokens) in user_positions { + for lp_token in lp_tokens { + let resp = helper.claim_rewards(&user, vec![lp_token.clone()]).unwrap(); + update_total_rewards(&resp.events, &lp_token, &mut rewards); + } + } + + // Collect orphaned rewards. + match helper.claim_orphaned_rewards(None, dereg_rewards_receiver) { + Err(err) => { + let err = err.downcast::().unwrap(); + match err { + ContractError::NoOrphanedRewards {} => {} + unexpected_err => panic!("Unexpected error: {unexpected_err:?}"), + } + } + _ => {} + } +} + +fn generate_cases() -> impl Strategy> { + let lp_token_id_strategy = 0..MAX_POOLS; + let reward_id_strategy = 0..MAX_REWARD_TOKENS; + let percent_strategy = 10..=100u8; + let time_strategy = 600..43200u64; + let sender_id = 0..MAX_USERS; + + let events_strategy = prop_oneof![ + ( + sender_id.clone(), + lp_token_id_strategy.clone(), + percent_strategy.clone() + ) + .prop_map(|(sender_id, lp_token_id, amount)| { + Event::Deposit { + sender_id, + lp_token_id, + amount, + } + }), + ( + sender_id.clone(), + lp_token_id_strategy.clone(), + percent_strategy.clone() + ) + .prop_map(|(sender_id, lp_token_id, amount)| { + Event::Withdraw { + sender_id, + lp_token_id, + amount, + } + }), + sender_id.prop_map(|sender_id| { Event::Claim { sender_id } }), + ( + lp_token_id_strategy.clone(), + reward_id_strategy.clone(), + 1_000000..=1_000_000_000000u128, + 1..=MAX_PERIODS, + ) + .prop_map(|(lp_token_id, reward_id, amount, duration)| { + Event::Incentivize { + lp_token_id, + reward_id, + amount, + duration, + } + }), + ( + prop::collection::vec( + (lp_token_id_strategy.clone(), percent_strategy.clone()), + 1..=10 + ), + 500..=1_000000u128, + ) + .prop_map(|(pools, tokens_per_second)| Event::SetupPools { + pools: pools, + tokens_per_second: tokens_per_second as u128, + }), + (lp_token_id_strategy.clone(), reward_id_strategy.clone()).prop_map( + |(lp_token_id, reward_id)| Event::RemoveReward { + lp_token_id, + reward_id, + } + ), + ]; + + prop::collection::vec((events_strategy, time_strategy), 0..MAX_EVENTS) +} + +proptest! { + #[ignore] + #[test] + fn simulate(case in generate_cases()) { + simulate_case(case); + } +} + +#[test] +fn single_test() { + simulate_case(include!("test_case")) +} diff --git a/contracts/tokenomics/incentives/tests/test_case b/contracts/tokenomics/incentives/tests/test_case new file mode 100644 index 000000000..a79e848b9 --- /dev/null +++ b/contracts/tokenomics/incentives/tests/test_case @@ -0,0 +1 @@ +vec![(Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 8343), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 20564), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 18410), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 6884), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 28220), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 36712), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 43023), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 31397), (Deposit { sender_id: 0, lp_token_id: 0, amount: 10 }, 34540), (Deposit { sender_id: 0, lp_token_id: 0, amount: 13 }, 10377), (RemoveReward { lp_token_id: 0, reward_id: 2 }, 9530), (SetupPools { pools: vec![(0, 14), (0, 56), (0, 75), (0, 60)], tokens_per_second: 115512 }, 17769), (Claim { sender_id: 0 }, 37956), (SetupPools { pools: vec![(0, 43), (0, 61), (0, 83), (0, 89), (0, 90), (0, 30), (0, 67), (0, 33), (0, 44)], tokens_per_second: 771359 }, 9802), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 285275035471, duration: 20 }, 27111), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 84 }, 32833), (RemoveReward { lp_token_id: 0, reward_id: 0 }, 42564), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 26 }, 4568), (Deposit { sender_id: 0, lp_token_id: 0, amount: 63 }, 41246), (Claim { sender_id: 0 }, 18617), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 85 }, 27829), (Incentivize { lp_token_id: 0, reward_id: 3, amount: 921515519125, duration: 12 }, 15429), (SetupPools { pools: vec![(0, 35), (0, 52)], tokens_per_second: 417047 }, 35537), (Deposit { sender_id: 0, lp_token_id: 0, amount: 36 }, 7107), (Claim { sender_id: 0 }, 28614), (Deposit { sender_id: 0, lp_token_id: 0, amount: 38 }, 24055), (Claim { sender_id: 0 }, 8655), (Claim { sender_id: 0 }, 1671), (RemoveReward { lp_token_id: 0, reward_id: 2 }, 26843), (SetupPools { pools: vec![(0, 20), (0, 49), (0, 73), (0, 63), (0, 17), (0, 58), (0, 15), (0, 48)], tokens_per_second: 366277 }, 26069), (Deposit { sender_id: 0, lp_token_id: 0, amount: 65 }, 20360), (Deposit { sender_id: 0, lp_token_id: 0, amount: 86 }, 3514), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 704011066621, duration: 21 }, 29483), (Deposit { sender_id: 0, lp_token_id: 0, amount: 87 }, 19538), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 96 }, 19230), (SetupPools { pools: vec![(0, 38), (0, 36), (0, 59), (0, 63), (0, 30), (0, 58), (0, 88), (0, 58), (0, 53)], tokens_per_second: 546925 }, 15964), (Deposit { sender_id: 0, lp_token_id: 0, amount: 74 }, 20751), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 34 }, 17136), (Claim { sender_id: 0 }, 28349), (SetupPools { pools: vec![(0, 65), (0, 60), (0, 34), (0, 12), (0, 99), (0, 99), (0, 42), (0, 89), (0, 66), (0, 27)], tokens_per_second: 780155 }, 2656), (Deposit { sender_id: 0, lp_token_id: 0, amount: 73 }, 42443), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 821977727576, duration: 16 }, 37243), (Claim { sender_id: 0 }, 22264), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 97 }, 27836), (SetupPools { pools: vec![(0, 10), (0, 18), (0, 88), (0, 55), (0, 80), (0, 72), (0, 19), (0, 90)], tokens_per_second: 766603 }, 17491), (Incentivize { lp_token_id: 0, reward_id: 3, amount: 259523676450, duration: 19 }, 33394), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 336733153430, duration: 9 }, 9398), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 795134936738, duration: 20 }, 17777), (Deposit { sender_id: 0, lp_token_id: 0, amount: 30 }, 33895), (Deposit { sender_id: 0, lp_token_id: 0, amount: 32 }, 42109), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 31 }, 17549), (SetupPools { pools: vec![(0, 99), (0, 43), (0, 21), (0, 35), (0, 33)], tokens_per_second: 297936 }, 2925), (Deposit { sender_id: 0, lp_token_id: 0, amount: 81 }, 8261), (Claim { sender_id: 0 }, 19209), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 688694053487, duration: 4 }, 11166), (Claim { sender_id: 0 }, 6985), (Incentivize { lp_token_id: 0, reward_id: 0, amount: 83185300737, duration: 1 }, 12380), (SetupPools { pools: vec![(0, 49), (0, 30), (0, 39), (0, 46), (0, 11), (0, 69), (0, 97), (0, 50), (0, 83), (0, 78)], tokens_per_second: 75000 }, 14592), (SetupPools { pools: vec![(0, 71), (0, 50), (0, 71), (0, 91), (0, 88), (0, 24), (0, 24), (0, 17), (0, 50)], tokens_per_second: 431736 }, 36355), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 45 }, 14188), (SetupPools { pools: vec![(0, 30), (0, 10), (0, 66), (0, 48)], tokens_per_second: 608112 }, 30527), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 31 }, 25146), (SetupPools { pools: vec![(0, 30), (0, 34), (0, 56), (0, 45), (0, 76), (0, 16), (0, 54), (0, 54), (0, 66)], tokens_per_second: 466609 }, 10959), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 379848713359, duration: 9 }, 19980), (Deposit { sender_id: 0, lp_token_id: 0, amount: 61 }, 34440), (Deposit { sender_id: 0, lp_token_id: 0, amount: 64 }, 15804), (Claim { sender_id: 0 }, 39025), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 26 }, 21948), (Incentivize { lp_token_id: 0, reward_id: 3, amount: 549356940622, duration: 5 }, 32705), (Incentivize { lp_token_id: 0, reward_id: 2, amount: 393975676577, duration: 24 }, 37065), (SetupPools { pools: vec![(0, 89), (0, 63), (0, 42), (0, 57)], tokens_per_second: 281013 }, 9260), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 348779273016, duration: 25 }, 30552), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 374219466316, duration: 7 }, 12636), (Deposit { sender_id: 0, lp_token_id: 0, amount: 13 }, 40123), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 89 }, 17853), (Claim { sender_id: 0 }, 32737), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 967653394246, duration: 6 }, 18534), (Deposit { sender_id: 0, lp_token_id: 0, amount: 12 }, 42357), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 21 }, 10047), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 44 }, 10717), (Claim { sender_id: 0 }, 36620), (SetupPools { pools: vec![(0, 56), (0, 63), (0, 55), (0, 58), (0, 74)], tokens_per_second: 457779 }, 30212), (RemoveReward { lp_token_id: 0, reward_id: 3 }, 36976), (Deposit { sender_id: 0, lp_token_id: 0, amount: 44 }, 15531), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 307379911276, duration: 15 }, 7783), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 27 }, 40106), (Incentivize { lp_token_id: 0, reward_id: 0, amount: 754990612079, duration: 21 }, 13224), (Claim { sender_id: 0 }, 12711), (Incentivize { lp_token_id: 0, reward_id: 2, amount: 777204997155, duration: 25 }, 3053), (Claim { sender_id: 0 }, 7806), (RemoveReward { lp_token_id: 0, reward_id: 2 }, 26823), (RemoveReward { lp_token_id: 0, reward_id: 1 }, 9375), (Claim { sender_id: 0 }, 12455), (Claim { sender_id: 0 }, 35913), (Claim { sender_id: 0 }, 7690), (Deposit { sender_id: 0, lp_token_id: 0, amount: 90 }, 28839), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 42 }, 16723), (Claim { sender_id: 0 }, 29554), (Claim { sender_id: 0 }, 16516), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 85 }, 33270), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 55 }, 21701), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 95851625067, duration: 9 }, 24927), (Claim { sender_id: 0 }, 5379), (SetupPools { pools: vec![(0, 27), (0, 89)], tokens_per_second: 218946 }, 28276), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 48 }, 10375), (Withdraw { sender_id: 0, lp_token_id: 0, amount: 26 }, 9293), (RemoveReward { lp_token_id: 0, reward_id: 1 }, 41930), (Deposit { sender_id: 0, lp_token_id: 0, amount: 91 }, 24918), (Deposit { sender_id: 0, lp_token_id: 0, amount: 28 }, 41734), (Incentivize { lp_token_id: 0, reward_id: 1, amount: 189670267452, duration: 14 }, 10989), (RemoveReward { lp_token_id: 0, reward_id: 0 }, 34397), (Incentivize { lp_token_id: 0, reward_id: 0, amount: 202185892204, duration: 16 }, 25017), (RemoveReward { lp_token_id: 0, reward_id: 0 }, 19707)] \ No newline at end of file diff --git a/packages/astroport/Cargo.toml b/packages/astroport/Cargo.toml index 7a837317c..4762df836 100644 --- a/packages/astroport/Cargo.toml +++ b/packages/astroport/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport" -version = "3.6.1" +version = "3.8.0" authors = ["Astroport"] edition = "2021" description = "Common Astroport types, queriers and other utils" diff --git a/packages/astroport/src/asset.rs b/packages/astroport/src/asset.rs index ec98f718a..982514566 100644 --- a/packages/astroport/src/asset.rs +++ b/packages/astroport/src/asset.rs @@ -1,18 +1,17 @@ -use std::collections::{HashMap, HashSet}; use std::fmt; -use crate::cosmwasm_ext::DecimalToInteger; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - coin, from_slice, to_binary, Addr, Api, BankMsg, Coin, ConversionOverflowError, CosmosMsg, - CustomMsg, CustomQuery, Decimal256, Fraction, MessageInfo, QuerierWrapper, StdError, StdResult, - Uint128, Uint256, WasmMsg, + coin, coins, ensure, to_binary, wasm_execute, Addr, Api, BankMsg, Coin, + ConversionOverflowError, CosmosMsg, CustomMsg, CustomQuery, Decimal256, Fraction, MessageInfo, + QuerierWrapper, ReplyOn, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg, Cw20QueryMsg, Denom, MinterResponse}; use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; use cw_utils::must_pay; use itertools::Itertools; +use crate::cosmwasm_ext::DecimalToInteger; use crate::factory::PairType; use crate::pair::QueryMsg as PairQueryMsg; use crate::querier::{ @@ -184,6 +183,55 @@ impl Asset { } } + /// Same as [`Asset::into_msg`] but allows to handle errors/msg response data in contract's reply endpoint. + /// If `reply_params` is None then the reply is disabled. + /// Returns a [`SubMsg`] object. + pub fn into_submsg( + self, + recipient: impl Into, + reply_params: Option<(ReplyOn, u64)>, + ) -> StdResult> + where + T: CustomMsg, + { + let recipient = recipient.into(); + let (reply_on, reply_id) = reply_params.unwrap_or((ReplyOn::Never, 0)); + + match &self.info { + AssetInfo::Token { contract_addr } => { + let inner_msg = wasm_execute( + contract_addr, + &Cw20ExecuteMsg::Transfer { + recipient, + amount: self.amount, + }, + vec![], + )?; + + Ok(SubMsg { + id: reply_id, + msg: inner_msg.into(), + gas_limit: None, + reply_on, + }) + } + AssetInfo::NativeToken { denom } => { + let bank_msg = BankMsg::Send { + to_address: recipient, + amount: coins(self.amount.u128(), denom), + } + .into(); + + Ok(SubMsg { + id: reply_id, + msg: bank_msg, + gas_limit: None, + reply_on, + }) + } + } + } + /// Validates an amount of native tokens being sent. pub fn assert_sent_native_token_balance(&self, message_info: &MessageInfo) -> StdResult<()> { if let AssetInfo::NativeToken { denom } = &self.info { @@ -232,41 +280,48 @@ impl CoinsExt for Vec { input_assets: &[Asset], pool_asset_infos: &[AssetInfo], ) -> StdResult<()> { - let pool_coins = pool_asset_infos - .iter() - .filter_map(|asset_info| match asset_info { - AssetInfo::NativeToken { denom } => Some(denom.to_string()), - _ => None, - }) - .collect::>(); + ensure!( + !input_assets.is_empty(), + StdError::generic_err("Empty input assets") + ); - let input_coins = input_assets - .iter() - .filter_map(|asset| match &asset.info { - AssetInfo::NativeToken { denom } => Some((denom.to_string(), asset.amount)), - _ => None, - }) - .map(|pair| { - if pool_coins.contains(&pair.0) { - Ok(pair) - } else { - Err(StdError::generic_err(format!( - "Asset {} is not in the pool", - pair.0 - ))) + ensure!( + input_assets.iter().map(|asset| &asset.info).all_unique(), + StdError::generic_err("Duplicated assets in the input") + ); + + input_assets.iter().try_for_each(|input| { + if pool_asset_infos.contains(&input.info) { + match &input.info { + AssetInfo::NativeToken { denom } => { + let coin = self + .iter() + .find(|coin| coin.denom == *denom) + .cloned() + .unwrap_or_else(|| coin(0, denom)); + if coin.amount != input.amount { + Err(StdError::generic_err( + format!("Native token balance mismatch between the argument ({}{denom}) and the transferred ({}{denom})", input.amount, coin.amount), + )) + } else { + Ok(()) + } + } + AssetInfo::Token { .. } => Ok(()) } - }) - .collect::>>()?; + } else { + Err(StdError::generic_err(format!( + "Asset {} is not in the pool", + input.info + ))) + } + })?; self.iter().try_for_each(|coin| { - if input_coins.contains_key(&coin.denom) { - if input_coins[&coin.denom] == coin.amount { - Ok(()) - } else { - Err(StdError::generic_err( - "Native token balance mismatch between the argument and the transferred", - )) - } + if pool_asset_infos.contains(&AssetInfo::NativeToken { + denom: coin.denom.clone(), + }) { + Ok(()) } else { Err(StdError::generic_err(format!( "Supplied coins contain {} that is not in the input asset vector", @@ -318,8 +373,8 @@ impl KeyDeserialize for &AssetInfo { type Output = AssetInfo; #[inline(always)] - fn from_vec(value: Vec) -> StdResult { - from_slice(&value) + fn from_vec(_value: Vec) -> StdResult { + unimplemented!("Due to lack of knowledge of enum variant in binary there is no way to determine correct AssetInfo") } } @@ -633,6 +688,29 @@ pub fn token_asset_info(contract_addr: Addr) -> AssetInfo { AssetInfo::Token { contract_addr } } +/// This function tries to determine asset info from the given input. +/// +/// **NOTE** +/// - this function relies on the fact that chain doesn't allow to mint native tokens in the form of bech32 addresses. +/// For example, if it is allowed to mint native token `wasm1xxxxxxx` then [`AssetInfo`] will be determined incorrectly; +/// - if you intend to test this functionality in cw-multi-test you must implement [`Api`] trait for your test App +/// with conjunction with [AddressGenerator](https://docs.rs/cw-multi-test/0.17.0/cw_multi_test/trait.AddressGenerator.html) +pub fn determine_asset_info(maybe_asset_info: &str, api: &dyn Api) -> StdResult { + if api.addr_validate(maybe_asset_info).is_ok() { + Ok(AssetInfo::Token { + contract_addr: Addr::unchecked(maybe_asset_info), + }) + } else if validate_native_denom(maybe_asset_info).is_ok() { + Ok(AssetInfo::NativeToken { + denom: maybe_asset_info.to_string(), + }) + } else { + Err(StdError::generic_err(format!( + "Cannot determine asset info from {maybe_asset_info}" + ))) + } +} + /// Returns [`PairInfo`] by specified pool address. /// /// * **pool_addr** address of the pool. @@ -759,11 +837,12 @@ impl Decimal256Ext for Decimal256 { #[cfg(test)] mod tests { - use super::*; use cosmwasm_std::testing::mock_info; use cosmwasm_std::{coin, coins}; use test_case::test_case; + use super::*; + fn mock_cw20() -> Asset { Asset { info: AssetInfo::Token { @@ -819,9 +898,7 @@ mod tests { .unwrap_err(); assert_eq!( err, - StdError::generic_err( - "Supplied coins contain random that is not in the input asset vector" - ) + StdError::generic_err("Native token balance mismatch between the argument (100uluna) and the transferred (0uluna)") ); let assets = [ @@ -846,7 +923,7 @@ mod tests { assert_eq!( err, StdError::generic_err( - "Native token balance mismatch between the argument and the transferred" + "Native token balance mismatch between the argument (1000uluna) and the transferred (100uluna)" ) ); @@ -877,6 +954,59 @@ mod tests { ); } + #[test] + fn test_empty_funds() { + let pool_asset_infos = [ + native_asset_info("uusd".to_string()), + native_asset_info("uluna".to_string()), + ]; + + let err = vec![] + .assert_coins_properly_sent(&[], &pool_asset_infos) + .unwrap_err(); + assert_eq!(err.to_string(), "Generic error: Empty input assets"); + + let assets = [ + pool_asset_infos[0].with_balance(1000u16), + pool_asset_infos[1].with_balance(100u16), + ]; + let err = vec![] + .assert_coins_properly_sent(&assets, &pool_asset_infos) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Native token balance mismatch between the argument (1000uusd) and the transferred (0uusd)" + ); + + let err = vec![assets[0].as_coin().unwrap()] + .assert_coins_properly_sent(&assets, &pool_asset_infos) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Native token balance mismatch between the argument (100uluna) and the transferred (0uluna)" + ); + } + + #[test] + fn test_duplicated_funds() { + let pool_asset_infos = [ + native_asset_info("uusd".to_string()), + native_asset_info("uusd".to_string()), + ]; + + let assets = [ + pool_asset_infos[0].with_balance(1000u16), + pool_asset_infos[1].with_balance(100u16), + ]; + let err = vec![assets[0].as_coin().unwrap(), assets[1].as_coin().unwrap()] + .assert_coins_properly_sent(&assets, &pool_asset_infos) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: Duplicated assets in the input" + ); + } + #[test] fn native_denom_validation() { let err = validate_native_denom("ab").unwrap_err(); diff --git a/packages/astroport/src/incentives.rs b/packages/astroport/src/incentives.rs new file mode 100644 index 000000000..7597965c4 --- /dev/null +++ b/packages/astroport/src/incentives.rs @@ -0,0 +1,475 @@ +use std::hash::{Hash, Hasher}; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin, Decimal, Env, StdError, StdResult, Uint128}; +use cw20::Cw20ReceiveMsg; + +use crate::asset::{Asset, AssetInfo}; + +/// External incentives schedules must be normalized to 1 week +pub const EPOCH_LENGTH: u64 = 86400 * 7; +/// External incentives schedules aligned to start on Monday. First date: Mon Oct 9 00:00:00 UTC 2023 +pub const EPOCHS_START: u64 = 1696809600; +/// Maximum allowed reward schedule duration (~6 month) +pub const MAX_PERIODS: u64 = 25; +/// Maximum allowed external reward tokens per pool +pub const MAX_REWARD_TOKENS: u8 = 5; + +/// Max items per page in queries +pub const MAX_PAGE_LIMIT: u8 = 50; + +/// Max number of orphaned rewards to claim at a time +pub const MAX_ORPHANED_REWARD_LIMIT: u8 = 10; + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, + pub factory: String, + pub astro_token: AssetInfo, + pub vesting_contract: String, + pub incentivization_fee_info: Option, + pub guardian: Option, +} + +#[cw_serde] +pub struct InputSchedule { + pub reward: Asset, + pub duration_periods: u64, +} + +#[cw_serde] +pub struct IncentivesSchedule { + /// Schedule start time (matches with epoch start time i.e. on Monday) + pub next_epoch_start_ts: u64, + /// Schedule end time (matches with epoch start time i.e. on Monday) + pub end_ts: u64, + /// Reward asset info + pub reward_info: AssetInfo, + /// Reward per second for the whole schedule + pub rps: Decimal, +} + +impl IncentivesSchedule { + /// Creates a new incentives schedule starting now and lasting for the specified number of periods. + pub fn from_input(env: &Env, input: &InputSchedule) -> StdResult { + if input.duration_periods > MAX_PERIODS || input.duration_periods == 0 { + return Err(StdError::generic_err(format!( + "Duration must be more 0 and less than or equal to {MAX_PERIODS}", + ))); + } + + let block_ts = env.block.time.seconds(); + + let rem = block_ts % EPOCHS_START; + let next_epoch_start_ts = if rem % EPOCH_LENGTH == 0 { + // Hit at the beginning of the current epoch + block_ts + } else { + // Hit somewhere in the middle. + // Partially distribute rewards for the current epoch and add input.duration_periods periods more + EPOCHS_START + (rem / EPOCH_LENGTH + 1) * EPOCH_LENGTH + }; + let end_ts = next_epoch_start_ts + input.duration_periods * EPOCH_LENGTH; + + let rps = Decimal::from_ratio(input.reward.amount, end_ts - block_ts); + + if rps < Decimal::one() { + return Err(StdError::generic_err(format!( + "Reward per second must be at least 1 unit but actual is {rps}", + ))); + } + + Ok(Self { + next_epoch_start_ts, + end_ts, + reward_info: input.reward.info.clone(), + rps, + }) + } +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Setup generators with their respective allocation points. + /// Only the owner or generator controller can execute this. + SetupPools { + /// The list of pools with allocation point. + pools: Vec<(String, Uint128)>, + }, + /// Update rewards and return it to user. + ClaimRewards { + /// The LP token cw20 address or token factory denom + lp_tokens: Vec, + }, + /// Receives a message of type [`Cw20ReceiveMsg`]. Handles cw20 LP token deposits. + Receive(Cw20ReceiveMsg), + /// Stake LP tokens in the Generator. LP tokens staked on behalf of recipient if recipient is set. + /// Otherwise LP tokens are staked on behalf of message sender. + Deposit { recipient: Option }, + /// Withdraw LP tokens from the Generator + Withdraw { + /// The LP token cw20 address or token factory denom + lp_token: String, + /// The amount to withdraw. Must not exceed total staked amount. + amount: Uint128, + }, + /// Set a new amount of ASTRO to distribute per seconds. + /// Only the owner can execute this. + SetTokensPerSecond { + /// The new amount of ASTRO to distribute per second + amount: Uint128, + }, + /// Incentivize a pool with external rewards. Rewards can be in either native or cw20 form. + /// Incentivizor must send incentivization fee along with rewards (if this reward token is new in this pool). + /// 3rd parties are encouraged to keep endless schedules without breaks even with the small rewards. + /// Otherwise, reward token will be removed from the pool info and go to outstanding rewards. + /// Next schedules with the same token will be considered as "new". + /// NOTE: Sender must approve allowance for cw20 reward tokens to this contract. + Incentivize { + /// The LP token cw20 address or token factory denom + lp_token: String, + /// Incentives schedule + schedule: InputSchedule, + }, + /// Remove specific reward token from the pool. + /// Only the owner can execute this. + RemoveRewardFromPool { + /// The LP token cw20 address or token factory denom + lp_token: String, + /// The reward token cw20 address or token factory denom + reward: String, + /// If there is too much spam in the state, owner can bypass upcoming schedules; + /// Tokens from these schedules will stuck in Generator balance forever. + /// Set true only in emergency cases i.e. if deregistration message hits gas limit during simulation. + /// Default: false + #[serde(default)] + bypass_upcoming_schedules: bool, + /// Receiver of unclaimed rewards + receiver: String, + }, + /// Claim all or up to the limit accumulated orphaned rewards. + /// Only the owner can execute this. + ClaimOrphanedRewards { + /// Number of assets to claim + limit: Option, + /// Receiver of orphaned rewards + receiver: String, + }, + /// Update config. + /// Only the owner can execute it. + UpdateConfig { + /// The new vesting contract address + vesting_contract: Option, + /// The new generator controller contract address + generator_controller: Option, + /// The new generator guardian + guardian: Option, + /// New incentivization fee info + incentivization_fee_info: Option, + }, + /// Add or remove token to the block list. + /// Only owner or guardian can execute this. + /// Pools which contain these tokens can't be incentivized with ASTRO rewards. + /// Also blocked tokens can't be used as external reward. + /// Current active pools with these tokens will be removed from active set. + UpdateBlockedTokenslist { + /// Tokens to add + #[serde(default)] + add: Vec, + /// Tokens to remove + #[serde(default)] + remove: Vec, + }, + /// Only factory can set the allocation points to zero for the specified pool. + /// Initiated from deregistration context in factory. + DeactivatePool { lp_token: String }, + /// Go through active pools and deactivate the ones which pair type is blocked + DeactivateBlockedPools {}, + /// Creates a request to change contract ownership + /// Only the current owner can execute this. + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the proposal to change the contract owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// Only the newly proposed owner can execute this + ClaimOwnership {}, +} + +#[cw_serde] +/// Cw20 hook message template +pub enum Cw20Msg { + Deposit { + recipient: Option, + }, + /// Besides this enum variant is redundant we keep this for backward compatibility with old pair contracts + DepositFor(String), +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns the main contract parameters + #[returns(Config)] + Config {}, + /// Deposit returns the LP token amount deposited in a specific generator + #[returns(Uint128)] + Deposit { lp_token: String, user: String }, + /// PendingToken returns the amount of rewards that can be claimed by an account that deposited a specific LP token in a generator + #[returns(Vec)] + PendingRewards { lp_token: String, user: String }, + /// RewardInfo returns reward information for a specified LP token + #[returns(Vec)] + RewardInfo { lp_token: String }, + /// PoolInfo returns information about a pool associated with the specified LP token + #[returns(PoolInfoResponse)] + PoolInfo { lp_token: String }, + /// Returns a list of tuples with addresses and their staked amount + #[returns(Vec<(String, Uint128)>)] + PoolStakers { + lp_token: String, + start_after: Option, + limit: Option, + }, + /// Returns paginated list of blocked tokens + #[returns(Vec)] + BlockedTokensList { + start_after: Option, + limit: Option, + }, + /// Checks whether fee expected for the specified pool if user wants to add new reward schedule + #[returns(bool)] + IsFeeExpected { lp_token: String, reward: String }, + /// Returns the list of all external reward schedules for the specified LP token + #[returns(Vec)] + ExternalRewardSchedules { + /// Reward cw20 addr/denom + reward: String, + lp_token: String, + /// Start after specified timestamp + start_after: Option, + /// Limit number of returned schedules. + limit: Option, + }, +} + +#[cw_serde] +pub struct IncentivizationFeeInfo { + /// Fee receiver can be either a contract or a wallet. + pub fee_receiver: Addr, + /// To make things easier we avoid CW20 fee tokens + pub fee: Coin, +} + +#[cw_serde] +pub struct Config { + /// Address allowed to change contract parameters + pub owner: Addr, + /// The Factory address + pub factory: Addr, + /// Contract address which can only set active generators and their alloc points + pub generator_controller: Option, + /// [`AssetInfo`] of the ASTRO token + pub astro_token: AssetInfo, + /// Total amount of ASTRO rewards per second + pub astro_per_second: Uint128, + /// Total allocation points. Must be the sum of all allocation points in all active generators + pub total_alloc_points: Uint128, + /// The vesting contract which distributes internal (ASTRO) rewards + pub vesting_contract: Addr, + /// The guardian address which can add or remove tokens from blacklist + pub guardian: Option, + /// Defines native fee along with fee receiver. + /// Fee is paid on adding NEW external reward to a specific pool + pub incentivization_fee_info: Option, +} + +#[cw_serde] +#[derive(Eq)] +/// This enum is a tiny wrapper over [`AssetInfo`] to differentiate between internal and external rewards. +/// External rewards always have a next_update_ts field which is used to update reward per second (or disable them). +pub enum RewardType { + /// Internal rewards aka ASTRO emissions don't have next_update_ts field and they are paid out from Vesting contract. + Int(AssetInfo), + /// External rewards always have corresponding schedules. Reward is paid out from Generator contract balance. + Ext { + info: AssetInfo, + /// Time when next schedule should start + next_update_ts: u64, + }, +} + +impl RewardType { + pub fn is_external(&self) -> bool { + matches!(&self, RewardType::Ext { .. }) + } + + pub fn asset_info(&self) -> &AssetInfo { + match &self { + RewardType::Int(info) | RewardType::Ext { info, .. } => info, + } + } + + pub fn matches(&self, other: &Self) -> bool { + match (&self, other) { + (RewardType::Int(info1), RewardType::Int(info2)) => info1 == info2, + (RewardType::Ext { info: info1, .. }, RewardType::Ext { info: info2, .. }) => { + info1 == info2 + } + _ => false, + } + } +} + +impl Hash for RewardType { + fn hash(&self, state: &mut H) { + // We ignore next_update_ts field to have the same hash for the same external reward token + match self { + RewardType::Int(info) => { + state.write_u8(0); + info.hash(state); + } + RewardType::Ext { info, .. } => { + state.write_u8(1); + info.hash(state); + } + } + } + + #[cfg(not(tarpaulin_include))] + fn hash_slice(data: &[Self], state: &mut H) + where + Self: Sized, + { + for d in data { + d.hash(state); + } + } +} + +#[cw_serde] +pub struct RewardInfo { + /// Defines [`AssetInfo`] of reward token as well as its type: protocol or external. + pub reward: RewardType, + /// Reward tokens per second for the whole pool + pub rps: Decimal, + /// Last checkpointed reward per LP token + pub index: Decimal, + /// Orphaned rewards might appear between the time when pool + /// gets incentivized and the time when first user stakes + pub orphaned: Decimal, +} + +#[cw_serde] +pub struct PoolInfoResponse { + /// Total amount of LP tokens staked in this pool + pub total_lp: Uint128, + /// Vector contains reward info for each reward token + pub rewards: Vec, + /// Last time when reward indexes were updated + pub last_update_ts: u64, +} + +#[cw_serde] +pub struct ScheduleResponse { + pub rps: Decimal, + pub start_ts: u64, + pub end_ts: u64, +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Timestamp; + + use crate::asset::AssetInfoExt; + + use super::*; + + #[test] + fn test_schedules() { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(EPOCHS_START); + + let schedule = IncentivesSchedule::from_input( + &env, + &InputSchedule { + reward: AssetInfo::native("test").with_balance(EPOCH_LENGTH), + duration_periods: 1, + }, + ) + .unwrap(); + + assert_eq!(schedule.next_epoch_start_ts, EPOCHS_START); + assert_eq!(schedule.end_ts, schedule.next_epoch_start_ts + EPOCH_LENGTH); + assert_eq!(schedule.rps, Decimal::one()); + + let err = IncentivesSchedule::from_input( + &env, + &InputSchedule { + reward: AssetInfo::native("test").with_balance(100000000u128), + duration_periods: 0, + }, + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Generic error: Duration must be more 0 and less than or equal to {MAX_PERIODS}" + ) + ); + + let err = IncentivesSchedule::from_input( + &env, + &InputSchedule { + reward: AssetInfo::native("test").with_balance(100000000u128), + duration_periods: MAX_PERIODS + 1, + }, + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + format!( + "Generic error: Duration must be more 0 and less than or equal to {MAX_PERIODS}" + ) + ); + + let err = IncentivesSchedule::from_input( + &env, + &InputSchedule { + reward: AssetInfo::native("test").with_balance(100000u128), + duration_periods: MAX_PERIODS, + }, + ) + .unwrap_err(); + assert!( + err.to_string() + .starts_with("Generic error: Reward per second must be at least 1 unit"), + "Unexpected error: {}", + err.to_string() + ); + + env.block.time = Timestamp::from_seconds(EPOCHS_START + 10 * EPOCH_LENGTH + 3 * 86400); + let schedule = IncentivesSchedule::from_input( + &env, + &InputSchedule { + // 4 days from current week + 21 days more + reward: AssetInfo::native("test").with_balance(25 * 86400u64), + duration_periods: 3, + }, + ) + .unwrap(); + + assert_eq!(schedule.next_epoch_start_ts, 1703462400); + assert_eq!( + schedule.end_ts, + schedule.next_epoch_start_ts + 3 * EPOCH_LENGTH + ); + assert_eq!(schedule.rps, Decimal::one()); + } +} diff --git a/packages/astroport/src/lib.rs b/packages/astroport/src/lib.rs index 461927f14..52da7aff8 100644 --- a/packages/astroport/src/lib.rs +++ b/packages/astroport/src/lib.rs @@ -31,6 +31,7 @@ pub mod xastro_token; #[cfg(test)] mod mock_querier; +pub mod incentives; pub mod liquidity_manager; #[cfg(test)] mod testing; diff --git a/packages/astroport/src/observation.rs b/packages/astroport/src/observation.rs index 052bb570f..18c64b313 100644 --- a/packages/astroport/src/observation.rs +++ b/packages/astroport/src/observation.rs @@ -1,5 +1,8 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{CustomQuery, Decimal, Deps, Env, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{ + CustomQuery, Decimal, Decimal256, Deps, Env, Fraction, StdError, StdResult, Storage, Uint128, + Uint256, +}; use cw_storage_plus::Item; use astroport_circular_buffer::{BufferManager, CircularBuffer}; @@ -14,15 +17,12 @@ pub const OBSERVATIONS_SIZE: u32 = 3000; #[cw_serde] #[derive(Copy, Default)] pub struct Observation { - pub timestamp: u64, - /// Base asset simple moving average (mean) - pub base_sma: Uint128, - /// Base asset amount that was added at this observation - pub base_amount: Uint128, - /// Quote asset simple moving average (mean) - pub quote_sma: Uint128, - /// Quote asset amount that was added at this observation - pub quote_amount: Uint128, + /// Timestamp of the observation + pub ts: u64, + /// Observed price at this point + pub price: Decimal, + /// Price simple moving average (mean) + pub price_sma: Decimal, } #[cw_serde] @@ -68,41 +68,41 @@ where } let newest_obs = buffer.read_single(deps.storage, newest_ind)?.unwrap(); - if target >= newest_obs.timestamp { + if target >= newest_obs.ts { return Ok(OracleObservation { timestamp: target, - price: Decimal::from_ratio(newest_obs.base_amount, newest_obs.quote_amount), + price: newest_obs.price_sma, }); } let oldest_obs = buffer.read_single(deps.storage, oldest_ind)?.unwrap(); - if target == oldest_obs.timestamp { + if target == oldest_obs.ts { return Ok(OracleObservation { timestamp: target, - price: Decimal::from_ratio(oldest_obs.base_amount, oldest_obs.quote_amount), + price: oldest_obs.price_sma, }); } - if target < oldest_obs.timestamp { + if target < oldest_obs.ts { return Err(StdError::generic_err(format!( "Requested observation is too old. Last known observation is at {}", - oldest_obs.timestamp + oldest_obs.ts ))); } let (left, right) = binary_search(deps.storage, &buffer, target, oldest_ind, newest_ind)?; - let price_left = Decimal::from_ratio(left.base_amount, left.quote_amount); - let price_right = Decimal::from_ratio(right.base_amount, right.quote_amount); - let price = if left.timestamp == target { + let price_left = left.price_sma; + let price_right = right.price_sma; + let price = if left.ts == target { price_left - } else if right.timestamp == target { + } else if right.ts == target { price_right } else if price_left == price_right { price_left } else { // Interpolate. - let price_slope = price_right.diff(price_left) - * Decimal::from_ratio(1u8, right.timestamp - left.timestamp); - let time_interval = Decimal::from_ratio(target - left.timestamp, 1u8); + let price_slope = + price_right.diff(price_left) * Decimal::from_ratio(1u8, right.ts - left.ts); + let time_interval = Decimal::from_ratio(target - left.ts, 1u8); if price_left > price_right { price_left - price_slope * time_interval } else { @@ -141,10 +141,10 @@ fn binary_search( )) })?; - if leftward_or_hit.timestamp <= target && target <= rightward_or_hit.timestamp { + if leftward_or_hit.ts <= target && target <= rightward_or_hit.ts { break Ok((leftward_or_hit, rightward_or_hit)); } - if leftward_or_hit.timestamp > target { + if leftward_or_hit.ts > target { end = mid - 1; } else { start = mid + 1; @@ -193,25 +193,65 @@ impl<'a> PrecommitObservation { } } +pub fn try_dec256_into_dec(val: Decimal256) -> StdResult { + let numerator: Uint128 = val.numerator().try_into()?; + + Ok(Decimal::from_ratio(numerator, Decimal::one().denominator())) +} + +/// Internal function to calculate new moving average using Uint256. +/// Overflow is possible only if new average price is greater than 2^128 - 1 which is unlikely. +/// Formula: (sma * count + new_price - oldest_price) / count +pub fn safe_sma_calculation( + price_sma: Decimal, + oldest_price: Decimal, + count: u32, + new_price: Decimal, +) -> StdResult { + let sma_times_count = price_sma.numerator().full_mul(count); + let res = Decimal256::from_ratio( + sma_times_count + Uint256::from(new_price.numerator()) + - Uint256::from(oldest_price.numerator()), + price_sma.denominator().full_mul(count), + ); + + try_dec256_into_dec(res) +} + +/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. +/// Formula: (sma * count + new_price) / (count + 1) +pub fn safe_sma_buffer_not_full( + price_sma: Decimal, + count: u32, + new_price: Decimal, +) -> StdResult { + let sma_times_count = price_sma.numerator().full_mul(count); + let res = Decimal256::from_ratio( + sma_times_count + Uint256::from(new_price.numerator()), + price_sma.denominator().full_mul(count + 1), + ); + + try_dec256_into_dec(res) +} + #[cfg(test)] mod test { - use crate::observation::Observation; use cosmwasm_std::to_binary; + use crate::observation::Observation; + #[test] fn check_observation_size() { // Checking [`Observation`] object size to estimate gas cost let obs = Observation { - timestamp: 0, - base_sma: Default::default(), - base_amount: Default::default(), - quote_sma: Default::default(), - quote_amount: Default::default(), + ts: 0, + price: Default::default(), + price_sma: Default::default(), }; - let storage_bytes = std::mem::size_of_val(&to_binary(&obs).unwrap()); - assert_eq!(storage_bytes, 24); // in storage + let storage_bytes = to_binary(&obs).unwrap().len(); + assert_eq!(storage_bytes, 36); // in storage // https://github.com/cosmos/cosmos-sdk/blob/47f46643affd7ec7978329c42bac47275ac7e1cc/store/types/gas.go#L199 println!("sdk gas cost per read {}", 1000 + storage_bytes * 3); diff --git a/packages/astroport/src/router.rs b/packages/astroport/src/router.rs index 320e5073a..312acd446 100644 --- a/packages/astroport/src/router.rs +++ b/packages/astroport/src/router.rs @@ -65,14 +65,11 @@ pub enum ExecuteMsg { max_spread: Option, single: bool, }, - /// Internal use - /// AssertMinimumReceive checks that a receiver will get a minimum amount of tokens from a swap - AssertMinimumReceive { - asset_info: AssetInfo, - prev_balance: Uint128, - minimum_receive: Uint128, - receiver: String, - }, +} + +#[cw_serde] +pub struct SwapResponseData { + pub return_amount: Uint128, } #[cw_serde] diff --git a/packages/astroport_mocks/Cargo.toml b/packages/astroport_mocks/Cargo.toml index 28cf91f3c..f60b1c294 100644 --- a/packages/astroport_mocks/Cargo.toml +++ b/packages/astroport_mocks/Cargo.toml @@ -19,7 +19,6 @@ astroport-shared-multisig = { path = "../../contracts/periphery/shared_multisig" astroport-pair = { path = "../../contracts/pair" } astroport-pair-stable = { path = "../../contracts/pair_stable" } astroport-pair-concentrated = { path = "../../contracts/pair_concentrated" } -astroport-pair-concentrated-injective = { path = "../../contracts/pair_concentrated_inj" } astroport-staking = { path = "../../contracts/tokenomics/staking" } astroport-token = { path = "../../contracts/token" } astroport-vesting = { path = "../../contracts/tokenomics/vesting" } diff --git a/packages/astroport_mocks/src/lib.rs b/packages/astroport_mocks/src/lib.rs index 0774c30a8..a7e0f2261 100644 --- a/packages/astroport_mocks/src/lib.rs +++ b/packages/astroport_mocks/src/lib.rs @@ -3,13 +3,27 @@ use std::{cell::RefCell, rc::Rc}; use cosmwasm_std::Addr; +pub use cw_multi_test; +use cw_multi_test::{App, Module, WasmKeeper}; + +pub use { + coin_registry::{MockCoinRegistry, MockCoinRegistryBuilder}, + factory::{MockFactory, MockFactoryBuilder}, + generator::{MockGenerator, MockGeneratorBuilder}, + pair::{MockXykPair, MockXykPairBuilder}, + pair_concentrated::{MockConcentratedPair, MockConcentratedPairBuilder}, + pair_stable::{MockStablePair, MockStablePairBuilder}, + staking::{MockStaking, MockStakingBuilder}, + token::{MockToken, MockTokenBuilder}, + vesting::{MockVesting, MockVestingBuilder}, + xastro::{MockXastro, MockXastroBuilder}, +}; pub mod coin_registry; pub mod factory; pub mod generator; pub mod pair; pub mod pair_concentrated; -pub mod pair_concentrated_inj; pub mod pair_stable; pub mod shared_multisig; pub mod staking; @@ -24,22 +38,6 @@ pub fn astroport_address() -> Addr { Addr::unchecked(ASTROPORT) } -pub use cw_multi_test; -use cw_multi_test::{App, Module, WasmKeeper}; -pub use { - coin_registry::{MockCoinRegistry, MockCoinRegistryBuilder}, - factory::{MockFactory, MockFactoryBuilder}, - generator::{MockGenerator, MockGeneratorBuilder}, - pair::{MockXykPair, MockXykPairBuilder}, - pair_concentrated::{MockConcentratedPair, MockConcentratedPairBuilder}, - pair_concentrated_inj::{MockConcentratedPairInj, MockConcentratedPairInjBuilder}, - pair_stable::{MockStablePair, MockStablePairBuilder}, - staking::{MockStaking, MockStakingBuilder}, - token::{MockToken, MockTokenBuilder}, - vesting::{MockVesting, MockVestingBuilder}, - xastro::{MockXastro, MockXastroBuilder}, -}; - pub type WKApp = Rc< RefCell::ExecT, ::QueryT>, X, D, I, G>>, >; diff --git a/packages/astroport_mocks/src/pair_concentrated_inj.rs b/packages/astroport_mocks/src/pair_concentrated_inj.rs deleted file mode 100644 index 6963adb8b..000000000 --- a/packages/astroport_mocks/src/pair_concentrated_inj.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::fmt::Debug; - -use astroport::{ - asset::{Asset, AssetInfo, PairInfo}, - factory::{ExecuteMsg as FactoryExecuteMsg, PairConfig, PairType, QueryMsg as FactoryQueryMsg}, - pair::QueryMsg, - pair_concentrated::ConcentratedPoolParams, - pair_concentrated_inj::{ConcentratedInjObParams, OrderbookConfig}, -}; -use astroport_pair_concentrated_injective::orderbook::utils::calc_market_ids; -use cosmwasm_std::{to_binary, Addr, Api, CustomQuery, Decimal, Storage}; -use cw_multi_test::{Bank, ContractWrapper, Distribution, Executor, Gov, Ibc, Module, Staking}; -use injective_cosmwasm::{InjectiveMsgWrapper, InjectiveQueryWrapper}; -use schemars::JsonSchema; -use serde::de::DeserializeOwned; - -use crate::{ - astroport_address, - factory::{MockFactory, MockFactoryOpt}, - MockFactoryBuilder, MockToken, MockXykPair, WKApp, -}; - -pub fn store_code(app: &WKApp) -> u64 -where - B: Bank, - A: Api, - S: Storage, - C: Module, - X: Staking, - D: Distribution, - I: Ibc, - G: Gov, - C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, - C::QueryT: CustomQuery + DeserializeOwned + 'static, -{ - use astroport_pair_concentrated_injective as cnt; - let contract = Box::new( - ContractWrapper::new( - cnt::contract::execute, - cnt::contract::instantiate, - cnt::queries::query, - ) - .with_reply(cnt::contract::reply), - ); - - app.borrow_mut().store_code(contract) -} - -pub struct MockConcentratedPairInjBuilder { - pub app: WKApp, - pub asset_infos: Vec, - pub factory: MockFactoryOpt, -} - -impl MockConcentratedPairInjBuilder -where - B: Bank, - A: Api, - S: Storage, - C: Module, - X: Staking, - D: Distribution, - I: Ibc, - G: Gov, - C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, - C::QueryT: CustomQuery + DeserializeOwned + 'static, -{ - pub fn new(app: &WKApp) -> Self { - Self { - app: app.clone(), - asset_infos: Default::default(), - factory: None, - } - } - - pub fn with_factory(mut self, factory: &MockFactory) -> Self { - self.factory = Some(MockFactory { - app: self.app.clone(), - address: factory.address.clone(), - }); - self - } - - pub fn with_asset(mut self, asset_info: &AssetInfo) -> Self { - self.asset_infos.push(asset_info.clone()); - self - } - - /// Set init_params to None to use the defaults - pub fn instantiate( - self, - init_params: Option<&ConcentratedInjObParams>, - ) -> MockConcentratedPairInj { - let factory = self - .factory - .unwrap_or_else(|| MockFactoryBuilder::new(&self.app).instantiate()); - - let config = factory.config(); - - if config - .pair_configs - .iter() - .all(|pc| pc.pair_type != PairType::Custom("concentrated_inj_orderbook".to_owned())) - { - let code_id = store_code(&self.app); - self.app - .borrow_mut() - .execute_contract( - astroport_address(), - factory.address.clone(), - &astroport::factory::ExecuteMsg::UpdatePairConfig { - config: PairConfig { - pair_type: PairType::Custom("concentrated_inj_orderbook".to_owned()), - code_id, - is_disabled: false, - total_fee_bps: 30, - maker_fee_bps: 3333, - is_generator_disabled: false, - }, - }, - &[], - ) - .unwrap(); - }; - - let astroport = astroport_address(); - - let market_id = calc_market_ids(&self.asset_infos).unwrap()[0].clone(); - - let default_params = ConcentratedInjObParams { - main_params: ConcentratedPoolParams { - amp: Decimal::from_ratio(40u128, 1u128), - gamma: Decimal::from_ratio(145u128, 1000000u128), - mid_fee: Decimal::from_ratio(26u128, 10000u128), - out_fee: Decimal::from_ratio(45u128, 10000u128), - fee_gamma: Decimal::from_ratio(23u128, 100000u128), - repeg_profit_threshold: Decimal::from_ratio(2u128, 1000000u128), - min_price_scale_delta: Decimal::from_ratio(146u128, 1000000u128), - price_scale: Decimal::one(), - ma_half_time: 600, - track_asset_balances: None, - fee_share: None, - }, - orderbook_config: OrderbookConfig { - market_id, - orders_number: 5, - min_trades_to_avg: 1, - }, - }; - - self.app - .borrow_mut() - .execute_contract( - astroport, - factory.address.clone(), - &FactoryExecuteMsg::CreatePair { - pair_type: PairType::Custom("concentrated_inj_orderbook".to_owned()), - asset_infos: self.asset_infos.to_vec(), - init_params: Some(to_binary(init_params.unwrap_or(&default_params)).unwrap()), - }, - &[], - ) - .unwrap(); - - let res: PairInfo = self - .app - .borrow() - .wrap() - .query_wasm_smart( - &factory.address, - &FactoryQueryMsg::Pair { - asset_infos: self.asset_infos.to_vec(), - }, - ) - .unwrap(); - - MockConcentratedPairInj { - app: self.app, - address: res.contract_addr, - } - } -} - -pub struct MockConcentratedPairInj { - pub app: WKApp, - pub address: Addr, -} - -impl MockConcentratedPairInj -where - B: Bank, - A: Api, - S: Storage, - C: Module, - X: Staking, - D: Distribution, - I: Ibc, - G: Gov, - C::ExecT: Clone + Debug + PartialEq + JsonSchema + DeserializeOwned + 'static, - C::QueryT: CustomQuery + DeserializeOwned + 'static, -{ - pub fn lp_token(&self) -> MockToken { - let res: PairInfo = self - .app - .borrow() - .wrap() - .query_wasm_smart(self.address.to_string(), &QueryMsg::Pair {}) - .unwrap(); - MockToken { - app: self.app.clone(), - address: res.liquidity_token, - } - } - - pub fn provide( - &self, - sender: &Addr, - assets: &[Asset], - slippage_tolerance: Option, - auto_stake: bool, - receiver: impl Into>, - ) { - let xyk = MockXykPair { - app: self.app.clone(), - address: self.address.clone(), - }; - xyk.provide(sender, assets, slippage_tolerance, auto_stake, receiver); - } - - pub fn mint_allow_provide_and_stake(&self, sender: &Addr, assets: &[Asset]) { - let xyk = MockXykPair { - app: self.app.clone(), - address: self.address.clone(), - }; - xyk.mint_allow_provide_and_stake(sender, assets); - } -} diff --git a/packages/astroport_pcl_common/Cargo.toml b/packages/astroport_pcl_common/Cargo.toml index ade4d457f..611352def 100644 --- a/packages/astroport_pcl_common/Cargo.toml +++ b/packages/astroport_pcl_common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-pcl-common" -version = "1.0.0" +version = "1.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -11,7 +11,7 @@ cosmwasm-schema = "1" cw-storage-plus = "1" cw20 = "1" thiserror = "1" -astroport = { path = "../astroport", version = "3" } +astroport = { path = "../astroport", version = "3.7" } astroport-factory = { path = "../../contracts/factory", version = "1.6", features = ["library"] } itertools = "0.11" diff --git a/packages/astroport_pcl_common/src/consts.rs b/packages/astroport_pcl_common/src/consts.rs index bd4ce9269..efa176896 100644 --- a/packages/astroport_pcl_common/src/consts.rs +++ b/packages/astroport_pcl_common/src/consts.rs @@ -26,22 +26,19 @@ pub const TWO: Decimal256 = Decimal256::raw(2000000000000000000); pub const MAX_ITER: usize = 64; /// ## Validation constants -/// 0.001 -pub const MIN_FEE: Decimal = Decimal::raw(1000000000000000); -/// 0.5 -pub const MAX_FEE: Decimal = Decimal::raw(500000000000000000); +/// 0.00005 (0.005%) +pub const MIN_FEE: Decimal = Decimal::raw(50000000000000); +/// 0.01 (1%) +pub const MAX_FEE: Decimal = Decimal::raw(10000000000000000); -/// 1e-8 -pub const FEE_GAMMA_MIN: Decimal = Decimal::raw(10000000000); -/// 0.02 -pub const FEE_GAMMA_MAX: Decimal = Decimal::raw(20000000000000000); +pub const FEE_GAMMA_MIN: Decimal = Decimal::zero(); +pub const FEE_GAMMA_MAX: Decimal = Decimal::one(); pub const REPEG_PROFIT_THRESHOLD_MIN: Decimal = Decimal::zero(); /// 0.01 pub const REPEG_PROFIT_THRESHOLD_MAX: Decimal = Decimal::raw(10000000000000000); -/// 0.00000000001 -pub const PRICE_SCALE_DELTA_MIN: Decimal = Decimal::raw(10000000); +pub const PRICE_SCALE_DELTA_MIN: Decimal = Decimal::zero(); pub const PRICE_SCALE_DELTA_MAX: Decimal = Decimal::one(); pub const MA_HALF_TIME_LIMITS: RangeInclusive = 1..=(7 * 86400); @@ -51,8 +48,8 @@ pub const AMP_MIN: Decimal = Decimal::raw(1e17 as u128); /// 100000 pub const AMP_MAX: Decimal = Decimal::raw(1e23 as u128); -/// 0.0000001 -pub const GAMMA_MIN: Decimal = Decimal::raw(100000000000); +/// 0.00000001 +pub const GAMMA_MIN: Decimal = Decimal::raw(10000000000); /// 0.02 pub const GAMMA_MAX: Decimal = Decimal::raw(20000000000000000); diff --git a/packages/astroport_pcl_common/src/math/math_decimal.rs b/packages/astroport_pcl_common/src/math/math_decimal.rs index e778e8846..775423fa7 100644 --- a/packages/astroport_pcl_common/src/math/math_decimal.rs +++ b/packages/astroport_pcl_common/src/math/math_decimal.rs @@ -4,8 +4,8 @@ use crate::consts::{HALFPOW_TOL, MAX_ITER, N, N_POW2, TOL}; use crate::math::signed_decimal::SignedDecimal256; use itertools::Itertools; -/// Internal constant to increase calculation accuracy. (1000.0) -const PADDING: Decimal256 = Decimal256::raw(1000000000000000000000); +/// Internal constant to increase calculation accuracy. +const PADDING: Decimal256 = Decimal256::raw(1e36 as u128); pub fn geometric_mean(x: &[Decimal256]) -> Decimal256 { (x[0] * x[1]).sqrt() @@ -88,8 +88,8 @@ pub(crate) fn df_dx( let k = a_gamma_pow2 * k0 / gamma_one_k0_pow2; let k0_x = x_r * N_POW2; - let k_x = k0_x * a_gamma_pow2 * (gamma + Decimal256::one() + k0) - / (d_pow2 * gamma_one_k0 * gamma_one_k0_pow2); + let k_x = k0_x * a_gamma_pow2 * (gamma + Decimal256::one() + k0) * PADDING + / (PADDING * d_pow2 * gamma_one_k0 * gamma_one_k0_pow2); (k_x * (x[0] + x[1]) + k) * d + x_r - k_x * d_pow2 } diff --git a/packages/astroport_pcl_common/src/utils.rs b/packages/astroport_pcl_common/src/utils.rs index 4ef08c77a..73df84c7d 100644 --- a/packages/astroport_pcl_common/src/utils.rs +++ b/packages/astroport_pcl_common/src/utils.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ to_binary, wasm_execute, Addr, Api, CosmosMsg, CustomMsg, CustomQuery, Decimal, Decimal256, - Env, Fraction, QuerierWrapper, StdError, StdResult, Uint128, Uint256, + Env, Fraction, QuerierWrapper, StdError, StdResult, Uint128, }; use cw20::Cw20ExecuteMsg; use itertools::Itertools; @@ -258,28 +258,24 @@ pub fn compute_swap( let amp_gamma = config.pool_state.get_amp_gamma(env); let d = calc_d(&ixs, &_gamma)?; - let offer_amount = if offer_ind == 1 { - offer_amount * config.pool_state.price_state.price_scale + if offer_ind == 1 { + ixs[offer_ind] += offer_amount * config.pool_state.price_state.price_scale; } else { - offer_amount - }; - - ixs[offer_ind] += offer_amount; + ixs[offer_ind] += offer_amount; + } let new_y = calc_y(&ixs, d, &_gamma, ask_ind)?; let mut dy = ixs[ask_ind] - new_y; ixs[ask_ind] = new_y; - let price = if ask_ind == 1 { + // Derive spread using oracle price + let spread_fee = if ask_ind == 1 { dy /= config.pool_state.price_state.price_scale; - config.pool_state.price_state.price_scale.inv().unwrap() + (offer_amount / config.pool_state.price_state.oracle_price).saturating_sub(dy) } else { - config.pool_state.price_state.price_scale + offer_amount.saturating_sub(dy / config.pool_state.price_state.oracle_price) }; - // Since price_scale moves slower than real price spread fee may become negative - let spread_fee = (offer_amount * price).saturating_sub(dy); - let fee_rate = config.pool_params.fee(&ixs); let total_fee = fee_rate * dy; dy -= total_fee; @@ -392,29 +388,6 @@ where .map(|inner| inner.is_some()) } -/// Internal function to calculate new moving average using Uint256. -/// Overflow is possible only if new average order size is greater than 2^128 - 1 which is unlikely. -pub fn safe_sma_calculation( - sma: Uint128, - oldest_amount: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount) - Uint256::from(oldest_amount)) - .checked_div(count.into())?; - res.try_into().map_err(StdError::from) -} - -/// Same as [`safe_sma_calculation`] but is being used when buffer is not full yet. -pub fn safe_sma_buffer_not_full( - sma: Uint128, - count: u32, - new_amount: Uint128, -) -> StdResult { - let res = (sma.full_mul(count) + Uint256::from(new_amount)).checked_div((count + 1).into())?; - res.try_into().map_err(StdError::from) -} - #[cfg(test)] mod tests { use std::error::Error;