diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22bdab4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# IDEA +.idea + +# Key folders +neardev +.neardev + +**/nodemodules +**/*.lock.json diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7ec134d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1293 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.4", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac", + "digest", + "opaque-debug", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive", + "hashbrown", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "byte-slice-cast" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c751592b77c499e7bce34d99d67c2c11bdc0574e9a488ddade14150a4698" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "c2-chacha" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27dae93fe7b1e0424dc57179ac396908c26b035a87234809f5c4dfd1b47dc80" +dependencies = [ + "cipher", + "ppv-lite86", +] + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "easy-ext" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" + +[[package]] +name = "ed25519" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74e1069e39f1454367eb2de793ed062fac4c35c2934b76a81d90dd9abcd28816" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "byteorder", + "rand 0.8.4", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-codec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dacb10c5b3bb92d46ba347505a9041e676bb20ad220101326bffb0c93031ee" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "keccak" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "near-account-id" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd61f43cedc1bb7a7c097ef3adbab0092a51185dca0e48d5851b37818e13978" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "near-crypto" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f68d8d55bd2a457eba5956d8c1255e288c47f394ca6fffe6184d19664bf0bc4c" +dependencies = [ + "arrayref", + "blake2", + "borsh", + "bs58", + "c2-chacha", + "curve25519-dalek", + "derive_more", + "ed25519-dalek", + "lazy_static", + "libc", + "near-account-id", + "parity-secp256k1", + "primitive-types", + "rand 0.7.3", + "rand_core 0.5.1", + "serde", + "serde_json", + "subtle", + "thiserror", +] + +[[package]] +name = "near-primitives" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d93aaf84ad4f5ccf780d8a0029c6fb636a2e6c1ddb3c772dee4686529e30a8" +dependencies = [ + "base64 0.13.0", + "borsh", + "bs58", + "byteorder", + "bytesize", + "chrono", + "derive_more", + "easy-ext", + "hex", + "near-crypto", + "near-primitives-core", + "near-rpc-error-macro", + "near-vm-errors", + "num-rational", + "primitive-types", + "rand 0.7.3", + "reed-solomon-erasure", + "regex", + "serde", + "serde_json", + "sha2", + "smart-default", + "validator", +] + +[[package]] +name = "near-primitives-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d88b2b0f418c1174214fb51106c90251f61ecfe4c7063f149c2e199ec2850fd" +dependencies = [ + "base64 0.11.0", + "borsh", + "bs58", + "derive_more", + "hex", + "lazy_static", + "near-account-id", + "num-rational", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "near-rpc-error-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a98c9bd7edee4dcfc293e3511e9fab826bcd6fbfbfe06124a1164b94ee60351" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "near-rpc-error-macro" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abade92d0fc76a6c25aeb82f3e7fd97678ab5d0fd92b80149a65d1088e86505" +dependencies = [ + "near-rpc-error-core", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "near-sdk" +version = "4.0.0-pre.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f17c763e91999827a2ad30b909f79e82a56c448bf7170ed72841756397e5a3" +dependencies = [ + "base64 0.13.0", + "borsh", + "bs58", + "near-primitives-core", + "near-sdk-macros", + "near-sys", + "near-vm-logic", + "serde", + "serde_json", + "wee_alloc", +] + +[[package]] +name = "near-sdk-macros" +version = "4.0.0-pre.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07b8d59302bf900707c41654a7ba178c5a2d8040a8812647f6b7e7e28dc5b1" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "near-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6a7aa3f46fac44416d8a93d14f30a562c4d730a1c6bf14bffafab5f475c244a" + +[[package]] +name = "near-vm-errors" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e781248bed1f8e4792aee0c0362cf8bc831fc9f51573bc43807b5e07e0e7db81" +dependencies = [ + "borsh", + "hex", + "near-account-id", + "near-rpc-error-macro", + "serde", +] + +[[package]] +name = "near-vm-logic" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c06b3cb3ccf0423a9ba6908ccf07abe19c3c34319af200c733f34b7ac5af0ab" +dependencies = [ + "base64 0.13.0", + "borsh", + "bs58", + "byteorder", + "near-account-id", + "near-crypto", + "near-primitives", + "near-primitives-core", + "near-vm-errors", + "ripemd160", + "serde", + "sha2", + "sha3", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parity-scale-codec" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parity-secp256k1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fca4f82fccae37e8bbdaeb949a4a218a1bbc485d11598f193d2a908042e5fc1" +dependencies = [ + "arrayvec 0.5.2", + "cc", + "cfg-if 0.1.10", + "rand 0.7.3", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "reed-solomon-erasure" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a415a013dd7c5d4221382329a5a3482566da675737494935cbbbcdec04662f9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ripemd160" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "semver" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer", + "digest", + "keccak", + "opaque-debug", +] + +[[package]] +name = "signature" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sputnikdao" +version = "0.1.0" +dependencies = [ + "near-sdk", +] + +[[package]] +name = "sputnikdao-factory" +version = "0.1.0" +dependencies = [ + "near-sdk", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "validator" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d6937c33ec6039d8071bcf72933146b5bbe378d645d8fa59bdadabfc2a249" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ac12273 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +# include a member for each contract +members = [ + "sputnikdao", + "sputnikdao-factory" +] + +[profile.release] +codegen-units = 1 +# Tell `rustc` to optimize for small code size. +opt-level = "z" +lto = true +debug = true +panic = "abort" +# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801 +overflow-checks = true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ae288db --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 near-daos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..401de4e --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# !V1 ARCHIVE! + +### IMPORTANT: This repo is an archive! + +Please see [Sputnik DAO Contracts](https://github.com/near-daos/sputnik-dao-contract) repo for the evergreen version of these contracts. This repo has been archived and made available for reference only. + +---- + +# Sputnik DAO Contracts + +A simple version of a DAO to give out tips, bounties and grants. +Allows anyone to send a proposal to reward other people with funds and get a council to vote for it. + +The major difference with Moloch DAO design is that this contract would receive its function via donation and council has equal rights. + +Spec for v1: + - Contract contains all the $NEAR in itself. It's initialized with it or receives later in form of donation. + - There are council members: list of accounts that can vote for various activity. All council members have equal weight. + - Next methods are available that can be called by anyone who attaches `bond` $NEAR (to prevent spam): + - Add new council member + - Remove council member + - Given funds to `receiver` for `description` (up to 280 characters) and proposed `amount` + - Finalize proposal + When proposal has passed the require time, anyone can call to finalize it. Rules for passing proposal see below. + - X of votes to approve proposal depends on the "policy": Policy allows to set number of votes required for different amount of funds spent. + - Only council members (or self) can call: + - `vote` for a given proposal. + - ``Finalize proposal can be called + - If this vote achieves >50% of council members saying "YES" - it executes action on success. + - Upgradability with super majority vote of the council + +Voting policy is a list of amounts and number or percentage of votes required. +Where the last number in the list is used for all the non payouts (let's call it MAX_VOTE). + +## Voting rules + +Next rules are used for voting: + - There is a policy that defines for `Payout` proposals at different `amount` how much "YES" votes are required. For non-`Payout` proposals - it's always 1/2 + 1 of council. + - If there is 0 "NO" votes and given "YES" votes - the expiration date updates to current time + cooldown time. + - If there is at least 1 "NO" then MAX_VOTE of "YES" votes is required. + - If there is MAX_VOTE "NO" votes - the proposal gets rejected and bond not returned + - If there is no super majority and time to withdraw have passed - proposal fails and the bond gets returned. + +For example, voting policy: + - `[(0, NumOrRation::Ration(1, 2))]` -- meaning that for any amount or vote MAX_VOTE = 1/2 + 1 is used. + - `[(100, NumOrRation::Number(2)), (10000000, NumOrRation::Ration(2, 3))]` -- if amount is below 100 - 2 votes is enough within grace period. Otherwise MAX_VOTE = 2/3 + 1 to pass any larger proposal or add/remove council members. + +Specific examples: + - If there are 2 councils, with default policy of 50%: proposal needs both of them to vote YES to "Succeed" or both of them to vote NO to be "Rejected". If they vote differently, the vote will be considered "Fail" and `bond` will be returned back to proposer. + +## Use cases + + - A person made a cool video about NEAR Wallet, development IDE, etc. They themself or anyone else can suggest to give them a bounty. + - You saw really cool tweet bashing STATE bill - send that person a bounty (need them to create account though). + - Someone contributed a small PR to one of NEAR libraries. One of maintainers can send them a bounty. + - A person in NEAR Collective went beyond and above - another person in NEAR Collective sent them a grant. + - Another GrantDAO applies for a grant to achieve their longer term goal via distributing to their guild members. + - Validators have their own GrantDAO to fund ping bot or other helpful tools for validators. + +**Even better: fork this code and create a more interesting ways to distribute.** + +Every guild can fork it and expand how this can be made more inclusive or more sophisticated. + +## Needs + + - Nice frontend to visualize past and present proposals, creation of proposal, payouts, stats, etc. + - This needs some form of notification service + +# Development + +Follow general WASM / Rust contract instructions. + +## Deploy to TestNet + +```bash + +# Deploy to new account on TestNet +near dev-deploy res/sputnikdao.wasm + +# Set contract Id (fish) +set CONTRACT_ID "dev-1608720833104-8969578" + +# Initialize contract with given council and parameters (this is for testing, where you stil have access key to the contract). +# For production use either a single command or the factory in ../sputnikdao-factory +near call $CONTRACT_ID new '{"purpose": "test", "council": ["testmewell.testnet", "illia"], "bond": "1000000000000000000000000", "vote_period": "1800000000000", "grace_period": "1800000000000"}' --accountId $CONTRACT_ID + +# Get current number of proposals. +near view $CONTRACT_ID get_num_proposals + +# Add new proposal to pay `illia` 1N. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "Payout", "amount": "1000000000000000000000000"}}}' --accountId=illia --amount 1 + +# View proposal #0 +near view $CONTRACT_ID get_proposal '{"id": 0}' +{ + status: 'Vote', + proposer: 'illia', + target: 'illia', + description: 'test', + kind: { Payout: { amount: '1000000000000000000000000' } }, + vote_period_end: 1607497778113967900, + vote_yes: 0, + vote_no: 0, + votes: {} +} + +# Get `limit=1` proposals from id=0 +near view $CONTRACT_ID get_proposals '{"from_index": 0, "limit": 1}' + +# Vote for a proposal #0 `Yes` from `illia` +near call $CONTRACT_ID vote '{"id": 0, "vote": "Yes"}' --accountId illia + +# Vote for a proposal #0 `No` from `testmewell.testnet` +near call $CONTRACT_ID vote '{"id": 0, "vote": "No"}' --accountId testmewell.testnet + +# Proposal to add new council `testnet`. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "testnet", "description": "test", "kind": {"type": "NewCouncil"}}}' --accountId=illia --amount 1 + +# Proposal to remove council `illia`. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": RemoveCouncil"}}}' --accountId=illia --amount 1 + +# Proposal to change vote period to 30min (in nanoseconds): +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangeVotePeriod", "vote_period": "1800000000000"}}}' --accountId=illia --amount 1 + +# Proposal to change purpose of this DAO: +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangePurpose", "purpose": "test me well"}}}' --accountId=illia --amount 1 + +# Proposal to change policy for this DAO, with next voting policy: +# - up until 100N: just need 2 votes +# - up until 1000N: need 3 votes +# - up until 2000N: need 50% + 1 votes +# - for anything larger or other types of proposals need 66% + 1 of votes +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangePolicy", "policy": [{"max_amount": "100", "votes": 2}, {"max_amount": "1000", "votes": 3}, {"max_amount": "2000", "votes": [1, 2]}, {"max_amount": "10000000", "votes": [2, 3]}]}}}' --accountId=illia --amount 1 + +# Finalize a proposal that has no deciding vote and expired. +near call $CONTRACT_ID finalize '{"id": 4}' +``` + +# Ideas for improving + +## Other tokens + +Add support for other tokens in the "bank". +Proposal can then specify amount in a token from whitelisted set. +There can be internal exchange function as well in case it's needed. + +## Bounties + +Bounties management is hard right now and done via github / notion. + +Here is the idea to attach bounties to the same council: + - Anyone can add a bounty: description + how much to pay for the bounty + - Council votes to approve the bounty (same thing with small bounties need less votes) + - There is a list of bounties, separate from requests + - People can indicate that they are working on it + - When someone completed bounty - they ping the bounty for "review" and council votes if the bounty is solved. + - When council voted -> bounty gets paid out + +## Canceling / redirecting proposals + +If proposal is made to a wrong DAO, it's not great to take the bond away from proposer. +It's possible to add an option to transfer proposals from one DAO to another DAO. +Also people can vote to dismiss instead of rejecting it, which will return bond. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..250161e --- /dev/null +++ b/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +cargo +stable build --target wasm32-unknown-unknown --release +cp target/wasm32-unknown-unknown/release/sputnikdao.wasm ./sputnikdao/res/ +cp target/wasm32-unknown-unknown/release/sputnikdao_factory.wasm ./sputnikdao-factory/res/ \ No newline at end of file diff --git a/sputnikdao-factory/Cargo.toml b/sputnikdao-factory/Cargo.toml new file mode 100644 index 0000000..361e344 --- /dev/null +++ b/sputnikdao-factory/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sputnikdao-factory" +version = "0.1.0" +authors = ["Illia Polosukhin "] +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.0.0-pre.4" diff --git a/sputnikdao-factory/README.md b/sputnikdao-factory/README.md new file mode 100644 index 0000000..fef6c90 --- /dev/null +++ b/sputnikdao-factory/README.md @@ -0,0 +1,31 @@ +# SputnikDAO Factory + +# Deployment & Usage + +## TestNet + +``` +near dev-deploy --wasmFile=res/sputnikdao_factory.wasm + +# bash +CONTRACT_ID="dev-1608694678554-8567049" +# fish +set CONTRACT_ID "dev-1608694678554-8567049" + +# Initialize the factory. +near call $CONTRACT_ID new '{}' --accountId $CONTRACT_ID + +# bash +ARGS=`echo '{"purpose": "test", "council": ["testmewell.testnet", "illia"], "bond": "1000000000000000000000000", "vote_period": "1800000000000", "grace_period": "1800000000000"}' | base64` +# fish +set ARGS (echo '{"purpose": "test", "council": ["testmewell.testnet", "illia"], "bond": "1000000000000000000000000", "vote_period": "1800000000000", "grace_period": "1800000000000"}' | base64) + +# Create a new DAO with the given parameters. +near call $CONTRACT_ID create "{\"name\": \"test\", \"public_key\": null, \"args\": \"$ARGS\"}" --accountId $CONTRACT_ID --amount 30 --gas 100000000000000 + +# Create a new DAO with the given parameters while having Full Access Key to the account (trusted, but useful in case of testing or upgrades) +near call $CONTRACT_ID create "{\"name\": \"test\", \"public_key\": \"\", \"args\": \"$ARGS\"}" --accountId $CONTRACT_ID --amount 30 --gas 100000000000000 + +# List all created DAOs. +near view $CONTRACT_ID get_dao_list +``` diff --git a/sputnikdao-factory/build.sh b/sputnikdao-factory/build.sh new file mode 100755 index 0000000..c38641f --- /dev/null +++ b/sputnikdao-factory/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +RUSTFLAGS='-C link-arg=-s' cargo +stable build --target wasm32-unknown-unknown --release +cp ../target/wasm32-unknown-unknown/release/sputnikdao_factory.wasm ./res/ diff --git a/sputnikdao-factory/res/sputnikdao_factory.wasm b/sputnikdao-factory/res/sputnikdao_factory.wasm new file mode 100755 index 0000000..9b5e364 Binary files /dev/null and b/sputnikdao-factory/res/sputnikdao_factory.wasm differ diff --git a/sputnikdao-factory/src/lib.rs b/sputnikdao-factory/src/lib.rs new file mode 100644 index 0000000..6f590a7 --- /dev/null +++ b/sputnikdao-factory/src/lib.rs @@ -0,0 +1,95 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::UnorderedSet; +use near_sdk::json_types::Base64VecU8; +use near_sdk::{env, near_bindgen, AccountId, Gas, Promise, PublicKey}; + +const CODE: &[u8] = include_bytes!("../../sputnikdao/res/sputnikdao.wasm"); + +/// This gas spent on the call & account creation, the rest goes to the `new` call. +const CREATE_CALL_GAS: Gas = Gas(40_000_000_000_000); + +#[near_bindgen] +#[derive(BorshSerialize, BorshDeserialize)] +pub struct SputnikDAOFactory { + daos: UnorderedSet, +} + +impl Default for SputnikDAOFactory { + fn default() -> Self { + env::panic_str("SputnikDAOFactory should be initialized before usage") + } +} + +#[near_bindgen] +impl SputnikDAOFactory { + #[init] + pub fn new() -> Self { + assert!(!env::state_exists(), "The contract is already initialized"); + Self { + daos: UnorderedSet::new(b"d".to_vec()), + } + } + + pub fn get_dao_list(&self) -> Vec { + self.daos.to_vec() + } + + #[payable] + pub fn create( + &mut self, + name: AccountId, + public_key: Option, + args: Base64VecU8, + ) -> Promise { + let account_id: AccountId = format!("{}.{}", name, env::current_account_id()) + .parse() + .unwrap(); + self.daos.insert(&account_id); + let mut promise = Promise::new(account_id) + .create_account() + .deploy_contract(CODE.to_vec()) + .transfer(env::attached_deposit()); + if let Some(key) = public_key { + promise = promise.add_full_access_key(key.into()) + } + promise.function_call( + "new".to_string(), + args.into(), + 0, + env::prepaid_gas() - CREATE_CALL_GAS, + ) + } +} + +#[cfg(test)] +mod tests { + use near_sdk::testing_env; + + use super::*; + use near_sdk::test_utils::{accounts, VMContextBuilder}; + + #[test] + fn test_basics() { + testing_env!(VMContextBuilder::new() + .current_account_id(accounts(0)) + .build()); + let mut factory = SputnikDAOFactory::new(); + testing_env!(VMContextBuilder::new() + .current_account_id(accounts(0)) + .attached_deposit(10) + .build()); + factory.create( + "test".parse().unwrap(), + Some( + "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp" + .parse() + .unwrap(), + ), + "{}".as_bytes().to_vec().into(), + ); + assert_eq!( + factory.get_dao_list(), + vec![format!("test.{}", accounts(0)).parse().unwrap()] + ); + } +} diff --git a/sputnikdao/Cargo.toml b/sputnikdao/Cargo.toml new file mode 100644 index 0000000..819ccc6 --- /dev/null +++ b/sputnikdao/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sputnikdao" +version = "0.1.0" +authors = ["Illia Polosukhin "] +edition = "2018" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +near-sdk = "4.0.0-pre.4" diff --git a/sputnikdao/README.md b/sputnikdao/README.md new file mode 100644 index 0000000..5a01d2f --- /dev/null +++ b/sputnikdao/README.md @@ -0,0 +1,155 @@ +# Sputnik DAO Contracts + +A simple version of a DAO to give out tips, bounties and grants. +Allows anyone to send a proposal to reward other people with funds and get a council to vote for it. + +The major difference with Moloch DAO design is that this contract would receive its function via donation and council has equal rights. + +Spec for v1: + - Contract contains all the $NEAR in itself. It's initialized with it or receives later in form of donation. + - There are council members: list of accounts that can vote for various activity. All council members have equal weight. + - Next methods are available that can be called by anyone who attaches `bond` $NEAR (to prevent spam): + - Add new council member + - Remove council member + - Given funds to `receiver` for `description` (up to 280 characters) and proposed `amount` + - Finalize proposal + When proposal has passed the require time, anyone can call to finalize it. Rules for passing proposal see below. + - X of votes to approve proposal depends on the "policy": Policy allows to set number of votes required for different amount of funds spent. + - Only council members (or self) can call: + - `vote` for a given proposal. + - ``Finalize proposal can be called + - If this vote achieves >50% of council members saying "YES" - it executes action on success. + - Upgradability with super majority vote of the council + +Voting policy is a list of amounts and number or percentage of votes required. +Where the last number in the list is used for all the non payouts (let's call it MAX_VOTE). + +## Voting rules + +Next rules are used for voting: + - There is a policy that defines for `Payout` proposals at different `amount` how much "YES" votes are required. For non-`Payout` proposals - it's always 1/2 + 1 of council. + - If there is 0 "NO" votes and given "YES" votes - the expiration date updates to current time + cooldown time. + - If there is at least 1 "NO" then MAX_VOTE of "YES" votes is required. + - If there is MAX_VOTE "NO" votes - the proposal gets rejected and bond not returned + - If there is no super majority and time to withdraw have passed - proposal fails and the bond gets returned. + +For example, voting policy: + - `[(0, NumOrRation::Ration(1, 2))]` -- meaning that for any amount or vote MAX_VOTE = 1/2 + 1 is used. + - `[(100, NumOrRation::Number(2)), (10000000, NumOrRation::Ration(2, 3))]` -- if amount is below 100 - 2 votes is enough within grace period. Otherwise MAX_VOTE = 2/3 + 1 to pass any larger proposal or add/remove council members. + +Specific examples: + - If there are 2 councils, with default policy of 50%: proposal needs both of them to vote YES to "Succeed" or both of them to vote NO to be "Rejected". If they vote differently, the vote will be considered "Fail" and `bond` will be returned back to proposer. + +## Use cases + + - A person made a cool video about NEAR Wallet, development IDE, etc. They themself or anyone else can suggest to give them a bounty. + - You saw really cool tweet bashing STATE bill - send that person a bounty (need them to create account though). + - Someone contributed a small PR to one of NEAR libraries. One of maintainers can send them a bounty. + - A person in NEAR Collective went beyond and above - another person in NEAR Collective sent them a grant. + - Another GrantDAO applies for a grant to achieve their longer term goal via distributing to their guild members. + - Validators have their own GrantDAO to fund ping bot or other helpful tools for validators. + +**Even better: fork this code and create a more interesting ways to distribute.** + +Every guild can fork it and expand how this can be made more inclusive or more sophisticated. + +## Needs + + - Nice frontend to visualize past and present proposals, creation of proposal, payouts, stats, etc. + - This needs some form of notification service + +# Development + +Follow general WASM / Rust contract instructions. + +## Deploy to TestNet + +```bash + +# Deploy to new account on TestNet +near dev-deploy res/sputnikdao.wasm + +# Set contract Id (fish) +set CONTRACT_ID "dev-1608720833104-8969578" + +# Initialize contract with given council and parameters (this is for testing, where you stil have access key to the contract). +# For production use either a single command or the factory in ../sputnikdao-factory +near call $CONTRACT_ID new '{"purpose": "test", "council": ["testmewell.testnet", "illia"], "bond": "1000000000000000000000000", "vote_period": "1800000000000", "grace_period": "1800000000000"}' --accountId $CONTRACT_ID + +# Get current number of proposals. +near view $CONTRACT_ID get_num_proposals + +# Add new proposal to pay `illia` 1N. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "Payout", "amount": "1000000000000000000000000"}}}' --accountId=illia --amount 1 + +# View proposal #0 +near view $CONTRACT_ID get_proposal '{"id": 0}' +{ + status: 'Vote', + proposer: 'illia', + target: 'illia', + description: 'test', + kind: { Payout: { amount: '1000000000000000000000000' } }, + vote_period_end: 1607497778113967900, + vote_yes: 0, + vote_no: 0, + votes: {} +} + +# Get `limit=1` proposals from id=0 +near view $CONTRACT_ID get_proposals '{"from_index": 0, "limit": 1}' + +# Vote for a proposal #0 `Yes` from `illia` +near call $CONTRACT_ID vote '{"id": 0, "vote": "Yes"}' --accountId illia + +# Vote for a proposal #0 `No` from `testmewell.testnet` +near call $CONTRACT_ID vote '{"id": 0, "vote": "No"}' --accountId testmewell.testnet + +# Proposal to add new council `testnet`. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "testnet", "description": "test", "kind": {"type": "NewCouncil"}}}' --accountId=illia --amount 1 + +# Proposal to remove council `illia`. +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": RemoveCouncil"}}}' --accountId=illia --amount 1 + +# Proposal to change vote period to 30min (in nanoseconds): +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangeVotePeriod", "vote_period": "1800000000000"}}}' --accountId=illia --amount 1 + +# Proposal to change purpose of this DAO: +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangePurpose", "purpose": "test me well"}}}' --accountId=illia --amount 1 + +# Proposal to change policy for this DAO, with next voting policy: +# - up until 100N: just need 2 votes +# - up until 1000N: need 3 votes +# - up until 2000N: need 50% + 1 votes +# - for anything larger or other types of proposals need 66% + 1 of votes +near call $CONTRACT_ID add_proposal '{"proposal": {"target": "illia", "description": "test", "kind": {"type": "ChangePolicy", "policy": [{"max_amount": "100", "votes": 2}, {"max_amount": "1000", "votes": 3}, {"max_amount": "2000", "votes": [1, 2]}, {"max_amount": "10000000", "votes": [2, 3]}]}}}' --accountId=illia --amount 1 + +# Finalize a proposal that has no deciding vote and expired. +near call $CONTRACT_ID finalize '{"id": 4}' +``` + +# Ideas for improving + +## Other tokens + +Add support for other tokens in the "bank". +Proposal can then specify amount in a token from whitelisted set. +There can be internal exchange function as well in case it's needed. + +## Bounties + +Bounties management is hard right now and done via github / notion. + +Here is the idea to attach bounties to the same council: + - Anyone can add a bounty: description + how much to pay for the bounty + - Council votes to approve the bounty (same thing with small bounties need less votes) + - There is a list of bounties, separate from requests + - People can indicate that they are working on it + - When someone completed bounty - they ping the bounty for "review" and council votes if the bounty is solved. + - When council voted -> bounty gets paid out + +## Canceling / redirecting proposals + +If proposal is made to a wrong DAO, it's not great to take the bond away from proposer. +It's possible to add an option to transfer proposals from one DAO to another DAO. +Also people can vote to dismiss instead of rejecting it, which will return bond. diff --git a/sputnikdao/build.sh b/sputnikdao/build.sh new file mode 100755 index 0000000..c9032e7 --- /dev/null +++ b/sputnikdao/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e + +RUSTFLAGS='-C link-arg=-s' cargo +stable build --target wasm32-unknown-unknown --release +cp ../target/wasm32-unknown-unknown/release/sputnikdao.wasm ./res/ diff --git a/sputnikdao/res/sputnikdao.wasm b/sputnikdao/res/sputnikdao.wasm new file mode 100755 index 0000000..0388530 Binary files /dev/null and b/sputnikdao/res/sputnikdao.wasm differ diff --git a/sputnikdao/src/bounty.rs b/sputnikdao/src/bounty.rs new file mode 100644 index 0000000..7e301ad --- /dev/null +++ b/sputnikdao/src/bounty.rs @@ -0,0 +1,32 @@ +use std::collections::HashSet; + +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +enum BountyStatus { + Open, + /// When was bounty claimed and by whom. + Claimed { account_id: AccountId, started: Timestamp }, + /// Bounty is done by given account, review started at given time. + InReview { account_id: AccountId, started: Timestamp }, + /// Bounty is done and closed. + Done, + /// Review expired, will pay out. + Expired, +} + +/// Stores information about bounties that this DAO has open. +/// Bounty can be `Open`, `InProgress`, `Ready` +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Bounty { + /// Status of the given bounty. + status: BountyStatus, + description: String, + /// Maximum how long should this bounty take. + duration: Duration, + /// Applicants. + /// TODO: add details and their proposed duration? + applicants: HashSet, +} diff --git a/sputnikdao/src/lib.rs b/sputnikdao/src/lib.rs new file mode 100644 index 0000000..02c62ca --- /dev/null +++ b/sputnikdao/src/lib.rs @@ -0,0 +1,722 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{UnorderedSet, Vector}; +use near_sdk::json_types::{U128, U64}; +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{env, near_bindgen, AccountId, Balance, Duration, Promise}; +use std::collections::HashMap; + +const MAX_DESCRIPTION_LENGTH: usize = 280; + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub enum Vote { + Yes, + No, +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +#[serde(untagged)] +pub enum NumOrRatio { + Number(u64), + Ratio(u64, u64), +} + +impl NumOrRatio { + pub fn as_ratio(&self) -> Option<(u64, u64)> { + match self { + NumOrRatio::Number(_) => None, + NumOrRatio::Ratio(a, b) => Some((*a, *b)), + } + } +} + +/// Policy item, defining how many votes required to approve up to this much amount. +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[serde(crate = "near_sdk::serde")] +pub struct PolicyItem { + pub max_amount: U64, + pub votes: NumOrRatio, +} + +impl PolicyItem { + pub fn num_votes(&self, num_council: u64) -> u64 { + match self.votes { + NumOrRatio::Number(num_votes) => num_votes, + NumOrRatio::Ratio(l, r) => std::cmp::min(num_council * l / r + 1, num_council), + } + } +} + +fn vote_requirement(policy: &[PolicyItem], num_council: u64, amount: Option) -> u64 { + if let Some(amount) = amount { + // TODO: replace with binary search. + for item in policy { + if u128::from(item.max_amount.0) > amount { + return item.num_votes(num_council); + } + } + } + policy[policy.len() - 1].num_votes(num_council) +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Debug, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(Eq))] +#[serde(crate = "near_sdk::serde")] +pub enum ProposalStatus { + /// Proposal is in active voting stage. + Vote, + /// Proposal has successfully passed. + Success, + /// Proposal was rejected by the vote. + Reject, + /// Vote for proposal has failed due (not enough votes). + Fail, + /// Given voting policy, the uncontested minimum of votes was acquired. + /// Delaying the finalization of the proposal to check that there is no contenders (who would vote against). + Delay, +} + +impl ProposalStatus { + pub fn is_finalized(&self) -> bool { + self != &ProposalStatus::Vote && self != &ProposalStatus::Delay + } +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +#[serde(tag = "type")] +pub enum ProposalKind { + NewCouncil, + RemoveCouncil, + Payout { amount: U64 }, + ChangeVotePeriod { vote_period: U64 }, + ChangeBond { bond: U128 }, + ChangePolicy { policy: Vec }, + ChangePurpose { purpose: String }, +} + +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Proposal { + status: ProposalStatus, + proposer: AccountId, + target: AccountId, + description: String, + kind: ProposalKind, + vote_period_end: Duration, + vote_yes: u64, + vote_no: u64, + votes: HashMap, +} + +impl Proposal { + pub fn get_amount(&self) -> Option { + match self.kind { + ProposalKind::Payout { amount } => Some(amount.0.into()), + _ => None, + } + } + + /// Compute new vote status given council size and current timestamp. + pub fn vote_status(&self, policy: &[PolicyItem], num_council: u64) -> ProposalStatus { + let votes_required = vote_requirement(policy, num_council, self.get_amount()); + let max_votes = policy[policy.len() - 1].num_votes(num_council); + if self.vote_yes >= max_votes { + ProposalStatus::Success + } else if self.vote_yes >= votes_required && self.vote_no == 0 { + if env::block_timestamp() > self.vote_period_end { + ProposalStatus::Success + } else { + ProposalStatus::Delay + } + } else if self.vote_no >= max_votes { + ProposalStatus::Reject + } else if env::block_timestamp() > self.vote_period_end + || self.vote_yes + self.vote_no == num_council + { + ProposalStatus::Fail + } else { + ProposalStatus::Vote + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub struct ProposalInput { + target: AccountId, + description: String, + kind: ProposalKind, +} + +#[near_bindgen] +#[derive(BorshSerialize, BorshDeserialize)] +pub struct SputnikDAO { + purpose: String, + bond: Balance, + vote_period: Duration, + grace_period: Duration, + policy: Vec, + council: UnorderedSet, + proposals: Vector, +} + +impl Default for SputnikDAO { + fn default() -> Self { + env::panic_str("SputnikDAO should be initialized before usage") + } +} + +#[near_bindgen] +impl SputnikDAO { + #[init] + pub fn new( + purpose: String, + council: Vec, + bond: U128, + vote_period: U64, + grace_period: U64, + ) -> Self { + assert!(!env::state_exists(), "The contract is already initialized"); + + let mut dao = Self { + purpose, + bond: bond.into(), + vote_period: vote_period.into(), + grace_period: grace_period.into(), + policy: vec![PolicyItem { + max_amount: 0.into(), + votes: NumOrRatio::Ratio(1, 2), + }], + council: UnorderedSet::new(b"c".to_vec()), + proposals: Vector::new(b"p".to_vec()), + }; + for account_id in council { + dao.council.insert(&account_id); + } + dao + } + + #[payable] + pub fn add_proposal(&mut self, proposal: ProposalInput) -> u64 { + // TODO: add also extra storage cost for the proposal itself. + assert!(env::attached_deposit() >= self.bond, "Not enough deposit"); + assert!( + proposal.description.len() < MAX_DESCRIPTION_LENGTH, + "Description length is too long" + ); + // Input verification. + match proposal.kind { + ProposalKind::ChangePolicy { ref policy } => { + assert_ne!(policy.len(), 0, "Policy shouldn't be empty"); + for i in 1..policy.len() { + assert!( + policy[i].max_amount.0 > policy[i - 1].max_amount.0, + "Policy must be sorted, item {} is wrong", + i + ); + } + let last_ratio = policy[policy.len() - 1] + .votes + .as_ratio() + .expect("Last item in policy must be a ratio"); + assert!( + last_ratio.0 * 2 / last_ratio.1 >= 1, + "Last item in policy must be equal or above 1/2 ratio" + ); + } + _ => {} + } + let p = Proposal { + status: ProposalStatus::Vote, + proposer: env::predecessor_account_id(), + target: proposal.target, + description: proposal.description, + kind: proposal.kind, + vote_period_end: env::block_timestamp() + self.vote_period, + vote_yes: 0, + vote_no: 0, + votes: HashMap::default(), + }; + self.proposals.push(&p); + self.proposals.len() - 1 + } + + pub fn get_vote_period(&self) -> U64 { + self.vote_period.into() + } + + pub fn get_bond(&self) -> U128 { + self.bond.into() + } + + pub fn get_council(&self) -> Vec { + self.council.to_vec() + } + + pub fn get_num_proposals(&self) -> u64 { + self.proposals.len() + } + + pub fn get_proposals(&self, from_index: u64, limit: u64) -> Vec { + (from_index..std::cmp::min(from_index + limit, self.proposals.len())) + .map(|index| self.proposals.get(index).unwrap()) + .collect() + } + + pub fn get_proposals_by_status( + &self, + status: ProposalStatus, + from_index: u64, + limit: u64, + ) -> HashMap { + let filtered_proposal_ids: Vec = (0..self.proposals.len()) + .filter(|index| self.proposals.get(index.clone()).unwrap().status == status) + .collect(); + + (from_index..std::cmp::min(from_index + limit, filtered_proposal_ids.len() as u64)) + .map(|index| { + let proposal_id: u64 = filtered_proposal_ids[index as usize]; + (proposal_id, self.proposals.get(proposal_id).unwrap()) + }) + .collect() + } + + pub fn get_proposals_by_statuses( + &self, + statuses: Vec, + from_index: u64, + limit: u64, + ) -> HashMap { + let filtered_proposal_ids: Vec = (0..self.proposals.len()) + .filter(|index| statuses.contains(&self.proposals.get(index.clone()).unwrap().status)) + .collect(); + + (from_index..std::cmp::min(from_index + limit, filtered_proposal_ids.len() as u64)) + .map(|index| { + let proposal_id: u64 = filtered_proposal_ids[index as usize]; + (proposal_id, self.proposals.get(proposal_id).unwrap()) + }) + .collect() + } + + pub fn get_proposal(&self, id: u64) -> Proposal { + self.proposals.get(id).expect("Proposal not found") + } + + pub fn get_purpose(&self) -> String { + self.purpose.clone() + } + + pub fn vote(&mut self, id: u64, vote: Vote) { + assert!( + self.council.contains(&env::predecessor_account_id()), + "Only council can vote" + ); + let mut proposal = self.proposals.get(id).expect("No proposal with such id"); + assert_eq!( + proposal.status, + ProposalStatus::Vote, + "Proposal already finalized" + ); + if proposal.vote_period_end < env::block_timestamp() { + env::log_str("Voting period expired, finalizing the proposal"); + self.finalize(id); + return; + } + assert!( + !proposal.votes.contains_key(&env::predecessor_account_id()), + "Already voted" + ); + match vote { + Vote::Yes => proposal.vote_yes += 1, + Vote::No => proposal.vote_no += 1, + } + proposal.votes.insert(env::predecessor_account_id(), vote); + let post_status = proposal.vote_status(&self.policy, self.council.len()); + // If just changed from vote to Delay, adjust the expiration date to grace period. + if !post_status.is_finalized() && post_status != proposal.status { + proposal.vote_period_end = env::block_timestamp() + self.grace_period; + proposal.status = post_status.clone(); + } + self.proposals.replace(id, &proposal); + // Finalize if this vote is done. + if post_status.is_finalized() { + self.finalize(id); + } + } + + pub fn finalize(&mut self, id: u64) { + let mut proposal = self.proposals.get(id).expect("No proposal with such id"); + assert!( + !proposal.status.is_finalized(), + "Proposal already finalized" + ); + proposal.status = proposal.vote_status(&self.policy, self.council.len()); + match proposal.status { + ProposalStatus::Success => { + env::log_str("Vote succeeded"); + let target = proposal.target.clone(); + Promise::new(proposal.proposer.clone()).transfer(self.bond); + match proposal.kind { + ProposalKind::NewCouncil => { + self.council.insert(&target); + } + ProposalKind::RemoveCouncil => { + self.council.remove(&target); + } + ProposalKind::Payout { amount } => { + Promise::new(target).transfer(amount.0.into()); + } + ProposalKind::ChangeVotePeriod { vote_period } => { + self.vote_period = vote_period.into(); + } + ProposalKind::ChangeBond { bond } => { + self.bond = bond.into(); + } + ProposalKind::ChangePolicy { ref policy } => { + self.policy = policy.clone(); + } + ProposalKind::ChangePurpose { ref purpose } => { + self.purpose = purpose.clone(); + } + }; + } + ProposalStatus::Reject => { + env::log_str("Proposal rejected"); + } + ProposalStatus::Fail => { + // If no majority vote, let's return the bond. + env::log_str("Proposal vote failed"); + Promise::new(proposal.proposer.clone()).transfer(self.bond); + } + ProposalStatus::Vote | ProposalStatus::Delay => { + env::panic_str("voting period has not expired and no majority vote yet") + } + } + self.proposals.replace(id, &proposal); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use near_sdk::test_utils::{accounts, VMContextBuilder}; + use near_sdk::testing_env; + + fn vote(dao: &mut SputnikDAO, proposal_id: u64, votes: Vec<(usize, Vote)>) { + for (id, vote) in votes { + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(id)) + .build()); + dao.vote(proposal_id, vote); + } + } + + // vec![accounts(0).as_ref(), accounts(1).as_ref()], + #[test] + fn test_basics() { + let mut dao = SputnikDAO::new( + "test".to_string(), + vec![accounts(0), accounts(1)], + 10.into(), + 1_000.into(), + 10.into(), + ); + + assert_eq!(dao.get_bond(), 10.into()); + assert_eq!(dao.get_vote_period(), 1_000.into()); + assert_eq!(dao.get_purpose(), "test"); + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "add new member".to_string(), + kind: ProposalKind::NewCouncil, + }); + assert_eq!(dao.get_num_proposals(), 1); + assert_eq!(dao.get_proposals(0, 1).len(), 1); + vote(&mut dao, id, vec![(0, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).vote_yes, 1); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Vote); + let account_0: AccountId = accounts(0); + let account_1: AccountId = accounts(1); + let account_2: AccountId = accounts(2); + assert_eq!( + dao.get_council(), + vec![account_0.clone(), account_1.clone()] + ); + vote(&mut dao, id, vec![(1, Vote::Yes)]); + assert_eq!(dao.get_council(), vec![account_0, account_1, account_2]); + + // Pay out money for proposal. 2 votes yes vs 1 vote no. + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "give me money".to_string(), + kind: ProposalKind::Payout { amount: 10.into() }, + }); + vote( + &mut dao, + id, + vec![(0, Vote::No), (1, Vote::Yes), (2, Vote::Yes)], + ); + assert_eq!(dao.get_proposal(id).vote_yes, 2); + assert_eq!(dao.get_proposal(id).vote_no, 1); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Success); + + // No vote for proposal. + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "give me more money".to_string(), + kind: ProposalKind::Payout { amount: 10.into() }, + }); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(3)) + .block_timestamp(1_001) + .build()); + dao.finalize(id); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Fail); + + // Change policy. + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "policy".to_string(), + kind: ProposalKind::ChangePolicy { + policy: vec![ + PolicyItem { + max_amount: 100.into(), + votes: NumOrRatio::Number(2), + }, + PolicyItem { + max_amount: 1_000_000.into(), + votes: NumOrRatio::Ratio(1, 1), + }, + ], + }, + }); + vote(&mut dao, id, vec![(0, Vote::Yes), (1, Vote::Yes)]); + + // Try new policy with small amount. + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "give me more money".to_string(), + kind: ProposalKind::Payout { amount: 10.into() }, + }); + assert_eq!(dao.get_proposal(id).vote_period_end, 1_000); + vote(&mut dao, id, vec![(0, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).vote_period_end, 1_000); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Vote); + vote(&mut dao, id, vec![(1, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Delay); + assert_eq!(dao.get_proposal(id).vote_period_end, 10); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(3)) + .block_timestamp(11) + .build()); + dao.finalize(id); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Success); + + // New policy for bigger amounts requires 100% votes. + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "give me more money".to_string(), + kind: ProposalKind::Payout { + amount: 10_000.into(), + }, + }); + vote(&mut dao, id, vec![(0, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Vote); + vote(&mut dao, id, vec![(1, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Vote); + vote(&mut dao, id, vec![(2, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Success); + } + + #[test] + fn test_expiration() { + let mut dao = SputnikDAO::new( + "test".to_string(), + vec![accounts(0), accounts(1), accounts(2)], + 10.into(), + 1_000.into(), + 10.into(), + ); + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(5), + description: "add new member".to_string(), + kind: ProposalKind::NewCouncil, + }); + let vote_period_end = dao.get_proposal(id).vote_period_end; + vote(&mut dao, id, vec![(0, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).vote_period_end, vote_period_end); + vote(&mut dao, id, vec![(1, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).vote_period_end, vote_period_end); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Success); + } + + #[test] + fn test_single_council() { + let mut dao = SputnikDAO::new( + "".to_string(), + vec![accounts(0)], + 10.into(), + 1_000.into(), + 10.into(), + ); + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(1), + description: "add new member".to_string(), + kind: ProposalKind::NewCouncil, + }); + vote(&mut dao, id, vec![(0, Vote::Yes)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Success); + let account_0: AccountId = accounts(0); + let account_1: AccountId = accounts(1); + assert_eq!(dao.get_council(), vec![account_0, account_1]); + } + + #[test] + #[should_panic] + fn test_double_vote() { + let mut dao = SputnikDAO::new( + "".to_string(), + vec![accounts(0), accounts(1)], + 10.into(), + 1000.into(), + 10.into(), + ); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "add new member".to_string(), + kind: ProposalKind::NewCouncil, + }); + assert_eq!(dao.get_proposals(0, 1).len(), 1); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(0)) + .build()); + dao.vote(id, Vote::Yes); + dao.vote(id, Vote::Yes); + } + + #[test] + fn test_two_council() { + let mut dao = SputnikDAO::new( + "".to_string(), + vec![accounts(0), accounts(1)], + 10.into(), + 1_000.into(), + 10.into(), + ); + + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(1), + description: "add new member".to_string(), + kind: ProposalKind::Payout { amount: 100.into() }, + }); + vote(&mut dao, id, vec![(0, Vote::Yes), (1, Vote::No)]); + assert_eq!(dao.get_proposal(id).status, ProposalStatus::Fail); + } + + #[test] + #[should_panic] + fn test_run_out_of_money() { + let mut dao = SputnikDAO::new( + "".to_string(), + vec![accounts(0)], + 10.into(), + 1000.into(), + 10.into(), + ); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + let id = dao.add_proposal(ProposalInput { + target: accounts(2), + description: "add new member".to_string(), + kind: ProposalKind::Payout { + amount: 1000.into(), + }, + }); + assert_eq!(dao.get_proposals(0, 1).len(), 1); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(0)) + .account_balance(10) + .build()); + dao.vote(id, Vote::Yes); + } + + #[test] + #[should_panic] + fn test_incorrect_policy() { + let mut dao = SputnikDAO::new( + "".to_string(), + vec![accounts(0), accounts(1)], + 10.into(), + 1000.into(), + 10.into(), + ); + testing_env!(VMContextBuilder::new() + .predecessor_account_id(accounts(2)) + .attached_deposit(10) + .build()); + dao.add_proposal(ProposalInput { + target: accounts(2), + description: "policy".to_string(), + kind: ProposalKind::ChangePolicy { + policy: vec![ + PolicyItem { + max_amount: 100.into(), + votes: NumOrRatio::Number(5), + }, + PolicyItem { + max_amount: 5.into(), + votes: NumOrRatio::Number(3), + }, + ], + }, + }); + } +}