From b196cec38899ee1d70c00c1447b629fcf13b6508 Mon Sep 17 00:00:00 2001 From: Keyne Date: Tue, 12 Dec 2023 11:49:51 +0000 Subject: [PATCH] Contract: Bridge fees collection and claiming implementation (#60) - Implemented fee collection and claiming for all tokens. How it works: Every time a user sends a token it will collect fees into the contract (part of the amount). The truncated amount counts towards the fee (so that users can choose what they want to receive on the other side). For claiming, any relayer can claim when ever he wants and the fees will be proportionally distributed to all of them. --- contract/Cargo.lock | 237 +++++--- contract/src/contract.rs | 153 ++++- contract/src/error.rs | 3 + contract/src/fees.rs | 105 ++++ contract/src/lib.rs | 1 + contract/src/msg.rs | 13 +- contract/src/state.rs | 9 +- contract/src/tests.rs | 900 ++++++++++++++++++++++++++--- integration-tests/xrpl/rpc_test.go | 16 +- relayer/coreum/contract.go | 4 + 10 files changed, 1233 insertions(+), 208 deletions(-) create mode 100644 contract/src/fees.rs diff --git a/contract/Cargo.lock b/contract/Cargo.lock index 3e0c418e..3f4acbd2 100644 --- a/contract/Cargo.lock +++ b/contract/Cargo.lock @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128a44527fc0d6abf05f9eda748b9027536e12dff93f5acc8449f51583309350" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" [[package]] name = "bs58" @@ -270,9 +270,9 @@ checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "coreum-test-tube" @@ -313,7 +313,7 @@ dependencies = [ "cosmwasm-std", "osmosis-std-derive", "prost 0.11.9", - "prost-types 0.12.2", + "prost-types 0.12.3", "schemars", "serde", "serde-cw-value", @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -568,14 +568,15 @@ dependencies = [ [[package]] name = "cw2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9431d14f64f49e41c6ef5561ed11a5391c417d0cb16455dea8cdcb9037a8d197" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus", "schemars", + "semver", "serde", "thiserror", ] @@ -592,9 +593,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", ] @@ -697,9 +698,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.7" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9775b22bc152ad86a0cf23f0f348b884b26add12bf741e7ffc4d4ab2ab4d205" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -722,19 +723,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" dependencies = [ "indenter", "once_cell", @@ -768,9 +769,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -896,9 +897,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" @@ -947,9 +948,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" @@ -1002,7 +1003,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1102,9 +1103,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1123,7 +1124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -1152,9 +1153,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1203,9 +1204,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "log" @@ -1242,13 +1243,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1302,9 +1303,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -1382,9 +1383,9 @@ checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1452,9 +1453,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -1471,12 +1472,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5a410fc7882af66deb8d01d01737353cf3ad6204c408177ba494291a626312" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive 0.12.2", + "prost-derive 0.12.3", ] [[package]] @@ -1494,9 +1495,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065717a5dfaca4a83d2fe57db3487b311365200000551d7a364e715dbf4346bc" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools 0.11.0", @@ -1516,11 +1517,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8339f32236f590281e2f6368276441394fcd1b2133b549cc895d0ae80f2f9a52" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost 0.12.2", + "prost 0.12.3", ] [[package]] @@ -1645,15 +1646,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.24" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1702,7 +1703,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1939,7 +1940,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1950,9 +1951,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -2216,7 +2217,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2291,9 +2292,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" @@ -2303,9 +2304,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -2330,9 +2331,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -2378,9 +2379,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2388,9 +2389,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -2403,9 +2404,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2413,9 +2414,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -2426,15 +2427,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -2508,7 +2509,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2517,13 +2527,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2532,42 +2557,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "zeroize" version = "1.7.0" diff --git a/contract/src/contract.rs b/contract/src/contract.rs index efe04542..e0b3ca2c 100644 --- a/contract/src/contract.rs +++ b/contract/src/contract.rs @@ -3,9 +3,10 @@ use std::collections::VecDeque; use crate::{ error::ContractError, evidence::{handle_evidence, hash_bytes, Evidence, OperationResult, TransactionResult}, + fees::{amount_after_fees, claim_fees_for_relayers, handle_fee_collection}, msg::{ - AvailableTicketsResponse, CoreumTokensResponse, ExecuteMsg, InstantiateMsg, - PendingOperationsResponse, QueryMsg, XRPLTokensResponse, + AvailableTicketsResponse, CoreumTokensResponse, ExecuteMsg, FeesCollectedResponse, + InstantiateMsg, PendingOperationsResponse, QueryMsg, XRPLTokensResponse, }, operation::{ check_operation_exists, create_pending_operation, @@ -16,10 +17,12 @@ use crate::{ signatures::add_signature, state::{ Config, ContractActions, CoreumToken, TokenState, XRPLToken, AVAILABLE_TICKETS, CONFIG, - COREUM_TOKENS, PENDING_OPERATIONS, PENDING_TICKET_UPDATE, USED_TICKETS_COUNTER, - XRPL_TOKENS, + COREUM_TOKENS, FEES_COLLECTED, PENDING_OPERATIONS, PENDING_TICKET_UPDATE, + USED_TICKETS_COUNTER, XRPL_TOKENS, + }, + tickets::{ + allocate_ticket, handle_ticket_allocation_confirmation, register_used_ticket, return_ticket, }, - tickets::{allocate_ticket, handle_ticket_allocation_confirmation, register_used_ticket, return_ticket}, }; use coreum_wasm_sdk::{ @@ -57,6 +60,8 @@ const XRP_ISSUER: &str = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; const XRP_DEFAULT_SENDING_PRECISION: i32 = 6; const XRP_DEFAULT_MAX_HOLDING_AMOUNT: u128 = 10u128.pow(16 - XRP_DEFAULT_SENDING_PRECISION as u32 + XRP_DECIMALS); +// TODO(keyleu): Update the value of the fee for XRP when we know it. +const XRP_DEFAULT_FEE: Uint128 = Uint128::zero(); pub const MAX_TICKETS: u32 = 250; @@ -95,6 +100,7 @@ pub fn instantiate( USED_TICKETS_COUNTER.save(deps.storage, &0)?; PENDING_TICKET_UPDATE.save(deps.storage, &false)?; AVAILABLE_TICKETS.save(deps.storage, &VecDeque::new())?; + FEES_COLLECTED.save(deps.storage, &vec![])?; let config = Config { relayers: msg.relayers, @@ -131,6 +137,7 @@ pub fn instantiate( max_holding_amount: Uint128::new(XRP_DEFAULT_MAX_HOLDING_AMOUNT), // The XRP token is enabled from the start because it doesn't need approval to be received on the XRPL side. state: TokenState::Enabled, + bridging_fee: XRP_DEFAULT_FEE, }; let key = build_xrpl_token_key(XRP_ISSUER.to_string(), XRP_CURRENCY.to_string()); @@ -160,6 +167,7 @@ pub fn execute( decimals, sending_precision, max_holding_amount, + bridging_fee, } => register_coreum_token( deps.into_empty(), env, @@ -168,12 +176,14 @@ pub fn execute( decimals, sending_precision, max_holding_amount, + bridging_fee, ), ExecuteMsg::RegisterXRPLToken { issuer, currency, sending_precision, max_holding_amount, + bridging_fee, } => register_xrpl_token( deps, env, @@ -182,6 +192,7 @@ pub fn execute( currency, sending_precision, max_holding_amount, + bridging_fee, ), ExecuteMsg::SaveEvidence { evidence } => { save_evidence(deps.into_empty(), info.sender, evidence) @@ -205,6 +216,7 @@ pub fn execute( ExecuteMsg::SendToXRPL { recipient } => { send_to_xrpl(deps.into_empty(), env, info, recipient) } + ExecuteMsg::ClaimFees {} => claim_fees(deps.into_empty(), info.sender), } } @@ -218,6 +230,7 @@ fn update_ownership( Ok(Response::new().add_attributes(ownership.into_attributes())) } +#[allow(clippy::too_many_arguments)] fn register_coreum_token( deps: DepsMut, env: Env, @@ -226,6 +239,7 @@ fn register_coreum_token( decimals: u32, sending_precision: i32, max_holding_amount: Uint128, + bridging_fee: Uint128, ) -> CoreumResult { assert_owner(deps.storage, &sender)?; @@ -265,6 +279,7 @@ fn register_coreum_token( max_holding_amount, // All registered Coreum originated tokens will start as enabled because they don't need a TrustSet operation to be bridged because issuer for such tokens is bridge address state: TokenState::Enabled, + bridging_fee, }; COREUM_TOKENS.save(deps.storage, denom.clone(), &token)?; @@ -275,6 +290,7 @@ fn register_coreum_token( .add_attribute("xrpl_currency_for_denom", xrpl_currency)) } +#[allow(clippy::too_many_arguments)] fn register_xrpl_token( deps: DepsMut, env: Env, @@ -283,6 +299,7 @@ fn register_xrpl_token( currency: String, sending_precision: i32, max_holding_amount: Uint128, + bridging_fee: Uint128, ) -> CoreumResult { assert_owner(deps.storage, &info.sender)?; @@ -340,6 +357,7 @@ fn register_xrpl_token( max_holding_amount, // Registered tokens will start in processing until TrustSet operation is accepted/rejected state: TokenState::Processing, + bridging_fee, }; XRPL_TOKENS.save(deps.storage, key, &token)?; @@ -406,12 +424,18 @@ fn save_evidence(deps: DepsMut, sender: Addr, evidence: Evidence) -> CoreumResul }; // Here we simply truncate because the Coreum tokens corresponding to XRPL originated tokens have the same decimals as their corresponding Coreum tokens - let amount_to_send = truncate_amount(token.sending_precision, decimals, amount)?; + let (amount_truncated, remainder) = + truncate_amount(token.sending_precision, decimals, amount)?; + + // We calculate the amount to send after applying the bridging fees for that token + let amount_to_send = + amount_after_fees(amount_truncated, token.bridging_fee, remainder)?; - if amount_to_send + if amount_truncated + .checked_add(remainder)? .checked_add( deps.querier - .query_supply(token.coreum_denom.clone())? + .query_supply(token.coreum_denom.to_owned())? .amount, )? .gt(&token.max_holding_amount) @@ -420,12 +444,25 @@ fn save_evidence(deps: DepsMut, sender: Addr, evidence: Evidence) -> CoreumResul } if threshold_reached { - let mint_msg = CosmosMsg::from(CoreumMsg::AssetFT(assetft::Msg::Mint { - coin: coin(amount_to_send.u128(), token.coreum_denom), - recipient: Some(recipient.to_string()), + let fee_collected = handle_fee_collection( + deps.storage, + token.bridging_fee, + token.coreum_denom.to_owned(), + remainder, + )?; + + let mint_msg_fees = CosmosMsg::from(CoreumMsg::AssetFT(assetft::Msg::Mint { + coin: coin(fee_collected.u128(), token.coreum_denom.to_owned()), + recipient: None, })); - response = response.add_message(mint_msg) + let mint_msg_for_recipient = + CosmosMsg::from(CoreumMsg::AssetFT(assetft::Msg::Mint { + coin: coin(amount_to_send.u128(), token.coreum_denom), + recipient: Some(recipient.to_string()), + })); + + response = response.add_messages([mint_msg_fees, mint_msg_for_recipient]) } } else { // We check that the token is registered and enabled @@ -447,15 +484,22 @@ fn save_evidence(deps: DepsMut, sender: Addr, evidence: Evidence) -> CoreumResul }; // We first convert the amount we receive with XRPL decimals to the corresponding decimals in Coreum and then we apply the truncation according to sending precision. - let amount_to_send = convert_and_truncate_amount( + let (amount_to_send, truncated_portion) = convert_and_truncate_amount( token.sending_precision, XRPL_TOKENS_DECIMALS, token.decimals, amount, + token.bridging_fee, )?; if threshold_reached { - // TODO(keyleu): for now we are SENDING back the entire amount but when fees are implemented this will not happen and part of the amount will be sent and funds will be collected + handle_fee_collection( + deps.storage, + token.bridging_fee, + token.denom.to_owned(), + truncated_portion, + )?; + let send_msg = BankMsg::Send { to_address: recipient.to_string(), amount: coins(amount_to_send.u128(), token.denom), @@ -546,7 +590,7 @@ fn save_evidence(deps: DepsMut, sender: Addr, evidence: Evidence) -> CoreumResul if transaction_result.eq(&TransactionResult::Invalid) && ticket_sequence.is_some() { // If an operation was invalid, the ticket was never consumed, so we must return it to the ticket array. - return_ticket(deps.storage, ticket_sequence.unwrap())?; + return_ticket(deps.storage, ticket_sequence.unwrap())?; } } @@ -717,7 +761,19 @@ fn send_to_xrpl( }; // We don't need any decimal conversion because the token is an XRPL originated token and they are issued with same decimals - amount_to_send = truncate_amount(xrpl_token.sending_precision, decimals, funds.amount)?; + let (amount_truncated, truncated_portion) = + truncate_amount(xrpl_token.sending_precision, decimals, funds.amount)?; + + // We calculate the amount to send after applying the bridging fees for that token + amount_to_send = + amount_after_fees(amount_truncated, xrpl_token.bridging_fee, truncated_portion)?; + + handle_fee_collection( + deps.storage, + xrpl_token.bridging_fee, + xrpl_token.coreum_denom, + truncated_portion, + )?; } None => { @@ -738,11 +794,20 @@ fn send_to_xrpl( // Since this is a Coreum originated token with different decimals, we are first going to truncate according to sending precision and then we will convert // to corresponding XRPL decimals. - amount_to_send = truncate_and_convert_amount( + let truncated_portion; + (amount_to_send, truncated_portion) = truncate_and_convert_amount( coreum_token.sending_precision, decimals, XRPL_TOKENS_DECIMALS, funds.amount, + coreum_token.bridging_fee, + )?; + + handle_fee_collection( + deps.storage, + coreum_token.bridging_fee, + coreum_token.denom.to_owned(), + truncated_portion, )?; // For Coreum originated tokens we need to check that we are not going over max bridge amount. @@ -779,6 +844,16 @@ fn send_to_xrpl( .add_attribute("coin", funds.to_string())) } +fn claim_fees(deps: DepsMut, sender: Addr) -> CoreumResult { + assert_relayer(deps.as_ref(), sender.clone())?; + + let response = claim_fees_for_relayers(deps.storage)?; + + Ok(response + .add_attribute("action", ContractActions::ClaimFees.as_str()) + .add_attribute("sender", sender)) +} + // ********** Queries ********** #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { @@ -793,6 +868,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Ownership {} => to_json_binary(&get_ownership(deps.storage)?), QueryMsg::PendingOperations {} => to_json_binary(&query_pending_operations(deps)?), QueryMsg::AvailableTickets {} => to_json_binary(&query_available_tickets(deps)?), + QueryMsg::FeesCollected {} => to_json_binary(&query_fees_collected(deps)?), } } @@ -855,6 +931,12 @@ fn query_available_tickets(deps: Deps) -> StdResult { }) } +fn query_fees_collected(deps: Deps) -> StdResult { + let fees_collected = FEES_COLLECTED.load(deps.storage)?; + + Ok(FeesCollectedResponse { fees_collected }) +} + // ********** Helpers ********** fn check_issue_fee(deps: &DepsMut, info: &MessageInfo) -> Result<(), ContractError> { @@ -912,7 +994,7 @@ fn truncate_amount( sending_precision: i32, decimals: u32, amount: Uint128, -) -> Result { +) -> Result<(Uint128, Uint128), ContractError> { // To get exactly by how much we need to divide the original amount // Example: if sending precision = -1. Exponent will be 15 - (-1) = 16 for XRPL tokens so we will divide the original amount by 1e16 // Example: if sending precision = 14. Exponent will be 15 - 14 = 1 for XRPL tokens so we will divide the original amount by 10 @@ -924,7 +1006,10 @@ fn truncate_amount( return Err(ContractError::AmountSentIsZeroAfterTruncation {}); } - Ok(amount_to_send.checked_mul(Uint128::new(10u128.pow(exponent.unsigned_abs())))?) + let truncated_amount = + amount_to_send.checked_mul(Uint128::new(10u128.pow(exponent.unsigned_abs())))?; + let remainder = amount.checked_sub(truncated_amount)?; + Ok((truncated_amount, remainder)) } // Function used to convert the amount received from XRPL with XRPL decimals to the Coreum amount with Coreum decimals @@ -946,28 +1031,40 @@ pub fn convert_amount_decimals( Ok(converted_amount) } -// Helper function to combine the conversion and truncation of amounts. +// Helper function to combine the conversion and truncation of amounts including substracting fees. fn convert_and_truncate_amount( sending_precision: i32, from_decimals: u32, to_decimals: u32, amount: Uint128, -) -> Result { + bridging_fee: Uint128, +) -> Result<(Uint128, Uint128), ContractError> { let converted_amount = convert_amount_decimals(from_decimals, to_decimals, amount)?; - let truncated_amount = truncate_amount(sending_precision, to_decimals, converted_amount)?; - Ok(truncated_amount) + // We save the remainder as well to deduct it from the bridging fees + let (truncated_amount, remainder) = + truncate_amount(sending_precision, to_decimals, converted_amount)?; + + let amount_after_fees = amount_after_fees(truncated_amount, bridging_fee, remainder)?; + + Ok((amount_after_fees, remainder)) } -// Helper function to combine the truncation and conversion of amounts +// Helper function to combine the truncation and conversion of amounts after substracting fees. fn truncate_and_convert_amount( sending_precision: i32, from_decimals: u32, to_decimals: u32, amount: Uint128, -) -> Result { - let truncated_amount = truncate_amount(sending_precision, from_decimals, amount)?; - let converted_amount = convert_amount_decimals(from_decimals, to_decimals, truncated_amount)?; - Ok(converted_amount) + bridging_fee: Uint128, +) -> Result<(Uint128, Uint128), ContractError> { + // We save the remainder as well to deduct it from the bridging fees + let (truncated_amount, remainder) = + truncate_amount(sending_precision, from_decimals, amount)?; + + let amount_after_fees = amount_after_fees(truncated_amount, bridging_fee, remainder)?; + + let converted_amount = convert_amount_decimals(from_decimals, to_decimals, amount_after_fees)?; + Ok((converted_amount, remainder)) } fn is_token_xrp(issuer: String, currency: String) -> bool { diff --git a/contract/src/error.rs b/contract/src/error.rs index 500a7499..e635ab4f 100644 --- a/contract/src/error.rs +++ b/contract/src/error.rs @@ -153,4 +153,7 @@ pub enum ContractError { #[error("InvalidOperationResult: OperationResult doesn't match a Pending Operation with the right Operation Type")] InvalidOperationResult {}, + + #[error("CannotCoverBridgingFees: The amount sent is not enough to cover the bridging fees")] + CannotCoverBridgingFees {}, } diff --git a/contract/src/fees.rs b/contract/src/fees.rs new file mode 100644 index 00000000..951224f1 --- /dev/null +++ b/contract/src/fees.rs @@ -0,0 +1,105 @@ +use coreum_wasm_sdk::core::CoreumMsg; +use cosmwasm_std::{coin, BankMsg, Coin, Response, Storage, Uint128}; + +use crate::{ + error::ContractError, + state::{CONFIG, FEES_COLLECTED}, +}; + +pub fn amount_after_fees( + amount: Uint128, + bridging_fee: Uint128, + truncated_portion: Uint128, +) -> Result { + let fee_to_collect = bridging_fee.saturating_sub(truncated_portion); + + let amount_after_fees = amount + .checked_sub(fee_to_collect) + .map_err(|_| ContractError::CannotCoverBridgingFees {})?; + + Ok(amount_after_fees) +} + +pub fn handle_fee_collection( + storage: &mut dyn Storage, + bridging_fee: Uint128, + token_denom: String, + truncated_portion: Uint128, +) -> Result { + // We substract the truncated portion from the bridging_fee. If truncated portion >= fee, + // then we already paid the fees and we collect the truncated portion instead of bridging fee (because it might be bigger than the bridging fee) + let fee_to_collect = bridging_fee.saturating_sub(truncated_portion); + let fee_collected = if fee_to_collect.is_zero() { + truncated_portion + } else { + bridging_fee + }; + + collect_fees(storage, coin(fee_collected.u128(), token_denom))?; + Ok(fee_collected) +} + +pub fn collect_fees(storage: &mut dyn Storage, fee: Coin) -> Result<(), ContractError> { + // We only collect fees if there is something to collect + // If for some reason there is a coin that we are not charging fees for, we don't collect it + if !fee.amount.is_zero() { + let mut fees_collected = FEES_COLLECTED.load(storage)?; + // If we already have the coin in the fee collected array, we update the amount, if not, we add it as a new element. + match fees_collected.iter_mut().find(|c| c.denom == fee.denom) { + Some(coin) => coin.amount += fee.amount, + None => fees_collected.push(fee), + } + FEES_COLLECTED.save(storage, &fees_collected)?; + } + + Ok(()) +} + +pub fn claim_fees_for_relayers( + storage: &mut dyn Storage, +) -> Result, ContractError> { + let mut fees_collected = FEES_COLLECTED.load(storage)?; + let relayers = CONFIG.load(storage)?.relayers; + let mut coins_for_each_relayer = vec![]; + + for fee in fees_collected.iter_mut() { + // For each token collected in fees, we will divide the amount by the number of relayers to know how much we need to send to each relayer + let amount_for_each_relayer = fee + .amount + .u128() + .checked_div(relayers.len() as u128) + .unwrap(); + + // If the amount is 0, we don't send it to the relayers + if amount_for_each_relayer != 0 { + coins_for_each_relayer.push(coin(amount_for_each_relayer, fee.denom.to_owned())); + } + + // We substract the amount we are sending to the relayers from the total amount collected + // We can't simply remove it from the array because there might be small amounts left due to truncation when dividing + fee.amount = fee + .amount + .checked_sub(Uint128::from( + amount_for_each_relayer + .checked_mul(relayers.len() as u128) + .unwrap(), + )) + .unwrap(); + } + + // We'll have 1 multi send message for each relayer + let mut send_messages = vec![]; + for relayer in relayers.iter() { + send_messages.push(BankMsg::Send { + to_address: relayer.coreum_address.to_string(), + amount: coins_for_each_relayer.clone(), + }); + } + + // Last thing we do is to clean the fees collected array removing the coins that have 0 amount + // We need to do this step to avoid the posibility of iterating over them next claim + fees_collected.retain(|c| !c.amount.is_zero()); + FEES_COLLECTED.save(storage, &fees_collected)?; + + Ok(Response::new().add_messages(send_messages)) +} diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 63e2d61e..518a7007 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -1,6 +1,7 @@ pub mod contract; pub mod error; pub mod evidence; +pub mod fees; pub mod msg; pub mod operation; pub mod relayer; diff --git a/contract/src/msg.rs b/contract/src/msg.rs index 07495b8c..2ddde585 100644 --- a/contract/src/msg.rs +++ b/contract/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, Coin, Uint128}; use cw_ownable::{cw_ownable_execute, cw_ownable_query}; #[allow(unused_imports)] @@ -29,6 +29,7 @@ pub enum ExecuteMsg { decimals: u32, sending_precision: i32, max_holding_amount: Uint128, + bridging_fee: Uint128, }, #[serde(rename = "register_xrpl_token")] RegisterXRPLToken { @@ -36,6 +37,7 @@ pub enum ExecuteMsg { currency: String, sending_precision: i32, max_holding_amount: Uint128, + bridging_fee: Uint128, }, RecoverTickets { account_sequence: u64, @@ -57,6 +59,8 @@ pub enum ExecuteMsg { SendToXRPL { recipient: String, }, + // Any relayer can claim fees at any point in time. They will be distributed proportionally among all of them. + ClaimFees {}, } #[cw_ownable_query] @@ -80,6 +84,8 @@ pub enum QueryMsg { PendingOperations {}, #[returns(AvailableTicketsResponse)] AvailableTickets {}, + #[returns(FeesCollectedResponse)] + FeesCollected {}, } #[cw_serde] @@ -101,3 +107,8 @@ pub struct PendingOperationsResponse { pub struct AvailableTicketsResponse { pub tickets: Vec, } + +#[cw_serde] +pub struct FeesCollectedResponse { + pub fees_collected: Vec, +} diff --git a/contract/src/state.rs b/contract/src/state.rs index 9ce16278..76e930e0 100644 --- a/contract/src/state.rs +++ b/contract/src/state.rs @@ -1,7 +1,7 @@ use std::collections::VecDeque; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Empty, Uint128}; +use cosmwasm_std::{Coin, Empty, Uint128}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, UniqueIndex}; use crate::{evidence::Evidences, operation::Operation, relayer::Relayer}; @@ -20,6 +20,7 @@ pub enum TopKey { UsedTickets = b'8', PendingOperations = b'9', PendingTicketUpdate = b'a', + FeesCollected = b'b', } impl TopKey { @@ -49,6 +50,7 @@ pub struct XRPLToken { pub sending_precision: i32, pub max_holding_amount: Uint128, pub state: TokenState, + pub bridging_fee: Uint128, } #[cw_serde] @@ -71,6 +73,7 @@ pub struct CoreumToken { pub sending_precision: i32, pub max_holding_amount: Uint128, pub state: TokenState, + pub bridging_fee: Uint128, } pub const CONFIG: Item = Item::new(TopKey::Config.as_str()); @@ -134,6 +137,8 @@ pub const USED_TICKETS_COUNTER: Item = Item::new(TopKey::UsedTickets.as_str pub const PENDING_OPERATIONS: Map = Map::new(TopKey::PendingOperations.as_str()); // Flag to know if we are currently waiting for new_tickets to be allocated pub const PENDING_TICKET_UPDATE: Item = Item::new(TopKey::PendingTicketUpdate.as_str()); +// Fees collected that will be distributed to all relayers when they are claimed +pub const FEES_COLLECTED: Item> = Item::new(TopKey::FeesCollected.as_str()); pub enum ContractActions { Instantiation, @@ -145,6 +150,7 @@ pub enum ContractActions { XRPLTransactionResult, SaveSignature, SendToXRPL, + ClaimFees, } impl ContractActions { @@ -159,6 +165,7 @@ impl ContractActions { ContractActions::XRPLTransactionResult => "submit_xrpl_transaction_result", ContractActions::SaveSignature => "save_signature", ContractActions::SendToXRPL => "send_to_xrpl", + ContractActions::ClaimFees => "claim_fees", } } } diff --git a/contract/src/tests.rs b/contract/src/tests.rs index 51277860..41c441bd 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -15,8 +15,8 @@ mod tests { error::ContractError, evidence::{Evidence, OperationResult, TransactionResult}, msg::{ - AvailableTicketsResponse, CoreumTokensResponse, ExecuteMsg, InstantiateMsg, - PendingOperationsResponse, QueryMsg, XRPLTokensResponse, + AvailableTicketsResponse, CoreumTokensResponse, ExecuteMsg, FeesCollectedResponse, + InstantiateMsg, PendingOperationsResponse, QueryMsg, XRPLTokensResponse, }, operation::{Operation, OperationType}, relayer::{validate_xrpl_address, Relayer}, @@ -41,7 +41,8 @@ mod tests { pub issuer: String, pub currency: String, pub sending_precision: i32, - pub max_holding_amount: u128, + pub max_holding_amount: Uint128, + pub bridging_fee: Uint128, } #[derive(Clone)] @@ -49,7 +50,8 @@ mod tests { pub denom: String, pub decimals: u32, pub sending_precision: i32, - pub max_holding_amount: u128, + pub max_holding_amount: Uint128, + pub bridging_fee: Uint128, } fn store_and_instantiate( @@ -568,6 +570,7 @@ mod tests { sending_precision: XRP_DEFAULT_SENDING_PRECISION, max_holding_amount: Uint128::new(XRP_DEFAULT_MAX_HOLDING_AMOUNT), state: TokenState::Enabled, + bridging_fee: Uint128::zero(), } ); } @@ -604,13 +607,15 @@ mod tests { denom: "denom1".to_string(), decimals: 6, sending_precision: 6, - max_holding_amount: 100000, + max_holding_amount: Uint128::new(100000), + bridging_fee: Uint128::zero(), }, CoreumToken { denom: "denom2".to_string(), decimals: 6, sending_precision: 6, - max_holding_amount: 100000, + max_holding_amount: Uint128::new(100000), + bridging_fee: Uint128::zero(), }, ]; @@ -622,7 +627,8 @@ mod tests { denom: token.denom, decimals: token.decimals, sending_precision: token.sending_precision, - max_holding_amount: Uint128::new(token.max_holding_amount), + max_holding_amount: token.max_holding_amount, + bridging_fee: token.bridging_fee, }, &vec![], &signer, @@ -639,6 +645,7 @@ mod tests { decimals: 6, sending_precision: 6, max_holding_amount: Uint128::new(1), + bridging_fee: test_tokens[0].bridging_fee, }, &vec![], &signer, @@ -662,6 +669,7 @@ mod tests { decimals: 6, sending_precision: -17, max_holding_amount: Uint128::new(1), + bridging_fee: test_tokens[0].bridging_fee, }, &vec![], &signer, @@ -747,13 +755,15 @@ mod tests { issuer: generate_xrpl_address(), // Valid issuer currency: "USD".to_string(), // Valid standard currency code sending_precision: -15, - max_holding_amount: 100, + max_holding_amount: Uint128::new(100), + bridging_fee: Uint128::zero(), }, XRPLToken { issuer: generate_xrpl_address(), // Valid issuer currency: "015841551A748AD2C1F76FF6ECB0CCCD00000000".to_string(), // Valid hexadecimal currency sending_precision: 15, - max_holding_amount: 50000, + max_holding_amount: Uint128::new(50000), + bridging_fee: Uint128::zero(), }, ]; @@ -765,7 +775,8 @@ mod tests { issuer: "not_valid_issuer".to_string(), currency: test_tokens[0].currency.clone(), sending_precision: test_tokens[0].sending_precision.clone(), - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount.clone()), + max_holding_amount: test_tokens[0].max_holding_amount.clone(), + bridging_fee: test_tokens[0].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -781,10 +792,11 @@ mod tests { .execute::( &contract_addr, &ExecuteMsg::RegisterXRPLToken { - issuer: test_tokens[1].issuer.clone(), + issuer: test_tokens[0].issuer.clone(), currency: test_tokens[0].currency.clone(), sending_precision: -16, - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount.clone()), + max_holding_amount: test_tokens[0].max_holding_amount.clone(), + bridging_fee: test_tokens[0].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -802,10 +814,11 @@ mod tests { .execute::( &contract_addr, &ExecuteMsg::RegisterXRPLToken { - issuer: test_tokens[1].issuer.clone(), + issuer: test_tokens[0].issuer.clone(), currency: test_tokens[0].currency.clone(), sending_precision: 16, - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount.clone()), + max_holding_amount: test_tokens[0].max_holding_amount.clone(), + bridging_fee: test_tokens[0].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -826,7 +839,8 @@ mod tests { issuer: test_tokens[1].issuer.clone(), currency: "invalid_currency".to_string(), sending_precision: test_tokens[1].sending_precision.clone(), - max_holding_amount: Uint128::new(test_tokens[1].max_holding_amount.clone()), + max_holding_amount: test_tokens[1].max_holding_amount.clone(), + bridging_fee: test_tokens[1].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -845,7 +859,8 @@ mod tests { issuer: test_tokens[0].issuer.clone(), currency: test_tokens[0].currency.clone(), sending_precision: test_tokens[0].sending_precision.clone(), - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount.clone()), + max_holding_amount: test_tokens[0].max_holding_amount.clone(), + bridging_fee: test_tokens[0].bridging_fee, }, &coins(20_000_000, FEE_DENOM), &signer, @@ -864,7 +879,8 @@ mod tests { issuer: test_tokens[0].issuer.clone(), currency: test_tokens[0].currency.clone(), sending_precision: test_tokens[0].sending_precision, - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount), + max_holding_amount: test_tokens[0].max_holding_amount, + bridging_fee: test_tokens[0].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -913,7 +929,8 @@ mod tests { issuer: token.issuer, currency: token.currency, sending_precision: token.sending_precision, - max_holding_amount: Uint128::new(token.max_holding_amount), + max_holding_amount: token.max_holding_amount, + bridging_fee: token.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -926,7 +943,8 @@ mod tests { issuer: generate_xrpl_address(), // Valid issuer currency: "USD".to_string(), // Valid standard currency code sending_precision: -15, - max_holding_amount: 100, + max_holding_amount: Uint128::new(100), + bridging_fee: Uint128::zero(), }; let last_ticket_error = wasm @@ -936,7 +954,8 @@ mod tests { issuer: extra_token.issuer, currency: extra_token.currency, sending_precision: extra_token.sending_precision, - max_holding_amount: Uint128::new(extra_token.max_holding_amount), + max_holding_amount: extra_token.max_holding_amount, + bridging_fee: extra_token.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -972,7 +991,8 @@ mod tests { issuer: test_tokens[0].issuer.clone(), currency: test_tokens[0].currency.clone(), sending_precision: test_tokens[0].sending_precision.clone(), - max_holding_amount: Uint128::new(test_tokens[0].max_holding_amount.clone()), + max_holding_amount: test_tokens[0].max_holding_amount.clone(), + bridging_fee: test_tokens[0].bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -1011,7 +1031,6 @@ mod tests { ) .unwrap(); assert_eq!(query_xrpl_tokens.tokens.len(), 1); - assert!(query_xrpl_tokens.tokens[0].coreum_denom.starts_with("xrpl")); // Query all tokens with pagination let query_xrpl_tokens = wasm @@ -1072,7 +1091,8 @@ mod tests { issuer: generate_xrpl_address(), currency: "USD".to_string(), sending_precision: 15, - max_holding_amount: 50000, + max_holding_amount: Uint128::new(50000), + bridging_fee: Uint128::zero(), }; // Set up enough tickets first to allow registering tokens. @@ -1111,7 +1131,8 @@ mod tests { issuer: test_token.issuer.clone(), currency: test_token.currency.clone(), sending_precision: test_token.sending_precision.clone(), - max_holding_amount: Uint128::new(test_token.max_holding_amount.clone()), + max_holding_amount: test_token.max_holding_amount.clone(), + bridging_fee: test_token.bridging_fee, }, &query_issue_fee(&asset_ft), signer, @@ -1285,7 +1306,8 @@ mod tests { issuer: test_token.issuer.clone(), currency: test_token.currency.clone(), sending_precision: test_token.sending_precision, - max_holding_amount: Uint128::new(test_token.max_holding_amount), + max_holding_amount: test_token.max_holding_amount, + bridging_fee: test_token.bridging_fee, }, &query_issue_fee(&asset_ft), signer, @@ -1651,6 +1673,7 @@ mod tests { decimals, sending_precision: 5, max_holding_amount: Uint128::new(10000000), + bridging_fee: Uint128::zero(), }, &vec![], &signer, @@ -1979,6 +2002,7 @@ mod tests { decimals, sending_precision: 10, max_holding_amount: Uint128::new(200000000000000000000), //2e20 + bridging_fee: Uint128::zero(), }, &vec![], &signer, @@ -2524,7 +2548,8 @@ mod tests { issuer: generate_xrpl_address(), currency: "TST".to_string(), sending_precision: 15, - max_holding_amount: 500000, + max_holding_amount: Uint128::new(500000), + bridging_fee: Uint128::zero(), }; // First we need to register and activate it @@ -2534,7 +2559,8 @@ mod tests { issuer: test_token.issuer.clone(), currency: test_token.currency.clone(), sending_precision: test_token.sending_precision, - max_holding_amount: Uint128::new(test_token.max_holding_amount), + max_holding_amount: test_token.max_holding_amount, + bridging_fee: test_token.bridging_fee, }, &query_issue_fee(&asset_ft), signer, @@ -2826,6 +2852,7 @@ mod tests { decimals, sending_precision: 5, max_holding_amount: Uint128::new(10000000), + bridging_fee: Uint128::zero(), }, &vec![], &signer, @@ -2974,20 +3001,23 @@ mod tests { issuer: generate_xrpl_address(), currency: "TT1".to_string(), sending_precision: -2, - max_holding_amount: 200000000000000000, + max_holding_amount: Uint128::new(200000000000000000), + bridging_fee: Uint128::zero(), }; let test_token2 = XRPLToken { issuer: generate_xrpl_address().to_string(), currency: "TT2".to_string(), sending_precision: 13, - max_holding_amount: 499, + max_holding_amount: Uint128::new(499), + bridging_fee: Uint128::zero(), }; let test_token3 = XRPLToken { issuer: generate_xrpl_address().to_string(), currency: "TT3".to_string(), sending_precision: 0, - max_holding_amount: 5000000000000000, + max_holding_amount: Uint128::new(5000000000000000), + bridging_fee: Uint128::zero(), }; // Set up enough tickets first to allow registering tokens. @@ -3029,7 +3059,8 @@ mod tests { issuer: test_token1.issuer.clone(), currency: test_token1.currency.clone(), sending_precision: test_token1.sending_precision.clone(), - max_holding_amount: Uint128::new(test_token1.max_holding_amount.clone()), + max_holding_amount: test_token1.max_holding_amount.clone(), + bridging_fee: test_token1.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -3112,7 +3143,7 @@ mod tests { tx_hash: generate_hash(), issuer: test_token1.issuer.clone(), currency: test_token1.currency.clone(), - // Sending more than 199999999999999999 will truncate to 100000000000000000 and send it to the user + // Sending more than 199999999999999999 will truncate to 100000000000000000 and send it to the user and keep the remainder in the contract as fees to collect. amount: Uint128::new(199999999999999999), recipient: Addr::unchecked(receiver.address()), }, @@ -3131,34 +3162,7 @@ mod tests { assert_eq!(request_balance.balance, "100000000000000000".to_string()); - // Sending it again should work too because we will not have passed maximum holding amount - wasm.execute::( - &contract_addr, - &ExecuteMsg::SaveEvidence { - evidence: Evidence::XRPLToCoreumTransfer { - tx_hash: generate_hash(), - issuer: test_token1.issuer.clone(), - currency: test_token1.currency.clone(), - // Let's try sending 199999999999999999 that will be truncated to 100000000000000000 and send it to the user - amount: Uint128::new(199999999999999999), - recipient: Addr::unchecked(receiver.address()), - }, - }, - &[], - &signer, - ) - .unwrap(); - - let request_balance = asset_ft - .query_balance(&QueryBalanceRequest { - account: receiver.address(), - denom: denom.clone(), - }) - .unwrap(); - - assert_eq!(request_balance.balance, "200000000000000000".to_string()); - - // Sending it a 3rd time will fail because will pass the maximum holding amount. + // Sending anything again should not work because we already sent the maximum amount possible including the fees in the contract. let maximum_amount_error = wasm .execute::( &contract_addr, @@ -3167,8 +3171,7 @@ mod tests { tx_hash: generate_hash(), issuer: test_token1.issuer.clone(), currency: test_token1.currency.clone(), - // Let's try sending 199999999999999999 that will be truncated to 100000000000000000 and send it to the user - amount: Uint128::new(199999999999999999), + amount: Uint128::new(100000000000000000), recipient: Addr::unchecked(receiver.address()), }, }, @@ -3185,12 +3188,13 @@ mod tests { let request_balance = asset_ft .query_balance(&QueryBalanceRequest { - account: receiver.address(), + account: contract_addr.to_owned(), denom: denom.clone(), }) .unwrap(); - assert_eq!(request_balance.balance, "200000000000000000".to_string()); + // Fees collected + assert_eq!(request_balance.balance, "99999999999999999".to_string()); // Test positive sending precisions @@ -3201,7 +3205,8 @@ mod tests { issuer: test_token2.issuer.clone(), currency: test_token2.currency.clone(), sending_precision: test_token2.sending_precision.clone(), - max_holding_amount: Uint128::new(test_token2.max_holding_amount.clone()), + max_holding_amount: test_token2.max_holding_amount.clone(), + bridging_fee: test_token2.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -3308,7 +3313,7 @@ mod tests { tx_hash: generate_hash(), issuer: test_token2.issuer.clone(), currency: test_token2.currency.clone(), - // Sending 299 should truncate the amount to 200 + // Sending 299 should truncate the amount to 200 and keep the 99 in the contract as fees to collect amount: Uint128::new(299), recipient: Addr::unchecked(receiver.address()), }, @@ -3327,7 +3332,7 @@ mod tests { assert_eq!(request_balance.balance, "200".to_string()); - // Sending it again should truncate the amount to 200 again and should pass + // Sending 200 should work because we will reach exactly the maximum bridged amount. wasm.execute::( &contract_addr, &ExecuteMsg::SaveEvidence { @@ -3335,8 +3340,7 @@ mod tests { tx_hash: generate_hash(), issuer: test_token2.issuer.clone(), currency: test_token2.currency.clone(), - // Sending 299 should truncate the amount to 200 - amount: Uint128::new(299), + amount: Uint128::new(200), recipient: Addr::unchecked(receiver.address()), }, }, @@ -3354,7 +3358,16 @@ mod tests { assert_eq!(request_balance.balance, "400".to_string()); - // Sending 199 should truncate to 100 and since maximum is 499, it should fail + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: contract_addr.to_owned(), + denom: denom.clone(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "99".to_string()); + + // Sending anything again should fail because we passed the maximum bridged amount let maximum_amount_error = wasm .execute::( &contract_addr, @@ -3363,7 +3376,6 @@ mod tests { tx_hash: generate_hash(), issuer: test_token2.issuer.clone(), currency: test_token2.currency.clone(), - // Sending 199 should truncate to 100 and since it's over the maximum it should fail amount: Uint128::new(199), recipient: Addr::unchecked(receiver.address()), }, @@ -3388,7 +3400,8 @@ mod tests { issuer: test_token3.issuer.clone(), currency: test_token3.currency.clone(), sending_precision: test_token3.sending_precision.clone(), - max_holding_amount: Uint128::new(test_token3.max_holding_amount.clone()), + max_holding_amount: test_token3.max_holding_amount.clone(), + bridging_fee: test_token3.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -3495,7 +3508,7 @@ mod tests { tx_hash: generate_hash(), issuer: test_token3.issuer.clone(), currency: test_token3.currency.clone(), - // Sending 1111111111111111 should truncate the amount to 1000000000000000 + // Sending 1111111111111111 should truncate the amount to 1000000000000000 and keep 111111111111111 as fees to collect amount: Uint128::new(1111111111111111), recipient: Addr::unchecked(receiver.address()), }, @@ -3521,8 +3534,8 @@ mod tests { tx_hash: generate_hash(), issuer: test_token3.issuer.clone(), currency: test_token3.currency.clone(), - // Sending 4111111111111111 should truncate the amount to 4000000000000000 and should pass because maximum is 5000000000000000 - amount: Uint128::new(4111111111111111), + // Sending 3111111111111111 should truncate the amount to 3000000000000000 and keep another 111111111111111 as fees to collect + amount: Uint128::new(3111111111111111), recipient: Addr::unchecked(receiver.address()), }, }, @@ -3538,7 +3551,16 @@ mod tests { }) .unwrap(); - assert_eq!(request_balance.balance, "5000000000000000".to_string()); + assert_eq!(request_balance.balance, "4000000000000000".to_string()); + + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: contract_addr.to_owned(), + denom: denom.clone(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "222222222222222".to_string()); let maximum_amount_error = wasm .execute::( @@ -3709,19 +3731,22 @@ mod tests { denom: denom1.to_owned(), decimals: 6, sending_precision: 6, - max_holding_amount: 3, + max_holding_amount: Uint128::new(3), + bridging_fee: Uint128::zero(), }, CoreumToken { denom: denom2.to_owned(), decimals: 6, sending_precision: 0, - max_holding_amount: 3990000, + max_holding_amount: Uint128::new(3990000), + bridging_fee: Uint128::zero(), }, CoreumToken { denom: denom3.to_owned(), decimals: 6, sending_precision: -6, - max_holding_amount: 2000000000000, + max_holding_amount: Uint128::new(2000000000000), + bridging_fee: Uint128::zero(), }, ]; @@ -3734,7 +3759,8 @@ mod tests { denom: token.denom, decimals: token.decimals, sending_precision: token.sending_precision, - max_holding_amount: Uint128::new(token.max_holding_amount), + max_holding_amount: token.max_holding_amount, + bridging_fee: token.bridging_fee, }, &vec![], &signer, @@ -3942,6 +3968,702 @@ mod tests { assert_eq!(request_balance.balance, "2000000000000".to_string()); } + #[test] + fn fee_collection_and_claiming() { + let app = CoreumTestApp::new(); + let accounts_number = 5; + let accounts = app + .init_accounts(&coins(100_000_000_000, FEE_DENOM), accounts_number) + .unwrap(); + + let signer = accounts.get((accounts_number - 1) as usize).unwrap(); + let receiver = accounts.get((accounts_number - 2) as usize).unwrap(); + let xrpl_addresses: Vec = (0..3).map(|_| generate_xrpl_address()).collect(); + let xrpl_pub_keys: Vec = (0..3).map(|_| generate_xrpl_pub_key()).collect(); + + let mut relayer_accounts = vec![]; + let mut relayers = vec![]; + + for i in 0..accounts_number - 2 { + relayer_accounts.push(accounts.get(i as usize).unwrap()); + relayers.push(Relayer { + coreum_address: Addr::unchecked(accounts.get(i as usize).unwrap().address()), + xrpl_address: xrpl_addresses[i as usize].to_string(), + xrpl_pub_key: xrpl_pub_keys[i as usize].to_string(), + }); + } + + let wasm = Wasm::new(&app); + let asset_ft = AssetFT::new(&app); + + let bridge_xrpl_address = generate_xrpl_address(); + let contract_addr = store_and_instantiate( + &wasm, + &signer, + Addr::unchecked(signer.address()), + vec![ + relayers[0].clone(), + relayers[1].clone(), + relayers[2].clone(), + ], + 3, + 14, + Uint128::new(TRUST_SET_LIMIT_AMOUNT), + query_issue_fee(&asset_ft), + bridge_xrpl_address.to_owned(), + ); + + // Recover enough tickets + wasm.execute::( + &contract_addr, + &ExecuteMsg::RecoverTickets { + account_sequence: 1, + number_of_tickets: Some(15), + }, + &vec![], + &signer, + ) + .unwrap(); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: Some(1), + ticket_sequence: None, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::TicketsAllocation { + tickets: Some((1..16).collect()), + }, + }, + }, + &vec![], + relayer, + ) + .unwrap(); + } + + // We are going to issue 2 tokens, one XRPL originated and one Coreum originated, with different fees. + let test_token_xrpl = XRPLToken { + issuer: generate_xrpl_address(), // Valid issuer + currency: "USD".to_string(), // Valid standard currency code + sending_precision: 10, + max_holding_amount: Uint128::new(5000000000000000), // 5e15 + bridging_fee: Uint128::new(50000), // 5e4 + }; + + let symbol = "TEST".to_string(); + let subunit = "utest".to_string(); + let decimals = 6; + let initial_amount = Uint128::new(100000000); + asset_ft + .issue( + MsgIssue { + issuer: receiver.address(), + symbol, + subunit: subunit.to_owned(), + precision: decimals, + initial_amount: initial_amount.to_string(), + description: "description".to_string(), + features: vec![MINTING as i32], + burn_rate: "0".to_string(), + send_commission_rate: "0".to_string(), + uri: "uri".to_string(), + uri_hash: "uri_hash".to_string(), + }, + &receiver, + ) + .unwrap(); + + let coreum_token_denom = format!("{}-{}", subunit, receiver.address()).to_lowercase(); + + let test_token_coreum = CoreumToken { + denom: coreum_token_denom.to_owned(), + decimals, + sending_precision: 4, + max_holding_amount: Uint128::new(10000000000), // 1e10 + bridging_fee: Uint128::new(300000), // 3e5 + }; + + // Register XRPL originated token and confirm trust set + wasm.execute::( + &contract_addr, + &ExecuteMsg::RegisterXRPLToken { + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + sending_precision: test_token_xrpl.sending_precision, + max_holding_amount: test_token_xrpl.max_holding_amount, + bridging_fee: test_token_xrpl.bridging_fee, + }, + &query_issue_fee(&asset_ft), + &signer, + ) + .unwrap(); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: None, + ticket_sequence: Some(1), + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::TrustSet { + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + }, + }, + }, + &vec![], + relayer, + ) + .unwrap(); + } + + let query_xrpl_tokens = wasm + .query::( + &contract_addr, + &QueryMsg::XRPLTokens { + offset: None, + limit: None, + }, + ) + .unwrap(); + + let xrpl_token = query_xrpl_tokens + .tokens + .iter() + .find(|t| t.issuer == test_token_xrpl.issuer && t.currency == test_token_xrpl.currency) + .unwrap(); + + // Register Coreum originated token + wasm.execute::( + &contract_addr, + &ExecuteMsg::RegisterCoreumToken { + denom: test_token_coreum.denom, + decimals: test_token_coreum.decimals, + sending_precision: test_token_coreum.sending_precision, + max_holding_amount: test_token_coreum.max_holding_amount, + bridging_fee: test_token_coreum.bridging_fee, + }, + &vec![], + &signer, + ) + .unwrap(); + + let query_coreum_tokens = wasm + .query::( + &contract_addr, + &QueryMsg::CoreumTokens { + offset: None, + limit: None, + }, + ) + .unwrap(); + + let coreum_token = query_coreum_tokens + .tokens + .iter() + .find(|t| t.denom == coreum_token_denom) + .unwrap(); + + // Let's bridge some tokens from XRPL to Coreum multiple times and verify that the fees are collected correctly in each step + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + amount: Uint128::new(1000000000050000), // 1e15 + 5e4 --> This should truncate the entire bridging fee and not take anything else + recipient: Addr::unchecked(receiver.address()), + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: receiver.address(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "1000000000000000".to_string()); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(50000, xrpl_token.coreum_denom.to_owned())] + ); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + amount: Uint128::new(1000000000040000), // 1e15 + 5e4 --> This should truncate and charge extra 10000 to cover bridging fee + recipient: Addr::unchecked(receiver.address()), + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: receiver.address(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "1999999999990000".to_string()); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(100000, xrpl_token.coreum_denom.to_owned())] + ); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + amount: Uint128::new(1000000000000000), // 1e15 + 5e4 --> This should truncate nothing and charge the entire bridging fee (50000) + recipient: Addr::unchecked(receiver.address()), + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + let request_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: receiver.address(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + + assert_eq!(request_balance.balance, "2999999999940000".to_string()); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(150000, xrpl_token.coreum_denom.to_owned())] + ); + + // Check that contract holds those tokens. + let query_contract_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: contract_addr.to_owned(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + assert_eq!(query_contract_balance.balance, "150000".to_string()); + + // Let's try to bridge some tokens back from Coreum to XRPL and verify that the fees are also collected correctly + let xrpl_receiver_address = generate_xrpl_address(); + wasm.execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: xrpl_receiver_address.to_owned(), + }, + &coins(1000000000020000, xrpl_token.coreum_denom.to_owned()), // This should truncate 20000 and charge 30000 extra to cover bridging fee + &receiver, + ) + .unwrap(); + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert_eq!(query_pending_operations.operations.len(), 1); + assert_eq!( + query_pending_operations.operations[0], + Operation { + ticket_sequence: Some(2), + account_sequence: None, + signatures: vec![], + operation_type: OperationType::CoreumToXRPLTransfer { + issuer: test_token_xrpl.issuer.to_owned(), + currency: test_token_xrpl.currency.to_owned(), + amount: Uint128::new(999999999970000), + sender: Addr::unchecked(receiver.address()), + recipient: xrpl_receiver_address.to_owned(), + }, + } + ); + + // Confirm operation to clear tokens from contract + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: query_pending_operations.operations[0].account_sequence, + ticket_sequence: query_pending_operations.operations[0].ticket_sequence, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::CoreumToXRPLTransfer {}, + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(200000, xrpl_token.coreum_denom.to_owned())] + ); + + // Now let's bridge tokens from Coreum to XRPL and verify that the fees are collected correctly in each step and accumulated with the previous ones + + // Trying to send less than the bridging fees should fail + let bridging_error = wasm + .execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: xrpl_receiver_address.to_owned(), + }, + &coins(100, coreum_token_denom.to_owned()), + &receiver, + ) + .unwrap_err(); + + assert!(bridging_error.to_string().contains( + ContractError::CannotCoverBridgingFees {} + .to_string() + .as_str() + )); + + wasm.execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: xrpl_receiver_address.to_owned(), + }, + &coins(600010, coreum_token_denom.to_owned()), // This should truncate 10 and charge extra 299990 to cover bridging fee + &receiver, + ) + .unwrap(); + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert_eq!(query_pending_operations.operations.len(), 1); + assert_eq!( + query_pending_operations.operations[0], + Operation { + ticket_sequence: Some(3), + account_sequence: None, + signatures: vec![], + operation_type: OperationType::CoreumToXRPLTransfer { + issuer: bridge_xrpl_address.to_owned(), + currency: coreum_token.xrpl_currency.to_owned(), + amount: Uint128::new(300010000000000), + sender: Addr::unchecked(receiver.address()), + recipient: xrpl_receiver_address.to_owned(), + }, + } + ); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![ + coin(200000, xrpl_token.coreum_denom.to_owned()), + coin(300000, coreum_token_denom.to_owned()) + ] + ); + + // Confirm operation + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: query_pending_operations.operations[0].account_sequence, + ticket_sequence: query_pending_operations.operations[0].ticket_sequence, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::CoreumToXRPLTransfer {}, + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + wasm.execute::( + &contract_addr, + &ExecuteMsg::SendToXRPL { + recipient: xrpl_receiver_address.to_owned(), + }, + &coins(900000, coreum_token_denom.to_owned()), // This should truncate nothing and charge the entire bridging fee (300000) + &receiver, + ) + .unwrap(); + + let query_pending_operations = wasm + .query::( + &contract_addr, + &QueryMsg::PendingOperations {}, + ) + .unwrap(); + + assert_eq!(query_pending_operations.operations.len(), 1); + assert_eq!( + query_pending_operations.operations[0], + Operation { + ticket_sequence: Some(4), + account_sequence: None, + signatures: vec![], + operation_type: OperationType::CoreumToXRPLTransfer { + issuer: bridge_xrpl_address.to_owned(), + currency: coreum_token.xrpl_currency.to_owned(), + amount: Uint128::new(600000000000000), + sender: Addr::unchecked(receiver.address()), + recipient: xrpl_receiver_address.to_owned(), + }, + } + ); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![ + coin(200000, xrpl_token.coreum_denom.to_owned()), + coin(600000, coreum_token_denom.to_owned()) + ] + ); + + // Confirm operation + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLTransactionResult { + tx_hash: Some(tx_hash.to_owned()), + account_sequence: query_pending_operations.operations[0].account_sequence, + ticket_sequence: query_pending_operations.operations[0].ticket_sequence, + transaction_result: TransactionResult::Accepted, + operation_result: OperationResult::CoreumToXRPLTransfer {}, + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + // Let's try to send the Coreum originated token in the opposite direction (from XRPL to Coreum) and see that fees are also accumulated correctly. + let previous_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: receiver.address(), + denom: coreum_token_denom.clone(), + }) + .unwrap(); + + let tx_hash = generate_hash(); + for relayer in relayer_accounts.iter() { + wasm.execute::( + &contract_addr, + &ExecuteMsg::SaveEvidence { + evidence: Evidence::XRPLToCoreumTransfer { + tx_hash: tx_hash.to_owned(), + issuer: bridge_xrpl_address.to_owned(), + currency: coreum_token.xrpl_currency.to_owned(), + amount: Uint128::new(650010000000000), // 650010000000000 will convert to 650010, which after truncating and charging fees will send 350010 to the receiver + recipient: Addr::unchecked(receiver.address()), + }, + }, + &[], + relayer, + ) + .unwrap(); + } + + let new_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: receiver.address(), + denom: coreum_token_denom.clone(), + }) + .unwrap(); + + assert_eq!( + new_balance.balance.parse::().unwrap(), + previous_balance + .balance + .parse::() + .unwrap() + .checked_add(350010) + .unwrap() + ); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![ + coin(200000, xrpl_token.coreum_denom.to_owned()), + coin(900000, coreum_token_denom.to_owned()) + ] + ); + + // Let's test the claiming + wasm.execute::( + &contract_addr, + &ExecuteMsg::ClaimFees {}, + &[], + relayer_accounts[0], + ) + .unwrap(); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + // Since we have 3 relayers, it should have claimed 1/3 of the fees for each one of them + // For the first coin 20000%3 = 2, so it should have left 2 tokens in the fees array + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(2, xrpl_token.coreum_denom.to_owned()),] + ); + + // Let's check the balances of the relayers + for relayer in relayer_accounts.iter() { + let request_balance_token1 = asset_ft + .query_balance(&QueryBalanceRequest { + account: relayer.address(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + let request_balance_token2 = asset_ft + .query_balance(&QueryBalanceRequest { + account: relayer.address(), + denom: coreum_token_denom.to_owned(), + }) + .unwrap(); + + assert_eq!(request_balance_token1.balance, "66666".to_string()); // 200000 / 3 = 66666 + assert_eq!(request_balance_token2.balance, "300000".to_string()); // 900000 / 3 = 300000 + } + + // If we try to claim again, nothing will change + wasm.execute::( + &contract_addr, + &ExecuteMsg::ClaimFees {}, + &[], + relayer_accounts[0], + ) + .unwrap(); + + let query_fees_collected = wasm + .query::(&contract_addr, &QueryMsg::FeesCollected {}) + .unwrap(); + + assert_eq!( + query_fees_collected.fees_collected, + vec![coin(2, xrpl_token.coreum_denom.to_owned()),] + ); + // Check that relayers balance hasn't changed. + for relayer in relayer_accounts.iter() { + let request_balance_token1 = asset_ft + .query_balance(&QueryBalanceRequest { + account: relayer.address(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + let request_balance_token2 = asset_ft + .query_balance(&QueryBalanceRequest { + account: relayer.address(), + denom: coreum_token_denom.to_owned(), + }) + .unwrap(); + + assert_eq!(request_balance_token1.balance, "66666".to_string()); // 200000 / 3 = 66666 + assert_eq!(request_balance_token2.balance, "300000".to_string()); // 900000 / 3 = 300000 + } + + // Check that final balance in the contract matches with those fees + let query_contract_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: contract_addr.to_owned(), + denom: xrpl_token.coreum_denom.to_owned(), + }) + .unwrap(); + assert_eq!(query_contract_balance.balance, "2".to_string()); + + let query_contract_balance = asset_ft + .query_balance(&QueryBalanceRequest { + account: contract_addr.to_owned(), + denom: coreum_token_denom.to_owned(), + }) + .unwrap(); + + // Amount that the user can still bridge back (he has on XRPL) from the token he has sent + // Sent: 300010 + 600000 (after applying fees) + // Sent back: 650010 + // Result: 300010 + 600000 - 650010 = 250000 + assert_eq!(query_contract_balance.balance, "250000".to_string()); + } + #[test] fn ticket_recovery() { let app = CoreumTestApp::new(); @@ -4472,7 +5194,8 @@ mod tests { issuer: token_issuer.to_owned(), currency: token_currency.to_owned(), sending_precision: -15, - max_holding_amount: 100, + max_holding_amount: Uint128::new(100), + bridging_fee: Uint128::zero(), }; let contract_addr = store_and_instantiate( @@ -4524,7 +5247,8 @@ mod tests { issuer: token.issuer.clone(), currency: token.currency.clone(), sending_precision: token.sending_precision, - max_holding_amount: Uint128::new(token.max_holding_amount), + max_holding_amount: token.max_holding_amount, + bridging_fee: token.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -4667,13 +5391,15 @@ mod tests { issuer: generate_xrpl_address(), // Valid issuer currency: "USD".to_string(), // Valid standard currency code sending_precision: -15, - max_holding_amount: 100, + max_holding_amount: Uint128::new(100), + bridging_fee: Uint128::zero(), }, XRPLToken { issuer: generate_xrpl_address(), // Valid issuer currency: "015841551A748AD2C1F76FF6ECB0CCCD00000000".to_string(), // Valid hexadecimal currency sending_precision: 15, - max_holding_amount: 50000, + max_holding_amount: Uint128::new(50000), + bridging_fee: Uint128::zero(), }, ]; @@ -4727,7 +5453,8 @@ mod tests { issuer: token.issuer.clone(), currency: token.currency.clone(), sending_precision: token.sending_precision, - max_holding_amount: Uint128::new(token.max_holding_amount), + max_holding_amount: token.max_holding_amount, + bridging_fee: token.bridging_fee, }, &query_issue_fee(&asset_ft), &signer, @@ -4922,6 +5649,7 @@ mod tests { decimals, sending_precision: 6, max_holding_amount: Uint128::new(10000000), + bridging_fee: Uint128::zero(), }, &vec![], &signer, @@ -5189,6 +5917,7 @@ mod tests { decimals: 6, sending_precision: 1, max_holding_amount: Uint128::one(), + bridging_fee: Uint128::zero(), }, &vec![], ¬_owner, @@ -5210,6 +5939,7 @@ mod tests { currency: "USD".to_string(), sending_precision: 4, max_holding_amount: Uint128::new(50000), + bridging_fee: Uint128::zero(), }, &query_issue_fee(&asset_ft), ¬_owner, diff --git a/integration-tests/xrpl/rpc_test.go b/integration-tests/xrpl/rpc_test.go index 686a61d0..138c2cf5 100644 --- a/integration-tests/xrpl/rpc_test.go +++ b/integration-tests/xrpl/rpc_test.go @@ -70,9 +70,9 @@ func TestXRPAndIssuedTokensPayment(t *testing.T) { TransactionType: rippledata.PAYMENT, }, } - t.Logf("Recipinet account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) + t.Logf("Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) require.NoError(t, chains.XRPL.AutoFillSignAndSubmitTx(ctx, t, &fooPaymentTx, issuerAcc)) - t.Logf("Recipinet account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) + t.Logf("Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) } func TestMultisigPayment(t *testing.T) { @@ -149,12 +149,12 @@ func TestMultisigPayment(t *testing.T) { require.NotEqual(t, xrpPaymentTxTwoSigners.Hash.String(), xrpPaymentTxThreeSigners.Hash.String()) t.Logf( - "Recipinet account balance before: %s", + "Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTxTwoSigners.Destination), ) require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &xrpPaymentTxTwoSigners)) t.Logf( - "Recipinet account balance after: %s", + "Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTxTwoSigners.Destination), ) @@ -226,9 +226,9 @@ func TestCreateAndUseTicketForPaymentAndTicketsCreation(t *testing.T) { xrpPaymentTx.TxBase.Sequence = 0 xrpPaymentTx.TicketSequence = createdTickets[0].TicketSequence - t.Logf("Recipinet account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) + t.Logf("Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) require.NoError(t, chains.XRPL.SignAndSubmitTx(ctx, t, &xrpPaymentTx, senderAcc)) - t.Logf("Recipinet account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) + t.Logf("Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, recipientAcc)) // try to use tickets for the transactions without the trust-line const newFooCurrencyCode = "NFO" @@ -488,9 +488,9 @@ func TestMultisigWithMasterKeyRemoval(t *testing.T) { xrpPaymentTx = buildXrpPaymentTxForMultiSigning(ctx, t, chains.XRPL, multisigAccToDisable, signer1Acc) require.NoError(t, rippledata.SetSigners(&xrpPaymentTx, signer1, signer2)) - t.Logf("Recipinet account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) + t.Logf("Recipient account balance before: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) require.NoError(t, chains.XRPL.SubmitTx(ctx, t, &xrpPaymentTx)) - t.Logf("Recipinet account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) + t.Logf("Recipient account balance after: %s", chains.XRPL.GetAccountBalances(ctx, t, xrpPaymentTx.Destination)) } func TestCreateAndUseUsedTicketAndSequencesWithMultisigning(t *testing.T) { diff --git a/relayer/coreum/contract.go b/relayer/coreum/contract.go index e4b04c6c..141dedaf 100644 --- a/relayer/coreum/contract.go +++ b/relayer/coreum/contract.go @@ -236,6 +236,7 @@ type registerCoreumTokenRequest struct { Decimals uint32 `json:"decimals"` SendingPrecision int32 `json:"sending_precision"` MaxHoldingAmount sdkmath.Int `json:"max_holding_amount"` + BridgingFee sdkmath.Int `json:"bridging_fee"` } type registerXRPLTokenRequest struct { @@ -243,6 +244,7 @@ type registerXRPLTokenRequest struct { Currency string `json:"currency"` SendingPrecision int32 `json:"sending_precision"` MaxHoldingAmount sdkmath.Int `json:"max_holding_amount"` + BridgingFee sdkmath.Int `json:"bridging_fee"` } type saveEvidenceRequest struct { @@ -514,6 +516,7 @@ func (c *ContractClient) RegisterCoreumToken( Decimals: decimals, SendingPrecision: sendingPrecision, MaxHoldingAmount: maxHoldingAmount, + BridgingFee: sdkmath.NewInt(0), }, }, }) @@ -544,6 +547,7 @@ func (c *ContractClient) RegisterXRPLToken( Currency: currency, SendingPrecision: sendingPrecision, MaxHoldingAmount: maxHoldingAmount, + BridgingFee: sdkmath.NewInt(0), }, }, Funds: sdk.NewCoins(fee),