├── .github └── workflows │ ├── go.yaml │ ├── rust.yaml │ └── wasm.yaml ├── .gitignore ├── .golangci.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── contracts ├── account │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── core │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── controller │ │ └── mod.rs │ │ ├── error.rs │ │ ├── handshake.rs │ │ ├── host │ │ ├── handler.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ ├── query.rs │ │ ├── state.rs │ │ ├── transfer │ │ ├── helpers.rs │ │ ├── mod.rs │ │ └── trace.rs │ │ └── utils │ │ ├── coins.rs │ │ └── mod.rs └── mocks │ ├── account-factory │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs │ ├── counter │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs │ ├── dex │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs │ └── sender │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── docs ├── 1-overview.md ├── ICS-999_awesomwasm.pdf └── README.md ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── justfile ├── packages └── ics999 │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs ├── rustfmt.toml ├── tests ├── factory_test.go ├── ica_test.go ├── suite_test.go ├── transfer_test.go └── types │ ├── core.go │ ├── mocks.go │ ├── ownable.go │ └── packet.go └── tools └── tools.go /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Just 14 | uses: extractions/setup-just@v1 15 | 16 | - name: Install Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '1.20' 20 | 21 | - name: Optimize contracts 22 | run: just optimize 23 | 24 | - name: Run tests 25 | run: just go-test 26 | 27 | lint: 28 | name: Lint 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout sources 32 | uses: actions/checkout@v3 33 | 34 | - name: Install Just 35 | uses: extractions/setup-just@v1 36 | 37 | - name: Install Go 38 | uses: actions/setup-go@v4 39 | with: 40 | go-version: '1.20' 41 | 42 | - name: Run linter 43 | run: just go-lint 44 | -------------------------------------------------------------------------------- /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Just 14 | uses: extractions/setup-just@v1 15 | 16 | - name: Install toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | toolchain: stable 20 | 21 | - name: Run tests 22 | run: just rust-test 23 | env: 24 | RUST_BACKTRACE: 1 25 | 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout sources 31 | uses: actions/checkout@v3 32 | 33 | - name: Install Just 34 | uses: extractions/setup-just@v1 35 | 36 | - name: Install toolchain 37 | uses: dtolnay/rust-toolchain@stable 38 | with: 39 | toolchain: nightly 40 | components: clippy 41 | 42 | - name: Run linter 43 | run: just rust-lint 44 | env: 45 | RUST_BACKTRACE: 1 46 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yaml: -------------------------------------------------------------------------------- 1 | name: Wasm 2 | 3 | on: push 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Just 14 | uses: extractions/setup-just@v1 15 | 16 | - name: Install toolchain 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | toolchain: stable 20 | targets: wasm32-unknown-unknown 21 | 22 | - name: Check for errors 23 | run: just rust-check 24 | env: 25 | RUST_BACKTRACE: 1 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # cosmwasm build artifacts 2 | artifacts/ 3 | 4 | # rust 5 | target/ 6 | 7 | # nodejs 8 | scripts/node_modules/ 9 | 10 | # schemas don't need to ve committed 11 | # whoever needs them can generate them on their own 12 | schemas/ 13 | 14 | # some data and notes while doing tests 15 | testdata/ 16 | 17 | # editors 18 | .idea/ 19 | .vscode/ 20 | 21 | # macos 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | timeout: 5m # timeout for analysis, e.g. 30s, 5m, default is 1m 4 | 5 | linters: 6 | disable-all: true 7 | enable: 8 | - dogsled 9 | - errcheck 10 | - exportloopref 11 | - goconst 12 | - gocritic 13 | - gofumpt 14 | - gosec 15 | - gosimple 16 | - govet 17 | - ineffassign 18 | - misspell 19 | - nakedret 20 | - nolintlint 21 | - prealloc 22 | - staticcheck 23 | - stylecheck 24 | - revive 25 | - typecheck 26 | - unconvert 27 | - unused 28 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" 10 | dependencies = [ 11 | "getrandom", 12 | "once_cell", 13 | "version_check", 14 | ] 15 | 16 | [[package]] 17 | name = "android-tzdata" 18 | version = "0.1.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 21 | 22 | [[package]] 23 | name = "anyhow" 24 | version = "1.0.71" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 27 | 28 | [[package]] 29 | name = "autocfg" 30 | version = "1.1.0" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 33 | 34 | [[package]] 35 | name = "base16ct" 36 | version = "0.1.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" 39 | 40 | [[package]] 41 | name = "base64" 42 | version = "0.13.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 45 | 46 | [[package]] 47 | name = "base64ct" 48 | version = "1.6.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 51 | 52 | [[package]] 53 | name = "block-buffer" 54 | version = "0.9.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" 57 | dependencies = [ 58 | "generic-array", 59 | ] 60 | 61 | [[package]] 62 | name = "block-buffer" 63 | version = "0.10.4" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 66 | dependencies = [ 67 | "generic-array", 68 | ] 69 | 70 | [[package]] 71 | name = "byteorder" 72 | version = "1.4.3" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 75 | 76 | [[package]] 77 | name = "bytes" 78 | version = "1.4.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 81 | 82 | [[package]] 83 | name = "cfg-if" 84 | version = "1.0.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 87 | 88 | [[package]] 89 | name = "chrono" 90 | version = "0.4.26" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" 93 | dependencies = [ 94 | "android-tzdata", 95 | "num-traits", 96 | ] 97 | 98 | [[package]] 99 | name = "const-oid" 100 | version = "0.9.3" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" 103 | 104 | [[package]] 105 | name = "cosmwasm-crypto" 106 | version = "1.2.7" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" 109 | dependencies = [ 110 | "digest 0.10.7", 111 | "ed25519-zebra", 112 | "k256", 113 | "rand_core 0.6.4", 114 | "thiserror", 115 | ] 116 | 117 | [[package]] 118 | name = "cosmwasm-derive" 119 | version = "1.2.7" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" 122 | dependencies = [ 123 | "syn 1.0.109", 124 | ] 125 | 126 | [[package]] 127 | name = "cosmwasm-schema" 128 | version = "1.2.7" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" 131 | dependencies = [ 132 | "cosmwasm-schema-derive", 133 | "schemars", 134 | "serde", 135 | "serde_json", 136 | "thiserror", 137 | ] 138 | 139 | [[package]] 140 | name = "cosmwasm-schema-derive" 141 | version = "1.2.7" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" 144 | dependencies = [ 145 | "proc-macro2", 146 | "quote", 147 | "syn 1.0.109", 148 | ] 149 | 150 | [[package]] 151 | name = "cosmwasm-std" 152 | version = "1.2.7" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" 155 | dependencies = [ 156 | "base64", 157 | "cosmwasm-crypto", 158 | "cosmwasm-derive", 159 | "derivative", 160 | "forward_ref", 161 | "hex", 162 | "schemars", 163 | "serde", 164 | "serde-json-wasm", 165 | "sha2 0.10.7", 166 | "thiserror", 167 | "uint", 168 | ] 169 | 170 | [[package]] 171 | name = "cpufeatures" 172 | version = "0.2.9" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" 175 | dependencies = [ 176 | "libc", 177 | ] 178 | 179 | [[package]] 180 | name = "crunchy" 181 | version = "0.2.2" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 184 | 185 | [[package]] 186 | name = "crypto-bigint" 187 | version = "0.4.9" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" 190 | dependencies = [ 191 | "generic-array", 192 | "rand_core 0.6.4", 193 | "subtle", 194 | "zeroize", 195 | ] 196 | 197 | [[package]] 198 | name = "crypto-common" 199 | version = "0.1.6" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 202 | dependencies = [ 203 | "generic-array", 204 | "typenum", 205 | ] 206 | 207 | [[package]] 208 | name = "curve25519-dalek" 209 | version = "3.2.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" 212 | dependencies = [ 213 | "byteorder", 214 | "digest 0.9.0", 215 | "rand_core 0.5.1", 216 | "subtle", 217 | "zeroize", 218 | ] 219 | 220 | [[package]] 221 | name = "cw-address-like" 222 | version = "1.0.4" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "451a4691083a88a3c0630a8a88799e9d4cd6679b7ce8ff22b8da2873ff31d380" 225 | dependencies = [ 226 | "cosmwasm-std", 227 | ] 228 | 229 | [[package]] 230 | name = "cw-ownable" 231 | version = "0.5.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "093dfb4520c48b5848274dd88ea99e280a04bc08729603341c7fb0d758c74321" 234 | dependencies = [ 235 | "cosmwasm-schema", 236 | "cosmwasm-std", 237 | "cw-address-like", 238 | "cw-ownable-derive", 239 | "cw-storage-plus", 240 | "cw-utils", 241 | "thiserror", 242 | ] 243 | 244 | [[package]] 245 | name = "cw-ownable-derive" 246 | version = "0.5.1" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "a1d3bf2e0f341bb6cc100d7d441d31cf713fbd3ce0c511f91e79f14b40a889af" 249 | dependencies = [ 250 | "proc-macro2", 251 | "quote", 252 | "syn 1.0.109", 253 | ] 254 | 255 | [[package]] 256 | name = "cw-paginate" 257 | version = "0.2.1" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "add278617f6251be1a35c781eb0fbffd44f899d8bb4dc5a9e420273a90684c4e" 260 | dependencies = [ 261 | "cosmwasm-std", 262 | "cw-storage-plus", 263 | "serde", 264 | ] 265 | 266 | [[package]] 267 | name = "cw-storage-plus" 268 | version = "1.1.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" 271 | dependencies = [ 272 | "cosmwasm-std", 273 | "schemars", 274 | "serde", 275 | ] 276 | 277 | [[package]] 278 | name = "cw-utils" 279 | version = "1.0.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" 282 | dependencies = [ 283 | "cosmwasm-schema", 284 | "cosmwasm-std", 285 | "cw2", 286 | "schemars", 287 | "semver", 288 | "serde", 289 | "thiserror", 290 | ] 291 | 292 | [[package]] 293 | name = "cw2" 294 | version = "1.1.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" 297 | dependencies = [ 298 | "cosmwasm-schema", 299 | "cosmwasm-std", 300 | "cw-storage-plus", 301 | "schemars", 302 | "serde", 303 | "thiserror", 304 | ] 305 | 306 | [[package]] 307 | name = "der" 308 | version = "0.6.1" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" 311 | dependencies = [ 312 | "const-oid", 313 | "zeroize", 314 | ] 315 | 316 | [[package]] 317 | name = "derivative" 318 | version = "2.2.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 321 | dependencies = [ 322 | "proc-macro2", 323 | "quote", 324 | "syn 1.0.109", 325 | ] 326 | 327 | [[package]] 328 | name = "digest" 329 | version = "0.9.0" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" 332 | dependencies = [ 333 | "generic-array", 334 | ] 335 | 336 | [[package]] 337 | name = "digest" 338 | version = "0.10.7" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 341 | dependencies = [ 342 | "block-buffer 0.10.4", 343 | "crypto-common", 344 | "subtle", 345 | ] 346 | 347 | [[package]] 348 | name = "dyn-clone" 349 | version = "1.0.11" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" 352 | 353 | [[package]] 354 | name = "ecdsa" 355 | version = "0.14.8" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" 358 | dependencies = [ 359 | "der", 360 | "elliptic-curve", 361 | "rfc6979", 362 | "signature", 363 | ] 364 | 365 | [[package]] 366 | name = "ed25519-zebra" 367 | version = "3.1.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" 370 | dependencies = [ 371 | "curve25519-dalek", 372 | "hashbrown", 373 | "hex", 374 | "rand_core 0.6.4", 375 | "serde", 376 | "sha2 0.9.9", 377 | "zeroize", 378 | ] 379 | 380 | [[package]] 381 | name = "either" 382 | version = "1.8.1" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 385 | 386 | [[package]] 387 | name = "elliptic-curve" 388 | version = "0.12.3" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" 391 | dependencies = [ 392 | "base16ct", 393 | "crypto-bigint", 394 | "der", 395 | "digest 0.10.7", 396 | "ff", 397 | "generic-array", 398 | "group", 399 | "pkcs8", 400 | "rand_core 0.6.4", 401 | "sec1", 402 | "subtle", 403 | "zeroize", 404 | ] 405 | 406 | [[package]] 407 | name = "ff" 408 | version = "0.12.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" 411 | dependencies = [ 412 | "rand_core 0.6.4", 413 | "subtle", 414 | ] 415 | 416 | [[package]] 417 | name = "forward_ref" 418 | version = "1.0.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" 421 | 422 | [[package]] 423 | name = "generic-array" 424 | version = "0.14.7" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 427 | dependencies = [ 428 | "typenum", 429 | "version_check", 430 | ] 431 | 432 | [[package]] 433 | name = "getrandom" 434 | version = "0.2.10" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 437 | dependencies = [ 438 | "cfg-if", 439 | "libc", 440 | "wasi", 441 | ] 442 | 443 | [[package]] 444 | name = "group" 445 | version = "0.12.1" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" 448 | dependencies = [ 449 | "ff", 450 | "rand_core 0.6.4", 451 | "subtle", 452 | ] 453 | 454 | [[package]] 455 | name = "hashbrown" 456 | version = "0.12.3" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 459 | dependencies = [ 460 | "ahash", 461 | ] 462 | 463 | [[package]] 464 | name = "hex" 465 | version = "0.4.3" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 468 | 469 | [[package]] 470 | name = "hmac" 471 | version = "0.12.1" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 474 | dependencies = [ 475 | "digest 0.10.7", 476 | ] 477 | 478 | [[package]] 479 | name = "ics999" 480 | version = "0.0.0" 481 | dependencies = [ 482 | "cosmwasm-schema", 483 | "cosmwasm-std", 484 | ] 485 | 486 | [[package]] 487 | name = "itertools" 488 | version = "0.10.5" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 491 | dependencies = [ 492 | "either", 493 | ] 494 | 495 | [[package]] 496 | name = "itoa" 497 | version = "1.0.8" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" 500 | 501 | [[package]] 502 | name = "k256" 503 | version = "0.11.6" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" 506 | dependencies = [ 507 | "cfg-if", 508 | "ecdsa", 509 | "elliptic-curve", 510 | "sha2 0.10.7", 511 | ] 512 | 513 | [[package]] 514 | name = "libc" 515 | version = "0.2.147" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 518 | 519 | [[package]] 520 | name = "mock-account-factory" 521 | version = "0.0.0" 522 | dependencies = [ 523 | "cosmwasm-schema", 524 | "cosmwasm-std", 525 | "cw-storage-plus", 526 | "cw-utils", 527 | "ics999", 528 | "thiserror", 529 | ] 530 | 531 | [[package]] 532 | name = "mock-counter" 533 | version = "0.0.0" 534 | dependencies = [ 535 | "cosmwasm-schema", 536 | "cosmwasm-std", 537 | "cw-storage-plus", 538 | "thiserror", 539 | ] 540 | 541 | [[package]] 542 | name = "mock-dex" 543 | version = "0.0.0" 544 | dependencies = [ 545 | "cosmwasm-schema", 546 | "cosmwasm-std", 547 | "cw-storage-plus", 548 | "cw-utils", 549 | "thiserror", 550 | ] 551 | 552 | [[package]] 553 | name = "mock-sender" 554 | version = "0.0.0" 555 | dependencies = [ 556 | "cosmwasm-schema", 557 | "cosmwasm-std", 558 | "cw-paginate", 559 | "cw-storage-plus", 560 | "ics999", 561 | "one-core", 562 | "thiserror", 563 | ] 564 | 565 | [[package]] 566 | name = "num-traits" 567 | version = "0.2.15" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 570 | dependencies = [ 571 | "autocfg", 572 | ] 573 | 574 | [[package]] 575 | name = "once_cell" 576 | version = "1.18.0" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 579 | 580 | [[package]] 581 | name = "one-account" 582 | version = "0.0.0" 583 | dependencies = [ 584 | "cosmwasm-schema", 585 | "cosmwasm-std", 586 | "cw-ownable", 587 | "cw-storage-plus", 588 | "cw2", 589 | "thiserror", 590 | ] 591 | 592 | [[package]] 593 | name = "one-core" 594 | version = "0.0.0" 595 | dependencies = [ 596 | "cosmwasm-schema", 597 | "cosmwasm-std", 598 | "cw-paginate", 599 | "cw-storage-plus", 600 | "cw-utils", 601 | "cw2", 602 | "ics999", 603 | "osmosis-std", 604 | "ripemd", 605 | "sha2 0.10.7", 606 | "thiserror", 607 | ] 608 | 609 | [[package]] 610 | name = "opaque-debug" 611 | version = "0.3.0" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 614 | 615 | [[package]] 616 | name = "osmosis-std" 617 | version = "0.15.3" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "87725a7480b98887167edf878daa52201a13322ad88e34355a7f2ddc663e047e" 620 | dependencies = [ 621 | "chrono", 622 | "cosmwasm-std", 623 | "osmosis-std-derive", 624 | "prost", 625 | "prost-types", 626 | "schemars", 627 | "serde", 628 | "serde-cw-value", 629 | ] 630 | 631 | [[package]] 632 | name = "osmosis-std-derive" 633 | version = "0.15.3" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "f4d482a16be198ee04e0f94e10dd9b8d02332dcf33bc5ea4b255e7e25eedc5df" 636 | dependencies = [ 637 | "itertools", 638 | "proc-macro2", 639 | "quote", 640 | "syn 1.0.109", 641 | ] 642 | 643 | [[package]] 644 | name = "pkcs8" 645 | version = "0.9.0" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" 648 | dependencies = [ 649 | "der", 650 | "spki", 651 | ] 652 | 653 | [[package]] 654 | name = "proc-macro2" 655 | version = "1.0.63" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" 658 | dependencies = [ 659 | "unicode-ident", 660 | ] 661 | 662 | [[package]] 663 | name = "prost" 664 | version = "0.11.9" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" 667 | dependencies = [ 668 | "bytes", 669 | "prost-derive", 670 | ] 671 | 672 | [[package]] 673 | name = "prost-derive" 674 | version = "0.11.9" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" 677 | dependencies = [ 678 | "anyhow", 679 | "itertools", 680 | "proc-macro2", 681 | "quote", 682 | "syn 1.0.109", 683 | ] 684 | 685 | [[package]] 686 | name = "prost-types" 687 | version = "0.11.9" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" 690 | dependencies = [ 691 | "prost", 692 | ] 693 | 694 | [[package]] 695 | name = "quote" 696 | version = "1.0.29" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" 699 | dependencies = [ 700 | "proc-macro2", 701 | ] 702 | 703 | [[package]] 704 | name = "rand_core" 705 | version = "0.5.1" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" 708 | 709 | [[package]] 710 | name = "rand_core" 711 | version = "0.6.4" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 714 | dependencies = [ 715 | "getrandom", 716 | ] 717 | 718 | [[package]] 719 | name = "rfc6979" 720 | version = "0.3.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" 723 | dependencies = [ 724 | "crypto-bigint", 725 | "hmac", 726 | "zeroize", 727 | ] 728 | 729 | [[package]] 730 | name = "ripemd" 731 | version = "0.1.3" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" 734 | dependencies = [ 735 | "digest 0.10.7", 736 | ] 737 | 738 | [[package]] 739 | name = "ryu" 740 | version = "1.0.14" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" 743 | 744 | [[package]] 745 | name = "schemars" 746 | version = "0.8.12" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" 749 | dependencies = [ 750 | "dyn-clone", 751 | "schemars_derive", 752 | "serde", 753 | "serde_json", 754 | ] 755 | 756 | [[package]] 757 | name = "schemars_derive" 758 | version = "0.8.12" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" 761 | dependencies = [ 762 | "proc-macro2", 763 | "quote", 764 | "serde_derive_internals", 765 | "syn 1.0.109", 766 | ] 767 | 768 | [[package]] 769 | name = "sec1" 770 | version = "0.3.0" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" 773 | dependencies = [ 774 | "base16ct", 775 | "der", 776 | "generic-array", 777 | "pkcs8", 778 | "subtle", 779 | "zeroize", 780 | ] 781 | 782 | [[package]] 783 | name = "semver" 784 | version = "1.0.17" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" 787 | 788 | [[package]] 789 | name = "serde" 790 | version = "1.0.166" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" 793 | dependencies = [ 794 | "serde_derive", 795 | ] 796 | 797 | [[package]] 798 | name = "serde-cw-value" 799 | version = "0.7.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" 802 | dependencies = [ 803 | "serde", 804 | ] 805 | 806 | [[package]] 807 | name = "serde-json-wasm" 808 | version = "0.5.1" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" 811 | dependencies = [ 812 | "serde", 813 | ] 814 | 815 | [[package]] 816 | name = "serde_derive" 817 | version = "1.0.166" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" 820 | dependencies = [ 821 | "proc-macro2", 822 | "quote", 823 | "syn 2.0.23", 824 | ] 825 | 826 | [[package]] 827 | name = "serde_derive_internals" 828 | version = "0.26.0" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" 831 | dependencies = [ 832 | "proc-macro2", 833 | "quote", 834 | "syn 1.0.109", 835 | ] 836 | 837 | [[package]] 838 | name = "serde_json" 839 | version = "1.0.100" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" 842 | dependencies = [ 843 | "itoa", 844 | "ryu", 845 | "serde", 846 | ] 847 | 848 | [[package]] 849 | name = "sha2" 850 | version = "0.9.9" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" 853 | dependencies = [ 854 | "block-buffer 0.9.0", 855 | "cfg-if", 856 | "cpufeatures", 857 | "digest 0.9.0", 858 | "opaque-debug", 859 | ] 860 | 861 | [[package]] 862 | name = "sha2" 863 | version = "0.10.7" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" 866 | dependencies = [ 867 | "cfg-if", 868 | "cpufeatures", 869 | "digest 0.10.7", 870 | ] 871 | 872 | [[package]] 873 | name = "signature" 874 | version = "1.6.4" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" 877 | dependencies = [ 878 | "digest 0.10.7", 879 | "rand_core 0.6.4", 880 | ] 881 | 882 | [[package]] 883 | name = "spki" 884 | version = "0.6.0" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" 887 | dependencies = [ 888 | "base64ct", 889 | "der", 890 | ] 891 | 892 | [[package]] 893 | name = "static_assertions" 894 | version = "1.1.0" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 897 | 898 | [[package]] 899 | name = "subtle" 900 | version = "2.5.0" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 903 | 904 | [[package]] 905 | name = "syn" 906 | version = "1.0.109" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 909 | dependencies = [ 910 | "proc-macro2", 911 | "quote", 912 | "unicode-ident", 913 | ] 914 | 915 | [[package]] 916 | name = "syn" 917 | version = "2.0.23" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" 920 | dependencies = [ 921 | "proc-macro2", 922 | "quote", 923 | "unicode-ident", 924 | ] 925 | 926 | [[package]] 927 | name = "thiserror" 928 | version = "1.0.41" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" 931 | dependencies = [ 932 | "thiserror-impl", 933 | ] 934 | 935 | [[package]] 936 | name = "thiserror-impl" 937 | version = "1.0.41" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" 940 | dependencies = [ 941 | "proc-macro2", 942 | "quote", 943 | "syn 2.0.23", 944 | ] 945 | 946 | [[package]] 947 | name = "typenum" 948 | version = "1.16.0" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 951 | 952 | [[package]] 953 | name = "uint" 954 | version = "0.9.5" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" 957 | dependencies = [ 958 | "byteorder", 959 | "crunchy", 960 | "hex", 961 | "static_assertions", 962 | ] 963 | 964 | [[package]] 965 | name = "unicode-ident" 966 | version = "1.0.10" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" 969 | 970 | [[package]] 971 | name = "version_check" 972 | version = "0.9.4" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 975 | 976 | [[package]] 977 | name = "wasi" 978 | version = "0.11.0+wasi-snapshot-preview1" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 981 | 982 | [[package]] 983 | name = "zeroize" 984 | version = "1.6.0" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" 987 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "contracts/account", 5 | "contracts/core", 6 | "contracts/mocks/*", 7 | "packages/*", 8 | ] 9 | 10 | [workspace.package] 11 | version = "0.0.0" 12 | authors = ["Larry Engineer "] 13 | edition = "2021" 14 | license = "UNLICENSED" 15 | homepage = "https://github.com/larry0x/ics999" 16 | repository = "https://github.com/larry0x/ics999" 17 | documentation = "https://github.com/larry0x/ics999#readme" 18 | keywords = ["blockchain", "cosmos", "cosmwasm", "ibc"] 19 | rust-version = "1.65.0" 20 | 21 | [workspace.dependencies] 22 | cosmwasm-schema = "1.2" 23 | cosmwasm-std = { version = "1.2", features = ["staking", "stargate", "ibc3", "cosmwasm_1_1", "cosmwasm_1_2"] } 24 | cw2 = "1.0" 25 | cw-ownable = "0.5" 26 | cw-paginate = "0.2" 27 | cw-storage-plus = "1.0" 28 | cw-utils = "1.0" 29 | osmosis-std = "0.15" 30 | ripemd = "0.1" 31 | sha2 = "0.10" 32 | thiserror = "1" 33 | 34 | [profile.release] 35 | codegen-units = 1 36 | debug = false 37 | debug-assertions = false 38 | incremental = false 39 | lto = true 40 | opt-level = 3 41 | overflow-checks = true 42 | rpath = false 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (c) larry0x, 2023 2 | 3 | Unless otherwise indicated, (a) all materials (including all source code, 4 | designs, and protocols) contained in this repository have been published for 5 | informational purposes only; (b) no license, right of reproduction, 6 | distribution, or other right with respect thereto is granted or implied; and 7 | (c) all moral, intellectual property, and other rights are hereby reserved by 8 | the copyright holder. 9 | 10 | THE SOFTWARE AND INTELLECTUAL PROPERTY INCLUDED IN THIS REPOSITORY IS PROVIDED 11 | BY THE COPYRIGHT HOLDERS "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, 12 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 13 | FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE 14 | COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 15 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THIS 16 | SOFTWARE OR INTELLECTUAL PROPERTY (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 17 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 18 | INTERRUPTION) HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, STRICT LIABILITY, 19 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF SUCH DAMAGES WERE 20 | REASONABLY FORESEEABLE OR THE COPYRIGHT HOLDERS WERE ADVISED OF THE POSSIBILITY 21 | OF SUCH DAMAGE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICS-999 2 | 3 | An all-in-one IBC protocol providing fungible token transfer, interchain account (ICA), and query (ICQ) functionalities, implemented in [CosmWasm][cosmwasm]. 4 | 5 | ## Requirements 6 | 7 | ICS-999 requires the following in order to work: 8 | 9 | - [wasmd][wasmd] >= 0.32 10 | - [tokenfactory][tf] module 11 | - tokenfactory's `denom_creation_fee` must be zero 12 | - tokenfactory's `Params` StargateQuery must be whitelisted ([example][stargate-query]) 13 | 14 | ## Acknowledgement 15 | 16 | We thank the authors of the following open source works, which ICS-999 took inspiration from: 17 | 18 | - [ICS-20][ics20] and [ICS-27][ics27] specifications, as well as their [Go implementations][ibc-go] 19 | - [Polytone][polytone] 20 | 21 | ## License 22 | 23 | (c) larry0x, 2023 - [All rights reserved](./LICENSE). 24 | 25 | [cosmwasm]: https://github.com/CosmWasm/cosmwasm 26 | [ibc-go]: https://github.com/cosmos/ibc-go 27 | [ics20]: https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer 28 | [ics27]: https://github.com/cosmos/ibc/tree/main/spec/app/ics-027-interchain-accounts 29 | [polytone]: https://github.com/DA0-DA0/polytone 30 | [stargate-query]: https://github.com/CosmosContracts/juno/blob/v15.0.0/app/keepers/keepers.go#L382-L402 31 | [tf]: https://github.com/osmosis-labs/osmosis/tree/main/x 32 | [wasmd]: https://github.com/CosmWasm/wasmd 33 | -------------------------------------------------------------------------------- /contracts/account/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "one-account" 3 | description = "ICS-999 interchain account contract" 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # use library feature to disable all instantiate/execute/query exports 20 | library = [] 21 | # for more explicit tests, cargo test --features=backtraces 22 | backtraces = ["cosmwasm-std/backtraces"] 23 | 24 | [dependencies] 25 | cosmwasm-schema = { workspace = true } 26 | cosmwasm-std = { workspace = true } 27 | cw2 = { workspace = true } 28 | cw-ownable = { workspace = true } 29 | cw-storage-plus = { workspace = true } 30 | thiserror = { workspace = true } 31 | -------------------------------------------------------------------------------- /contracts/account/README.md: -------------------------------------------------------------------------------- 1 | # one-account 2 | 3 | ICS-999 interchain account contract. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../LICENSE). 8 | -------------------------------------------------------------------------------- /contracts/account/src/lib.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::{ 2 | entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, 3 | QueryRequest, Reply, Response, SubMsg, 4 | }; 5 | 6 | pub type InstantiateMsg = Empty; 7 | pub type ExecuteMsg = CosmosMsg; 8 | pub type QueryMsg = QueryRequest; 9 | 10 | pub const CONTRACT_NAME: &str = "crates.io:one-account"; 11 | pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 12 | 13 | const REPLY_ID: u64 = 69420; 14 | 15 | #[derive(Debug, PartialEq, thiserror::Error)] 16 | pub enum Error { 17 | #[error(transparent)] 18 | Std(#[from] cosmwasm_std::StdError), 19 | 20 | #[error(transparent)] 21 | Ownership(#[from] cw_ownable::OwnershipError), 22 | 23 | #[error("query failed due to system error: {0}")] 24 | QuerySystem(#[from] cosmwasm_std::SystemError), 25 | 26 | #[error("query failed due to contract error: {0}")] 27 | QueryContract(String), 28 | 29 | #[error("submessage failed to execute: {0}")] 30 | SubMsgFailed(String), 31 | 32 | #[error("unknown reply id: {0}")] 33 | UnknownReplyId(u64), 34 | } 35 | 36 | type Result = core::result::Result; 37 | 38 | #[cfg_attr(not(feature = "library"), entry_point)] 39 | pub fn instantiate( 40 | deps: DepsMut, 41 | _: Env, 42 | info: MessageInfo, 43 | _: InstantiateMsg, 44 | ) -> Result { 45 | cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 46 | cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; 47 | 48 | Ok(Response::new() 49 | .add_attribute("method", "instantiate") 50 | .add_attribute("owner", info.sender)) 51 | } 52 | 53 | #[cfg_attr(not(feature = "library"), entry_point)] 54 | pub fn execute(deps: DepsMut, _: Env, info: MessageInfo, msg: ExecuteMsg) -> Result { 55 | cw_ownable::assert_owner(deps.storage, &info.sender)?; 56 | 57 | Ok(Response::new() 58 | .add_submessage(SubMsg::reply_on_success(msg, REPLY_ID)) 59 | .add_attribute("method", "execute")) 60 | } 61 | 62 | #[cfg_attr(not(feature = "library"), entry_point)] 63 | pub fn reply(_deps: DepsMut, _: Env, msg: Reply) -> Result { 64 | match msg.id { 65 | // if the submsg returned data, we need to forward it back to one-core 66 | // 67 | // NOTE: The `data` is protobuf-encoded MsgInstantiateContractResponse, 68 | // MsgExecuteContractResponse, etc. We don't decode them here. The ICA 69 | // controller is responsible for decoding it. 70 | REPLY_ID => { 71 | let mut res = Response::new(); 72 | 73 | // this submsg is reply on success, so we expect it to succeed 74 | let submsg_res = msg.result.into_result().map_err(Error::SubMsgFailed)?; 75 | if let Some(data) = submsg_res.data { 76 | res = res.set_data(data); 77 | } 78 | 79 | Ok(res) 80 | }, 81 | id => Err(Error::UnknownReplyId(id)), 82 | } 83 | } 84 | 85 | #[cfg_attr(not(feature = "library"), entry_point)] 86 | pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result { 87 | deps.querier 88 | .raw_query(&to_binary(&msg)?) 89 | .into_result()? 90 | .into_result() 91 | .map_err(Error::QueryContract) 92 | } 93 | 94 | // ----------------------------------- Tests ----------------------------------- 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use cosmwasm_std::{ 99 | coins, 100 | testing::{mock_dependencies, mock_env, mock_info}, 101 | BankMsg, CosmosMsg, SubMsgResult, SubMsgResponse, 102 | }; 103 | use cw_ownable::OwnershipError; 104 | 105 | use super::*; 106 | 107 | #[test] 108 | fn proper_execute() { 109 | let mut deps = mock_dependencies(); 110 | 111 | let cosmos_msg: CosmosMsg = BankMsg::Send { 112 | to_address: "larry".into(), 113 | amount: coins(88888, "uastro"), 114 | } 115 | .into(); 116 | 117 | instantiate( 118 | deps.as_mut(), 119 | mock_env(), 120 | mock_info("one-core", &[]), 121 | InstantiateMsg {}, 122 | ) 123 | .unwrap(); 124 | 125 | // not owner 126 | { 127 | let err = execute( 128 | deps.as_mut(), 129 | mock_env(), 130 | mock_info("larry", &[]), 131 | cosmos_msg.clone(), 132 | ) 133 | .unwrap_err(); 134 | assert_eq!(err, Error::Ownership(OwnershipError::NotOwner)); 135 | } 136 | 137 | // owner 138 | { 139 | let res = execute( 140 | deps.as_mut(), 141 | mock_env(), 142 | mock_info("one-core", &[]), 143 | cosmos_msg.clone(), 144 | ) 145 | .unwrap(); 146 | assert_eq!(res.messages, vec![SubMsg::reply_on_success(cosmos_msg, REPLY_ID)]); 147 | } 148 | } 149 | 150 | #[test] 151 | fn proper_reply() { 152 | let mut deps = mock_dependencies(); 153 | 154 | // no data 155 | { 156 | let res = reply( 157 | deps.as_mut(), 158 | mock_env(), 159 | Reply { 160 | id: REPLY_ID, 161 | result: SubMsgResult::Ok(SubMsgResponse { 162 | events: vec![], 163 | data: None, 164 | }), 165 | }, 166 | ) 167 | .unwrap(); 168 | assert_eq!(res.data, None); 169 | } 170 | 171 | // with data 172 | { 173 | let data = b"hello"; 174 | 175 | let res = reply( 176 | deps.as_mut(), 177 | mock_env(), 178 | Reply { 179 | id: REPLY_ID, 180 | result: SubMsgResult::Ok(SubMsgResponse { 181 | events: vec![], 182 | data: Some(data.into()), 183 | }), 184 | }, 185 | ) 186 | .unwrap(); 187 | assert_eq!(res.data, Some(data.into())); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /contracts/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "one-core" 3 | description = "ICS-999 core contract" 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # use library feature to disable all instantiate/execute/query exports 20 | library = [] 21 | # for more explicit tests, cargo test --features=backtraces 22 | backtraces = ["cosmwasm-std/backtraces"] 23 | 24 | [dependencies] 25 | cosmwasm-schema = { workspace = true } 26 | cosmwasm-std = { workspace = true } 27 | cw2 = { workspace = true } 28 | cw-paginate = { workspace = true } 29 | cw-storage-plus = { workspace = true } 30 | cw-utils = { workspace = true } 31 | ics999 = { path = "../../packages/ics999" } 32 | osmosis-std = { workspace = true } 33 | ripemd = { workspace = true } 34 | sha2 = { workspace = true } 35 | thiserror = { workspace = true } 36 | -------------------------------------------------------------------------------- /contracts/core/README.md: -------------------------------------------------------------------------------- 1 | # one-core 2 | 3 | ICS-999 core contract. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../LICENSE). 8 | -------------------------------------------------------------------------------- /contracts/core/src/contract.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | controller, 4 | error::{Error, Result}, 5 | handshake, host, 6 | msg::{AccountKey, Config, ExecuteMsg, QueryMsg}, 7 | query, 8 | state::CONFIG, 9 | AFTER_ACTION, AFTER_ALL_ACTIONS, AFTER_CALLBACK, CONTRACT_NAME, CONTRACT_VERSION, 10 | }, 11 | cosmwasm_std::{ 12 | entry_point, to_binary, Binary, Deps, DepsMut, Env, IbcBasicResponse, IbcChannelCloseMsg, 13 | IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcPacketAckMsg, 14 | IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, MessageInfo, Reply, Response, 15 | StdResult, 16 | }, 17 | }; 18 | 19 | #[entry_point] 20 | pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, cfg: Config) -> Result { 21 | cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; 22 | 23 | CONFIG.save(deps.storage, &cfg)?; 24 | 25 | Ok(Response::new()) 26 | } 27 | 28 | #[entry_point] 29 | pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result { 30 | match msg { 31 | ExecuteMsg::Dispatch { 32 | connection_id, 33 | actions, 34 | timeout, 35 | } => { 36 | if actions.is_empty() { 37 | return Err(Error::EmptyActionQueue); 38 | } 39 | 40 | controller::dispatch(deps, env, info, connection_id, actions, timeout) 41 | }, 42 | ExecuteMsg::Handle { 43 | counterparty_endpoint, 44 | endpoint, 45 | controller, 46 | actions, 47 | traces, 48 | } => { 49 | if info.sender != env.contract.address { 50 | return Err(Error::Unauthorized); 51 | } 52 | 53 | host::handle(deps, env, counterparty_endpoint, endpoint, controller, actions, traces) 54 | }, 55 | } 56 | } 57 | 58 | #[entry_point] 59 | pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { 60 | match msg.id { 61 | AFTER_ACTION => host::after_action(deps, env, msg.result), 62 | AFTER_ALL_ACTIONS => host::after_all_actions(msg.result), 63 | AFTER_CALLBACK => controller::after_callback(msg.result.is_ok()), 64 | id => unreachable!("unknown reply ID: `{id}`"), 65 | } 66 | } 67 | 68 | #[entry_point] 69 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { 70 | match msg { 71 | QueryMsg::Config {} => to_binary(&query::config(deps)?), 72 | QueryMsg::DenomHash { 73 | trace, 74 | } => to_binary(&query::denom_hash(trace)), 75 | QueryMsg::DenomTrace { 76 | denom, 77 | } => to_binary(&query::denom_trace(deps, denom)?), 78 | QueryMsg::DenomTraces { 79 | start_after, 80 | limit, 81 | } => to_binary(&query::denom_traces(deps, start_after, limit)?), 82 | QueryMsg::Account(AccountKey { 83 | src, 84 | controller, 85 | }) => to_binary(&query::account(deps, src, controller)?), 86 | QueryMsg::Accounts { 87 | start_after, 88 | limit, 89 | } => to_binary(&query::accounts(deps, start_after, limit)?), 90 | QueryMsg::ActiveChannel { 91 | connection_id, 92 | } => to_binary(&query::active_channel(deps, connection_id)?), 93 | QueryMsg::ActiveChannels { 94 | start_after, 95 | limit, 96 | } => to_binary(&query::active_channels(deps, start_after, limit)?), 97 | } 98 | } 99 | 100 | #[entry_point] 101 | pub fn ibc_channel_open( 102 | deps: DepsMut, 103 | _: Env, 104 | msg: IbcChannelOpenMsg, 105 | ) -> Result { 106 | match msg { 107 | IbcChannelOpenMsg::OpenInit { 108 | channel, 109 | } => handshake::open_init(deps, channel), 110 | IbcChannelOpenMsg::OpenTry { 111 | channel, 112 | counterparty_version, 113 | } => handshake::open_try(deps, channel, counterparty_version), 114 | } 115 | } 116 | 117 | #[entry_point] 118 | pub fn ibc_channel_connect( 119 | deps: DepsMut, 120 | _: Env, 121 | msg: IbcChannelConnectMsg, 122 | ) -> Result { 123 | handshake::open_connect(deps, msg.channel(), msg.counterparty_version()) 124 | } 125 | 126 | #[entry_point] 127 | pub fn ibc_channel_close(_: DepsMut, _: Env, msg: IbcChannelCloseMsg) -> Result { 128 | handshake::close(msg) 129 | } 130 | 131 | #[entry_point] 132 | pub fn ibc_packet_receive( 133 | _: DepsMut, 134 | env: Env, 135 | msg: IbcPacketReceiveMsg, 136 | ) -> Result { 137 | host::packet_receive(env, msg.packet) 138 | } 139 | 140 | #[entry_point] 141 | pub fn ibc_packet_ack(deps: DepsMut, env: Env, msg: IbcPacketAckMsg) -> Result { 142 | controller::packet_lifecycle_complete( 143 | deps, 144 | env, 145 | msg.original_packet, 146 | Some(msg.acknowledgement.data), 147 | ) 148 | } 149 | 150 | #[entry_point] 151 | pub fn ibc_packet_timeout( 152 | deps: DepsMut, 153 | env: Env, 154 | msg: IbcPacketTimeoutMsg, 155 | ) -> Result { 156 | controller::packet_lifecycle_complete(deps, env, msg.packet, None) 157 | } 158 | -------------------------------------------------------------------------------- /contracts/core/src/controller/mod.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::{Error, Result}, 4 | state::{ACTIVE_CHANNELS, CONFIG, DENOM_TRACES}, 5 | transfer::{burn, escrow, mint, release, TraceItem}, 6 | utils::Coins, 7 | AFTER_CALLBACK, 8 | }, 9 | cosmwasm_std::{ 10 | from_slice, to_binary, Binary, Coin, DepsMut, Env, IbcBasicResponse, 11 | IbcMsg, IbcPacket, IbcTimeout, MessageInfo, Response, Storage, SubMsg, WasmMsg, 12 | }, 13 | ics999::{Action, CallbackMsg, ControllerExecuteMsg, PacketData, PacketOutcome, Trace}, 14 | }; 15 | 16 | pub fn dispatch( 17 | deps: DepsMut, 18 | env: Env, 19 | info: MessageInfo, 20 | connection_id: String, 21 | actions: Vec, 22 | timeout: Option, 23 | ) -> Result { 24 | let received_funds = Coins::from(info.funds); 25 | let mut sending_funds = Coins::empty(); 26 | let mut msgs = vec![]; 27 | let mut attrs = vec![]; 28 | let mut traces: Vec = vec![]; 29 | 30 | // find the current chain's port and channel IDs 31 | let endpoint = ACTIVE_CHANNELS.load(deps.storage, &connection_id)?; 32 | 33 | // go through all transfer actions, either escrow or burn the coins based on 34 | // whether the current chain is the source or the sink. 35 | // also, compose the traces which will be included in the packet. 36 | for action in &actions { 37 | if let Action::Transfer { denom, amount, .. } = action { 38 | let trace = trace_of(deps.storage, denom)?; 39 | 40 | let coin = Coin { 41 | denom: denom.clone(), 42 | amount: *amount, 43 | }; 44 | 45 | if trace.sender_is_source(&endpoint) { 46 | escrow(&coin, &mut attrs); 47 | } else { 48 | // note that we burn from the contract address instead of from 49 | // info.sender 50 | // this is because the token to be burned should have already 51 | // been sent to the contract address along with the executeMsg 52 | burn(&env.contract.address, coin.clone(), &mut msgs, &mut attrs); 53 | } 54 | 55 | if !traces.iter().any(|trace| trace.denom == *denom) { 56 | traces.push(trace.into_full_trace(denom)); 57 | } 58 | 59 | sending_funds.add(coin)?; 60 | } 61 | } 62 | 63 | // the total amount of coins the user has sent to the contract must equal 64 | // the amount they want to transfer via IBC 65 | if received_funds != sending_funds { 66 | return Err(Error::FundsMismatch { 67 | actual: received_funds, 68 | expected: sending_funds, 69 | }); 70 | } 71 | 72 | // if the user does not specify a timeout, we use the default 73 | let timeout = match timeout { 74 | None => { 75 | let cfg = CONFIG.load(deps.storage)?; 76 | IbcTimeout::with_timestamp(env.block.time.plus_seconds(cfg.default_timeout_secs)) 77 | }, 78 | Some(to) => to, 79 | }; 80 | 81 | Ok(Response::new() 82 | .add_attribute("method", "dispatch") 83 | .add_attributes(attrs) 84 | .add_messages(msgs) 85 | .add_message(IbcMsg::SendPacket { 86 | channel_id: endpoint.channel_id, 87 | data: to_binary(&PacketData { 88 | controller: info.sender.into(), 89 | actions, 90 | traces, 91 | })?, 92 | timeout, 93 | })) 94 | } 95 | 96 | pub fn packet_lifecycle_complete( 97 | deps: DepsMut, 98 | env: Env, 99 | packet: IbcPacket, 100 | ack_bin: Option, 101 | ) -> Result { 102 | let mut msgs = vec![]; 103 | let mut attrs = vec![]; 104 | 105 | // deserialize the original packet 106 | let packet_data: PacketData = from_slice(&packet.data)?; 107 | 108 | // deserialize the ack 109 | let ack = ack_bin.map(|bin| from_slice(&bin)).transpose()?; 110 | let outcome: PacketOutcome = ack.into(); 111 | 112 | // process refund if the packet timed out or failed 113 | if should_refund(&outcome) { 114 | for action in &packet_data.actions { 115 | if let Action::Transfer { denom, amount, .. } = action { 116 | let trace = trace_of(deps.storage, denom)?; 117 | 118 | let coin = Coin { 119 | denom: denom.clone(), 120 | amount: *amount, 121 | }; 122 | 123 | // do the reverse of what was done in `act` 124 | // if the tokens were escrowed, then release them 125 | // if the tokens were burned, then mint them 126 | if trace.sender_is_source(&packet.src) { 127 | release(coin, &packet_data.controller, &mut msgs, &mut attrs); 128 | } else { 129 | mint(&env.contract.address, &packet_data.controller, coin, &mut msgs, &mut attrs); 130 | } 131 | } 132 | } 133 | } 134 | 135 | Ok(IbcBasicResponse::new() 136 | .add_attribute("method", "packet_lifecycle_complete") 137 | .add_attribute("controller", &packet_data.controller) 138 | .add_attribute("port_id", &packet.src.port_id) 139 | .add_attribute("channel_id", &packet.src.channel_id) 140 | .add_attribute("sequence", packet.sequence.to_string()) 141 | .add_attribute("outcome", outcome.ty()) 142 | .add_attributes(attrs) 143 | .add_messages(msgs) 144 | .add_submessage(SubMsg::reply_always( 145 | WasmMsg::Execute { 146 | contract_addr: packet_data.controller, 147 | msg: to_binary(&ControllerExecuteMsg::Ics999(CallbackMsg { 148 | endpoint: packet.src, 149 | sequence: packet.sequence, 150 | outcome, 151 | }))?, 152 | funds: vec![], 153 | }, 154 | AFTER_CALLBACK, 155 | ))) 156 | } 157 | 158 | // this method must succeed whether the callback was successful or not 159 | // if the callback failed, we simply log it here 160 | pub fn after_callback(success: bool) -> Result { 161 | Ok(Response::new() 162 | .add_attribute("method", "after_callback") 163 | .add_attribute("success", success.to_string())) 164 | } 165 | 166 | /// Find the trace associated with a denom. 167 | /// 168 | /// If there isn't a trace stored for this denom, then the current chain must be 169 | /// the source. In this case, initialize a new trace with the current chain 170 | /// being the first and only step in the path. 171 | fn trace_of(store: &dyn Storage, denom: &str) -> Result { 172 | Ok(DENOM_TRACES 173 | .may_load(store, denom)? 174 | .unwrap_or_else(|| TraceItem::new(denom))) 175 | } 176 | 177 | fn should_refund(outcome: &PacketOutcome) -> bool { 178 | match outcome { 179 | // packet timed out -- refund 180 | PacketOutcome::Timeout {} => true, 181 | 182 | // packet acknowledged but failed -- refund 183 | PacketOutcome::Failed(_) => true, 184 | 185 | // packet acknowledged and succeeded -- no refund 186 | PacketOutcome::Success(_) => false, 187 | } 188 | } 189 | 190 | // ----------------------------------- Tests ----------------------------------- 191 | 192 | #[cfg(test)] 193 | mod tests { 194 | use cosmwasm_std::{ 195 | testing::{mock_dependencies, mock_env, mock_info}, 196 | IbcEndpoint, Uint128, 197 | }; 198 | 199 | use crate::msg::Config; 200 | use super::*; 201 | 202 | #[test] 203 | fn asserting_funds() { 204 | struct TestCase { 205 | sending_funds: Vec, 206 | should_ok: bool, 207 | } 208 | 209 | // this contains the correct amount of coins expected to be sent 210 | let actions = vec![ 211 | Action::Transfer { 212 | denom: "uatom".into(), 213 | amount: Uint128::new(10000), 214 | recipient: None, 215 | }, 216 | Action::Transfer { 217 | denom: "uosmo".into(), 218 | amount: Uint128::new(23456), 219 | recipient: None, 220 | }, 221 | Action::Transfer { 222 | denom: "uatom".into(), 223 | amount: Uint128::new(2345), 224 | recipient: Some("pumpkin".into()), 225 | }, 226 | ]; 227 | 228 | let testcases = [ 229 | // no fund sent 230 | TestCase { 231 | sending_funds: vec![], 232 | should_ok: false, 233 | }, 234 | 235 | // only 1 coin sent 236 | TestCase { 237 | sending_funds: vec![ 238 | Coin { 239 | denom: "uatom".into(), 240 | amount: Uint128::new(12345), 241 | }, 242 | ], 243 | should_ok: false, 244 | }, 245 | 246 | // two coins sent but incorrect amount 247 | TestCase { 248 | sending_funds: vec![ 249 | Coin { 250 | denom: "uatom".into(), 251 | amount: Uint128::new(12345), 252 | }, 253 | Coin { 254 | denom: "uosmo".into(), 255 | amount: Uint128::new(12345), 256 | }, 257 | ], 258 | should_ok: false, 259 | }, 260 | 261 | // extra coins sent 262 | TestCase { 263 | sending_funds: vec![ 264 | Coin { 265 | denom: "uatom".into(), 266 | amount: Uint128::new(12345), 267 | }, 268 | Coin { 269 | denom: "uosmo".into(), 270 | amount: Uint128::new(23456), 271 | }, 272 | Coin { 273 | denom: "ujuno".into(), 274 | amount: Uint128::new(34567), 275 | }, 276 | ], 277 | should_ok: false, 278 | }, 279 | 280 | // correct funds sent 281 | TestCase { 282 | sending_funds: vec![ 283 | Coin { 284 | denom: "uatom".into(), 285 | amount: Uint128::new(12345), 286 | }, 287 | Coin { 288 | denom: "uosmo".into(), 289 | amount: Uint128::new(23456), 290 | }, 291 | ], 292 | should_ok: true, 293 | }, 294 | ]; 295 | 296 | for testcase in testcases { 297 | let mut deps = mock_dependencies(); 298 | 299 | let mock_connection_id = "connection-0"; 300 | let mock_active_channel = IbcEndpoint { port_id: "port-0".into(), channel_id: "channel-0".into() }; 301 | let mock_cfg = Config { default_account_code_id: 1, default_timeout_secs: 300 }; 302 | 303 | CONFIG 304 | .save(deps.as_mut().storage, &mock_cfg) 305 | .unwrap(); 306 | ACTIVE_CHANNELS 307 | .save(deps.as_mut().storage, mock_connection_id, &mock_active_channel) 308 | .unwrap(); 309 | 310 | let result = dispatch( 311 | deps.as_mut(), 312 | mock_env(), 313 | mock_info("larry", &testcase.sending_funds), 314 | mock_connection_id.into(), 315 | actions.clone(), 316 | None, 317 | ); 318 | 319 | if testcase.should_ok { 320 | assert!(result.is_ok()); 321 | } else { 322 | assert!(matches!(result, Err(Error::FundsMismatch { .. }))); 323 | } 324 | } 325 | } 326 | 327 | #[test] 328 | fn sending_packet() { 329 | // TODO 330 | } 331 | 332 | #[test] 333 | fn receiving_packet() { 334 | // TODO 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /contracts/core/src/error.rs: -------------------------------------------------------------------------------- 1 | use cosmwasm_std::IbcEndpoint; 2 | 3 | use { 4 | crate::utils::Coins, 5 | cosmwasm_std::{IbcOrder, Instantiate2AddressError, OverflowError, StdError}, 6 | cw_utils::{ParseReplyError, PaymentError}, 7 | }; 8 | 9 | #[derive(Debug, PartialEq, thiserror::Error)] 10 | pub enum Error { 11 | #[error(transparent)] 12 | Std(#[from] StdError), 13 | 14 | #[error(transparent)] 15 | Overflow(#[from] OverflowError), 16 | 17 | #[error(transparent)] 18 | Instantiate2Address(#[from] Instantiate2AddressError), 19 | 20 | #[error(transparent)] 21 | Payment(#[from] PaymentError), 22 | 23 | #[error(transparent)] 24 | ParseReply(#[from] ParseReplyError), 25 | 26 | #[error("query failed due to system error: {0}")] 27 | QuerySystem(#[from] cosmwasm_std::SystemError), 28 | 29 | #[error("query failed due to contract error: {0}")] 30 | QueryContract(String), 31 | 32 | #[error("action queue cannot be empty")] 33 | EmptyActionQueue, 34 | 35 | #[error("account factory failed to return instantiate data in its response")] 36 | FactoryResponseDataMissing, 37 | 38 | #[error("cannot create voucher token because token create fee is non-zero")] 39 | NonZeroTokenCreationFee, 40 | 41 | #[error("unauthorized")] 42 | Unauthorized, 43 | 44 | #[error("ICS-999 channel may not be closed")] 45 | UnexpectedChannelClosure, 46 | 47 | #[error("packet does not contain the trace for denom `{denom}`")] 48 | TraceNotFound { 49 | denom: String, 50 | }, 51 | 52 | #[error("incorrect amount of funds sent: expecting `{expected}`, found `{actual}`")] 53 | FundsMismatch { 54 | actual: Coins, 55 | expected: Coins, 56 | }, 57 | 58 | #[error("incorrect IBC channel order: expecting `{expected:?}`, found `{actual:?}`")] 59 | IncorrectOrder { 60 | actual: IbcOrder, 61 | expected: IbcOrder, 62 | }, 63 | 64 | #[error("incorrect IBC channel version: expecting `{expected}`, found `{actual}`")] 65 | IncorrectVersion { 66 | actual: String, 67 | expected: String, 68 | }, 69 | 70 | #[error("an open ICS-999 channel already exists on connection `{connection_id}`")] 71 | ChannelExists { 72 | connection_id: String, 73 | }, 74 | 75 | #[error("an interchain account already exists for endpoint `{endpoint:?}`, and controller `{controller}`")] 76 | AccountExists { 77 | endpoint: IbcEndpoint, 78 | controller: String, 79 | }, 80 | 81 | #[error("no interchain account found at endpoint `{endpoint:?}`, and controller `{controller}`")] 82 | AccountNotFound { 83 | endpoint: IbcEndpoint, 84 | controller: String, 85 | }, 86 | } 87 | 88 | pub(crate) type Result = core::result::Result; 89 | -------------------------------------------------------------------------------- /contracts/core/src/handshake.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::{Error, Result}, 4 | state::ACTIVE_CHANNELS, 5 | }, 6 | cosmwasm_std::{ 7 | DepsMut, IbcBasicResponse, IbcChannel, IbcChannelCloseMsg, IbcChannelOpenResponse, 8 | IbcOrder, Storage, 9 | }, 10 | ics999, 11 | }; 12 | 13 | pub fn open_init( 14 | deps: DepsMut, 15 | channel: IbcChannel, 16 | ) -> Result { 17 | validate_order_and_version(&channel.order, &channel.version, None)?; 18 | 19 | // only one active ICS-999 channel per connection 20 | assert_unique_channel(deps.storage, &channel.connection_id)?; 21 | 22 | // no need to validate counterparty version at this step, because we don't 23 | // know what it is yet 24 | // 25 | // return None means we don't want to set the version to a different value 26 | Ok(None) 27 | } 28 | 29 | pub fn open_try( 30 | deps: DepsMut, 31 | channel: IbcChannel, 32 | counterparty_version: String, 33 | ) -> Result { 34 | validate_order_and_version(&channel.order, &channel.version, Some(&counterparty_version))?; 35 | 36 | assert_unique_channel(deps.storage, &channel.connection_id)?; 37 | 38 | Ok(None) 39 | } 40 | 41 | pub fn open_connect( 42 | deps: DepsMut, 43 | channel: &IbcChannel, 44 | counterparty_version: Option<&str>, 45 | ) -> Result { 46 | validate_order_and_version(&channel.order, &channel.version, counterparty_version)?; 47 | 48 | ACTIVE_CHANNELS.save(deps.storage, &channel.connection_id, &channel.endpoint)?; 49 | 50 | Ok(IbcBasicResponse::new() 51 | .add_attribute("method", "open_connect") 52 | .add_attribute("connection_id", &channel.connection_id) 53 | .add_attribute("port_id", &channel.endpoint.port_id) 54 | .add_attribute("channel_id", &channel.endpoint.channel_id)) 55 | } 56 | 57 | fn validate_order_and_version( 58 | order: &IbcOrder, 59 | version: &str, 60 | counterparty_version: Option<&str>, 61 | ) -> Result<()> { 62 | if *order != ics999::ORDER { 63 | return Err(Error::IncorrectOrder { 64 | actual: order.clone(), 65 | expected: ics999::ORDER, 66 | }); 67 | } 68 | 69 | if version != ics999::VERSION { 70 | return Err(Error::IncorrectVersion { 71 | actual: version.into(), 72 | expected: ics999::VERSION.into(), 73 | }); 74 | } 75 | 76 | if let Some(cp_version) = counterparty_version { 77 | if cp_version != ics999::VERSION { 78 | return Err(Error::IncorrectVersion { 79 | actual: cp_version.into(), 80 | expected: ics999::VERSION.into(), 81 | }); 82 | } 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | fn assert_unique_channel(store: &dyn Storage, connection_id: &str) -> Result<()> { 89 | if ACTIVE_CHANNELS.has(store, connection_id) { 90 | return Err(Error::ChannelExists { 91 | connection_id: connection_id.into(), 92 | }); 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | pub fn close(msg: IbcChannelCloseMsg) -> Result { 99 | match msg { 100 | // we do not expect an ICS-999 channel to be closed 101 | IbcChannelCloseMsg::CloseInit { 102 | .. 103 | } => Err(Error::UnexpectedChannelClosure), 104 | 105 | // If we're here, something has gone catastrophically wrong on our 106 | // counterparty chain. Per the CloseInit handler above, this contract 107 | // should never allow its channel to be closed. 108 | // 109 | // Note: Erroring here would prevent our side of the channel closing, 110 | // leading to a situation where the counterparty thinks the channel is 111 | // closed, but we think it's still open. To avoid this inconsistency, 112 | // we must let the tx go through. 113 | // 114 | // We probably should delete the ACTIVE_CHANNEL, since the channel is 115 | // now closed... However, as we're in a catastrophic situation that 116 | // requires admin intervention anyways, let's leave this to the admin. 117 | IbcChannelCloseMsg::CloseConfirm { 118 | .. 119 | } => Ok(IbcBasicResponse::new()), 120 | } 121 | } 122 | 123 | // ----------------------------------- Tests ----------------------------------- 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use cosmwasm_std::{ 128 | testing::{mock_dependencies, MOCK_CONTRACT_ADDR}, 129 | IbcEndpoint, 130 | }; 131 | 132 | use super::*; 133 | 134 | fn mock_ibc_endpoint() -> IbcEndpoint { 135 | IbcEndpoint { 136 | port_id: format!("wasm.{MOCK_CONTRACT_ADDR}"), 137 | channel_id: "channel-0".into(), 138 | } 139 | } 140 | 141 | fn mock_ibc_channel() -> IbcChannel { 142 | IbcChannel::new( 143 | mock_ibc_endpoint(), 144 | mock_ibc_endpoint(), 145 | ics999::ORDER, 146 | ics999::VERSION, 147 | "connection-0", 148 | ) 149 | } 150 | 151 | #[test] 152 | fn proper_open_init() { 153 | let mut deps = mock_dependencies(); 154 | 155 | // valid channel 156 | { 157 | let res = open_init(deps.as_mut(), mock_ibc_channel()).unwrap(); 158 | assert_eq!(res, None); 159 | } 160 | 161 | // incorrect ordering 162 | { 163 | let mut channel = mock_ibc_channel(); 164 | channel.order = IbcOrder::Ordered; 165 | 166 | let err = open_init(deps.as_mut(), channel).unwrap_err(); 167 | assert!(matches!(err, Error::IncorrectOrder { .. })); 168 | } 169 | 170 | // incorrect version 171 | { 172 | let mut channel = mock_ibc_channel(); 173 | channel.version = "ics20".into(); 174 | 175 | let err = open_init(deps.as_mut(), channel).unwrap_err(); 176 | assert!(matches!(err, Error::IncorrectVersion { .. })); 177 | } 178 | 179 | // channel already exists for the connection 180 | { 181 | let channel = mock_ibc_channel(); 182 | 183 | ACTIVE_CHANNELS 184 | .save( 185 | deps.as_mut().storage, 186 | &channel.connection_id, 187 | &IbcEndpoint { 188 | port_id: "port-123".into(), 189 | channel_id: "channel-123".into(), 190 | }, 191 | ) 192 | .unwrap(); 193 | 194 | let err = open_init(deps.as_mut(), channel).unwrap_err(); 195 | assert!(matches!(err, Error::ChannelExists { .. })); 196 | } 197 | } 198 | 199 | #[test] 200 | fn proper_open_try() { 201 | let mut deps = mock_dependencies(); 202 | 203 | // valid channel 204 | { 205 | let res = open_try(deps.as_mut(), mock_ibc_channel(), ics999::VERSION.into()).unwrap(); 206 | assert_eq!(res, None); 207 | } 208 | 209 | // incorrect countarparty version 210 | { 211 | let err = open_try(deps.as_mut(), mock_ibc_channel(), "ics20".into()).unwrap_err(); 212 | assert!(matches!(err, Error::IncorrectVersion { .. })); 213 | } 214 | } 215 | 216 | #[test] 217 | fn proper_open_connect() { 218 | let mut deps = mock_dependencies(); 219 | 220 | let channel = mock_ibc_channel(); 221 | 222 | let res = open_connect(deps.as_mut(), &channel, Some(ics999::VERSION)).unwrap(); 223 | assert!(res.messages.is_empty()); 224 | 225 | let active_channel = ACTIVE_CHANNELS.load(deps.as_ref().storage, &channel.connection_id).unwrap(); 226 | assert_eq!(active_channel, channel.endpoint); 227 | } 228 | 229 | #[test] 230 | fn rejecting_channel_close() { 231 | let err = close(IbcChannelCloseMsg::CloseInit { 232 | channel: mock_ibc_channel(), 233 | }) 234 | .unwrap_err(); 235 | assert_eq!(err, Error::UnexpectedChannelClosure); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /contracts/core/src/host/handler.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::{Error, Result}, 4 | state::{ACCOUNTS, CONFIG, DENOM_TRACES}, 5 | transfer::{assert_free_denom_creation, construct_denom, into_proto_coin, TraceItem}, 6 | AFTER_ACTION, 7 | }, 8 | cosmwasm_schema::cw_serde, 9 | cosmwasm_std::{ 10 | from_binary, instantiate2_address, to_binary, Addr, BankMsg, Binary, Coin, Deps, DepsMut, 11 | Empty, Env, IbcEndpoint, QueryRequest, Response, StdResult, Storage, SubMsg, Uint128, 12 | WasmMsg, WasmQuery, 13 | }, 14 | cw_storage_plus::Item, 15 | cw_utils::parse_execute_response_data, 16 | ics999::{ 17 | Action, ActionResult, FactoryExecuteMsg, FactoryMsg, FactoryResponse, RegisterOptions, 18 | Trace, 19 | }, 20 | osmosis_std::types::osmosis::tokenfactory::v1beta1 as tokenfactory, 21 | sha2::{Digest, Sha256}, 22 | }; 23 | 24 | const HANDLER: Item = Item::new("handler"); 25 | 26 | /// An ICS-999 packet contains one or more `Action`'s that need to be executed 27 | /// one at a time and atomically. 28 | /// 29 | /// Handler is an object that contains necessary states and methods for 30 | /// executing the actions. It also implements serde traits so that it can be 31 | /// saved/loaded from the contract store. 32 | #[cw_serde] 33 | pub(super) struct Handler { 34 | counterparty_endpoint: IbcEndpoint, 35 | endpoint: IbcEndpoint, 36 | controller: String, 37 | host: Option, 38 | traces: Vec, 39 | action: Option, 40 | pending_actions: Vec, 41 | results: Vec, 42 | } 43 | 44 | impl Handler { 45 | pub fn create( 46 | store: &dyn Storage, 47 | counterparty_endpoint: IbcEndpoint, 48 | endpoint: IbcEndpoint, 49 | controller: String, 50 | mut actions: Vec, 51 | traces: Vec, 52 | ) -> StdResult { 53 | // load the controller's ICA host, which may or may not have already 54 | // been instantiated 55 | let host = ACCOUNTS.may_load(store, (&endpoint.port_id, &endpoint.channel_id, &controller))?; 56 | 57 | // reverse the actions, so that we can use pop() to grab the 1st action 58 | actions.reverse(); 59 | 60 | Ok(Self { 61 | counterparty_endpoint, 62 | endpoint, 63 | controller, 64 | host, 65 | traces, 66 | action: None, 67 | pending_actions: actions, 68 | results: vec![], 69 | }) 70 | } 71 | 72 | pub fn load(store: &dyn Storage) -> StdResult { 73 | HANDLER.load(store) 74 | } 75 | 76 | fn save(&self, store: &mut dyn Storage) -> StdResult<()> { 77 | HANDLER.save(store, self) 78 | } 79 | 80 | fn remove(store: &mut dyn Storage) { 81 | HANDLER.remove(store) 82 | } 83 | 84 | /// Execute the next action in the queue. Saved the updated handler state. 85 | pub fn handle_next_action( 86 | mut self, 87 | mut deps: DepsMut, 88 | env: Env, 89 | response: Option, 90 | ) -> Result { 91 | let mut response = response.unwrap_or_else(|| self.default_handle_action_response()); 92 | 93 | // grab the first action in the queue 94 | self.action = self.pending_actions.pop(); 95 | 96 | // if there is no more action to execute 97 | // delete handler state from contract store, return the results as data 98 | // in the response 99 | let Some(action) = &self.action else { 100 | Handler::remove(deps.storage); 101 | return Ok(response.set_data(to_binary(&self.results)?)); 102 | }; 103 | 104 | // convert the action to the appropriate msgs and event attributes 105 | let response = match action.clone() { 106 | Action::Transfer { 107 | denom, 108 | amount, 109 | recipient, 110 | } => self.handle_transfer(response, deps.branch(), env, denom, amount, recipient)?, 111 | 112 | Action::RegisterAccount(RegisterOptions::Default { 113 | salt, 114 | }) => self.handle_register_account_default(response, deps.branch(), env, salt)?, 115 | 116 | Action::RegisterAccount(RegisterOptions::CustomFactory { 117 | address, 118 | data, 119 | }) => self.handle_register_account_custom_factory(response, address, data)?, 120 | 121 | Action::Query(msg) => { 122 | response = self.handle_query(response, deps.as_ref(), msg)?; 123 | return self.handle_next_action(deps, env, Some(response)); 124 | }, 125 | 126 | Action::Execute(msg) => self.handle_execute(response, msg)?, 127 | }; 128 | 129 | self.save(deps.storage)?; 130 | 131 | Ok(response) 132 | } 133 | 134 | fn handle_transfer( 135 | &mut self, 136 | mut response: Response, 137 | deps: DepsMut, 138 | env: Env, 139 | src_denom: String, 140 | amount: Uint128, 141 | recipient: Option, 142 | ) -> Result { 143 | response = response.add_attribute("action", "transfer"); 144 | 145 | let mut trace: TraceItem = self 146 | .traces 147 | .iter() 148 | .find(|trace| trace.denom == src_denom) 149 | .ok_or(Error::TraceNotFound { 150 | denom: src_denom, 151 | })? 152 | .into(); 153 | 154 | let recipient = match recipient { 155 | // if the sender doesn't specify the recipient, default to 156 | // their interchain account 157 | // error if the sender does not already own an ICA 158 | None => self.get_host().cloned()?, 159 | 160 | // if the sender does specify a recipient, simply validate 161 | // the address 162 | Some(r) => deps.api.addr_validate(&r)?, 163 | }; 164 | 165 | if trace.sender_is_source(&self.counterparty_endpoint) { 166 | // append current chain to the path 167 | trace.path.push(self.endpoint.clone()); 168 | 169 | // derive the ibc denom 170 | let subdenom = trace.hash().to_hex(); 171 | let denom = construct_denom(env.contract.address.as_str(), &subdenom); 172 | 173 | let new_token = !DENOM_TRACES.has(deps.storage, &denom); 174 | 175 | // if the denom does not exist yet -- create the denom and 176 | // save the trace to store 177 | if new_token { 178 | DENOM_TRACES.save(deps.storage, &denom, &trace)?; 179 | 180 | // we can only create the denom if denom creation fee 181 | // is zero 182 | assert_free_denom_creation(&deps.querier)?; 183 | 184 | response = response.add_message(tokenfactory::MsgCreateDenom { 185 | sender: env.contract.address.to_string(), 186 | subdenom, 187 | }); 188 | } 189 | 190 | self.results.push(ActionResult::Transfer { 191 | denom: denom.clone(), 192 | new_token, 193 | recipient: recipient.to_string(), 194 | }); 195 | 196 | let coin = Coin { 197 | denom, 198 | amount, 199 | }; 200 | 201 | // tokenfactory only supports minting to the sender 202 | // therefore we first mint to ourself, then transfer to the recipient 203 | Ok(response 204 | .add_message(tokenfactory::MsgMint { 205 | sender: env.contract.address.clone().into(), 206 | mint_to_address: env.contract.address.into(), 207 | amount: Some(into_proto_coin(coin.clone())), 208 | }) 209 | .add_submessage(SubMsg::reply_on_success( 210 | BankMsg::Send { 211 | to_address: recipient.into(), 212 | amount: vec![coin], 213 | }, 214 | AFTER_ACTION, 215 | ))) 216 | } else { 217 | // pop the sender chain from the path 218 | trace.path.pop(); 219 | 220 | // derive the ibc denom 221 | let denom = if trace.path.is_empty() { 222 | trace.base_denom 223 | } else { 224 | let subdenom = trace.hash().to_hex(); 225 | construct_denom(env.contract.address.as_str(), &subdenom) 226 | }; 227 | 228 | self.results.push(ActionResult::Transfer { 229 | denom: denom.clone(), 230 | new_token: false, 231 | recipient: recipient.to_string(), 232 | }); 233 | 234 | let coin = Coin { 235 | denom, 236 | amount, 237 | }; 238 | 239 | Ok(response.add_submessage(SubMsg::reply_on_success( 240 | BankMsg::Send { 241 | to_address: recipient.into(), 242 | amount: vec![coin], 243 | }, 244 | AFTER_ACTION, 245 | ))) 246 | } 247 | } 248 | 249 | fn handle_register_account_default( 250 | &mut self, 251 | response: Response, 252 | deps: DepsMut, 253 | env: Env, 254 | salt: Option, 255 | ) -> Result { 256 | // only one ICA per controller allowed 257 | self.assert_no_host()?; 258 | 259 | // if a salt is not provided, by default use: 260 | // sha256(channel_id_bytes | controller_addr_bytes) 261 | let salt = salt.unwrap_or_else(|| self.default_salt()); 262 | 263 | // load the one-account contract's code ID and checksum, which is 264 | // used in Instantiate2 to determine the contract address 265 | let cfg = CONFIG.load(deps.storage)?; 266 | let code_res = deps.querier.query_wasm_code_info(cfg.default_account_code_id)?; 267 | 268 | // predict the contract address 269 | let addr_raw = instantiate2_address( 270 | &code_res.checksum, 271 | &deps.api.addr_canonicalize(env.contract.address.as_str())?, 272 | &salt, 273 | )?; 274 | let addr = deps.api.addr_humanize(&addr_raw)?; 275 | 276 | ACCOUNTS.save( 277 | deps.storage, 278 | (&self.endpoint.port_id, &self.endpoint.channel_id, &self.controller), 279 | &addr, 280 | )?; 281 | 282 | self.results.push(ActionResult::RegisterAccount { address: addr.to_string() }); 283 | self.host = Some(addr); 284 | 285 | Ok(response 286 | .add_attribute("action", "register_account") 287 | .add_submessage(SubMsg::reply_on_success( 288 | WasmMsg::Instantiate2 { 289 | code_id: cfg.default_account_code_id, 290 | msg: to_binary(&Empty {})?, 291 | funds: vec![], 292 | admin: Some(env.contract.address.into()), 293 | label: format!("one-account/{}/{}", self.endpoint.channel_id, self.controller), 294 | salt, 295 | }, 296 | AFTER_ACTION, 297 | ))) 298 | } 299 | 300 | fn handle_register_account_custom_factory( 301 | &self, 302 | response: Response, 303 | factory: String, 304 | data: Option, 305 | ) -> Result { 306 | // only one ICA per controller allowed 307 | self.assert_no_host()?; 308 | 309 | Ok(response 310 | .add_attribute("action", "register_account") 311 | .add_submessage(SubMsg::reply_on_success( 312 | WasmMsg::Execute { 313 | contract_addr: factory, 314 | msg: to_binary(&FactoryExecuteMsg::Ics999(FactoryMsg { 315 | endpoint: self.endpoint.clone(), 316 | controller: self.controller.clone(), 317 | data, 318 | }))?, 319 | funds: vec![], 320 | }, 321 | AFTER_ACTION, 322 | ))) 323 | } 324 | 325 | fn handle_query( 326 | &mut self, 327 | response: Response, 328 | deps: Deps, 329 | msg: Binary, 330 | ) -> Result { 331 | let addr = self.get_host()?; 332 | 333 | let query_req = to_binary(&QueryRequest::::Wasm(WasmQuery::Smart { 334 | contract_addr: addr.into(), 335 | msg, 336 | }))?; 337 | 338 | let query_res = deps 339 | .querier 340 | .raw_query(&query_req) 341 | .into_result()? 342 | .into_result() 343 | .map_err(Error::QueryContract)?; 344 | 345 | self.results.push(ActionResult::Query { response: query_res }); 346 | 347 | Ok(response.add_attribute("action", "query")) 348 | } 349 | 350 | fn handle_execute(&self, response: Response, msg: Binary) -> Result { 351 | let addr = self.get_host()?; 352 | 353 | Ok(response 354 | .add_attribute("action", "execute") 355 | .add_submessage(SubMsg::reply_on_success( 356 | WasmMsg::Execute { 357 | contract_addr: addr.into(), 358 | msg, 359 | funds: vec![], 360 | }, 361 | AFTER_ACTION, 362 | ))) 363 | } 364 | 365 | fn assert_no_host(&self) -> Result<()> { 366 | if self.host.is_some() { 367 | return Err(Error::AccountExists { 368 | endpoint: self.endpoint.clone(), 369 | controller: self.controller.clone(), 370 | })?; 371 | } 372 | 373 | Ok(()) 374 | } 375 | 376 | fn get_host(&self) -> Result<&Addr> { 377 | self.host.as_ref().ok_or_else(|| Error::AccountNotFound { 378 | endpoint: self.endpoint.clone(), 379 | controller: self.controller.clone(), 380 | }) 381 | } 382 | 383 | /// After an `Execute` action has been completed, parse the response 384 | pub fn after_action(&mut self, deps: DepsMut, data: Option) -> Result<()> { 385 | // the action that was executed 386 | let action = self.action.as_ref().expect("missing active action"); 387 | 388 | if let Action::Execute(_) = action { 389 | return self.after_execute(data); 390 | } 391 | 392 | if let Action::RegisterAccount(RegisterOptions::CustomFactory { .. }) = action { 393 | return self.after_register_account_custom_factory(deps, data); 394 | } 395 | 396 | Ok(()) 397 | } 398 | 399 | fn after_execute(&mut self, data: Option) -> Result<()> { 400 | // note that the contract being executed does not necessarily return 401 | // any data 402 | let data = data 403 | .map(|bin| parse_execute_response_data(&bin)) 404 | .transpose()? 405 | .and_then(|res| res.data); 406 | 407 | self.results.push(ActionResult::Execute { data }); 408 | 409 | Ok(()) 410 | } 411 | 412 | fn after_register_account_custom_factory( 413 | &mut self, 414 | deps: DepsMut, 415 | data: Option, 416 | ) -> Result<()> { 417 | let execute_res_bytes = data.ok_or(Error::FactoryResponseDataMissing)?; 418 | let execute_res = parse_execute_response_data(&execute_res_bytes)?; 419 | 420 | let factory_res_bytes = execute_res.data.ok_or(Error::FactoryResponseDataMissing)?; 421 | let factory_res: FactoryResponse = from_binary(&factory_res_bytes)?; 422 | 423 | let addr = deps.api.addr_validate(&factory_res.address)?; 424 | 425 | ACCOUNTS.save( 426 | deps.storage, 427 | (&self.endpoint.port_id, &self.endpoint.channel_id, &self.controller), 428 | &addr, 429 | )?; 430 | 431 | self.results.push(ActionResult::RegisterAccount { address: addr.to_string() }); 432 | self.host = Some(addr); 433 | 434 | Ok(()) 435 | } 436 | 437 | fn default_handle_action_response(&self) -> Response { 438 | Response::new() 439 | .add_attribute("method", "handle_next_action") 440 | .add_attribute("actions_left", self.pending_actions.len().to_string()) 441 | } 442 | 443 | /// Generate a salt to be used in Instantiate2, if the user does not provide one. 444 | /// 445 | /// The salt is sha256 hash of the connection ID and controller address. 446 | /// This entures: 447 | /// - unique for each {port_id, channel_id, controller} pair 448 | /// - not exceed the 64 byte max length 449 | fn default_salt(&self) -> Binary { 450 | let mut hasher = Sha256::new(); 451 | hasher.update(self.endpoint.port_id.as_bytes()); 452 | hasher.update(self.endpoint.channel_id.as_bytes()); 453 | hasher.update(self.controller.as_bytes()); 454 | hasher.finalize().to_vec().into() 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /contracts/core/src/host/mod.rs: -------------------------------------------------------------------------------- 1 | mod handler; 2 | 3 | use { 4 | self::handler::Handler, 5 | crate::{error::Result, msg::ExecuteMsg, AFTER_ALL_ACTIONS}, 6 | cosmwasm_std::{ 7 | from_slice, to_binary, DepsMut, Env, IbcEndpoint, IbcPacket, IbcReceiveResponse, Response, 8 | SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, 9 | }, 10 | cw_utils::parse_execute_response_data, 11 | ics999::{Action, PacketAck, PacketData, Trace}, 12 | }; 13 | 14 | pub fn packet_receive(env: Env, packet: IbcPacket) -> Result { 15 | // deserialize packet data 16 | let pd: PacketData = from_slice(&packet.data)?; 17 | 18 | // we don't add an ack in this response 19 | // the ack will be added in after_all_actions reply (see below) 20 | Ok(IbcReceiveResponse::new() 21 | .add_attribute("method", "packet_receive") 22 | .add_attribute("port_id", &packet.dest.port_id) 23 | .add_attribute("channel_id", &packet.dest.channel_id) 24 | .add_attribute("sequence", packet.sequence.to_string()) 25 | .add_submessage(SubMsg::reply_always( 26 | WasmMsg::Execute { 27 | contract_addr: env.contract.address.into(), 28 | msg: to_binary(&ExecuteMsg::Handle { 29 | counterparty_endpoint: packet.src, 30 | endpoint: packet.dest, 31 | controller: pd.controller, 32 | actions: pd.actions, 33 | traces: pd.traces, 34 | })?, 35 | funds: vec![], 36 | }, 37 | AFTER_ALL_ACTIONS, 38 | ))) 39 | } 40 | 41 | pub fn handle( 42 | deps: DepsMut, 43 | env: Env, 44 | src: IbcEndpoint, 45 | dest: IbcEndpoint, 46 | controller: String, 47 | actions: Vec, 48 | traces: Vec, 49 | ) -> Result { 50 | let handler = Handler::create(deps.storage, src, dest, controller, actions, traces)?; 51 | handler.handle_next_action(deps, env, None) 52 | } 53 | 54 | pub fn after_action(mut deps: DepsMut, env: Env, res: SubMsgResult) -> Result { 55 | let mut handler = Handler::load(deps.storage)?; 56 | handler.after_action(deps.branch(), res.unwrap().data)?; // reply on success so unwrap can't fail 57 | handler.handle_next_action(deps, env, None) 58 | } 59 | 60 | pub fn after_all_actions(res: SubMsgResult) -> Result { 61 | let ack = match &res { 62 | // all actions were successful - write an Success ack 63 | SubMsgResult::Ok(SubMsgResponse { 64 | data, 65 | .. 66 | }) => { 67 | let execute_res_bin = data.as_ref().expect("missing execute response data"); 68 | let execute_res = parse_execute_response_data(execute_res_bin)?; 69 | 70 | let action_res_bin = execute_res.data.expect("missing action results data"); 71 | let action_res = from_slice(&action_res_bin)?; 72 | 73 | PacketAck::Success(action_res) 74 | }, 75 | 76 | // one of actions failed - write an Error ack 77 | SubMsgResult::Err(err) => PacketAck::Failed(err.clone()), 78 | }; 79 | 80 | Ok(Response::new() 81 | .add_attribute("method", "after_actions") 82 | .add_attribute("success", res.is_ok().to_string()) 83 | // wasmd will interpret this data field as the ack, overriding the ack 84 | // emitted in the ibc_packet_receive entry point 85 | .set_data(to_binary(&ack)?)) 86 | } 87 | -------------------------------------------------------------------------------- /contracts/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "library"))] 2 | pub mod contract; 3 | pub mod controller; 4 | pub mod error; 5 | pub mod handshake; 6 | pub mod host; 7 | pub mod msg; 8 | pub mod query; 9 | pub mod state; 10 | pub mod transfer; 11 | pub mod utils; 12 | 13 | pub const CONTRACT_NAME: &str = "crates.io:one-core"; 14 | pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); 15 | 16 | // reply IDs 17 | const AFTER_ACTION: u64 = 1111; 18 | const AFTER_ALL_ACTIONS: u64 = 2222; 19 | const AFTER_CALLBACK: u64 = 3333; 20 | -------------------------------------------------------------------------------- /contracts/core/src/msg.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::transfer::TraceItem, 3 | cosmwasm_schema::{cw_serde, QueryResponses}, 4 | cosmwasm_std::{HexBinary, IbcEndpoint, IbcTimeout}, 5 | ics999::{Action, Trace}, 6 | }; 7 | 8 | #[cw_serde] 9 | pub struct Config { 10 | /// Code ID of the one-account contract 11 | pub default_account_code_id: u64, 12 | 13 | /// The default timeout (in seconds) if the user does not provide a timeout 14 | /// timestamp 15 | pub default_timeout_secs: u64, 16 | } 17 | 18 | #[cw_serde] 19 | pub enum ExecuteMsg { 20 | // ----------------------- USED ON CONTROLLER CHAIN ------------------------ 21 | 22 | /// Send a packet consisting of a series of actions 23 | Dispatch { 24 | connection_id: String, 25 | actions: Vec, 26 | timeout: Option, 27 | }, 28 | 29 | // ------------------------ USED ON THE HOST CHAIN ------------------------- 30 | 31 | /// Execute a series of actions received in a packet. 32 | /// 33 | /// Can only be invoked by the contract itself. 34 | /// 35 | /// NOTE: We have to create an execute method for this instead of handling 36 | /// the actions in the `ibc_packet_receive` entry point, because only this 37 | /// way we can achieve atomicity - one action fails means all actions fail, 38 | /// and no state changes from any action (even those that succeeded) will be 39 | /// committed. 40 | Handle { 41 | counterparty_endpoint: IbcEndpoint, 42 | endpoint: IbcEndpoint, 43 | controller: String, 44 | actions: Vec, 45 | traces: Vec, 46 | }, 47 | } 48 | 49 | #[cw_serde] 50 | #[derive(QueryResponses)] 51 | pub enum QueryMsg { 52 | /// Contract configuration 53 | #[returns(Config)] 54 | Config {}, 55 | 56 | /// Compute the denom hash of a given denom trace 57 | #[returns(DenomHashResponse)] 58 | DenomHash { 59 | trace: TraceItem, 60 | }, 61 | 62 | /// Query the denom trace associated with a given denom hash 63 | #[returns(Trace)] 64 | DenomTrace { 65 | denom: String, 66 | }, 67 | 68 | /// Iterate all known denom traces 69 | #[returns(Vec)] 70 | DenomTraces { 71 | start_after: Option, 72 | limit: Option, 73 | }, 74 | 75 | /// Interchain account controlled by a specific controller 76 | #[returns(AccountResponse)] 77 | Account(AccountKey), 78 | 79 | /// Iterate all interchain accounts 80 | #[returns(Vec)] 81 | Accounts { 82 | start_after: Option, 83 | limit: Option, 84 | }, 85 | 86 | /// Active channel associated with a connection 87 | #[returns(ActiveChannelResponse)] 88 | ActiveChannel { 89 | connection_id: String, 90 | }, 91 | 92 | /// Iterate active channels on all connections 93 | #[returns(Vec)] 94 | ActiveChannels { 95 | start_after: Option, 96 | limit: Option, 97 | }, 98 | } 99 | 100 | #[cw_serde] 101 | pub struct DenomHashResponse { 102 | pub hash: HexBinary, 103 | } 104 | 105 | #[cw_serde] 106 | pub struct AccountKey { 107 | pub src: IbcEndpoint, 108 | pub controller: String, 109 | } 110 | 111 | #[cw_serde] 112 | pub struct AccountResponse { 113 | pub src: IbcEndpoint, 114 | pub controller: String, 115 | pub address: String, 116 | } 117 | 118 | #[cw_serde] 119 | pub struct ActiveChannelResponse { 120 | pub connection_id: String, 121 | pub endpoint: IbcEndpoint, 122 | } 123 | -------------------------------------------------------------------------------- /contracts/core/src/query.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | msg::{AccountKey, AccountResponse, ActiveChannelResponse, Config, DenomHashResponse}, 4 | state::{ACCOUNTS, ACTIVE_CHANNELS, CONFIG, DENOM_TRACES}, 5 | transfer::TraceItem, 6 | }, 7 | cosmwasm_std::{Deps, IbcEndpoint, StdResult}, 8 | cw_paginate::paginate_map, 9 | cw_storage_plus::Bound, 10 | ics999::Trace, 11 | }; 12 | 13 | pub fn config(deps: Deps) -> StdResult { 14 | CONFIG.load(deps.storage) 15 | } 16 | 17 | pub fn denom_hash(trace: TraceItem) -> DenomHashResponse { 18 | DenomHashResponse { 19 | hash: trace.hash(), 20 | } 21 | } 22 | 23 | pub fn denom_trace(deps: Deps, denom: String) -> StdResult { 24 | let trace = DENOM_TRACES.load(deps.storage, &denom)?; 25 | Ok(Trace { 26 | denom, 27 | base_denom: trace.base_denom, 28 | path: trace.path, 29 | }) 30 | } 31 | 32 | pub fn denom_traces( 33 | deps: Deps, 34 | start_after: Option, 35 | limit: Option, 36 | ) -> StdResult> { 37 | let start = start_after.as_ref().map(|denom| Bound::exclusive(denom.as_str())); 38 | paginate_map(&DENOM_TRACES, deps.storage, start, limit, |denom, trace| { 39 | Ok(Trace { 40 | denom, 41 | base_denom: trace.base_denom, 42 | path: trace.path, 43 | }) 44 | }) 45 | } 46 | 47 | pub fn account( 48 | deps: Deps, 49 | src: IbcEndpoint, 50 | controller: String, 51 | ) -> StdResult { 52 | Ok(AccountResponse { 53 | address: ACCOUNTS.load(deps.storage, (&src.port_id, &src.channel_id, &controller))?.into(), 54 | src, 55 | controller, 56 | }) 57 | } 58 | 59 | pub fn accounts( 60 | deps: Deps, 61 | start_after: Option, 62 | limit: Option, 63 | ) -> StdResult> { 64 | let start = start_after 65 | .as_ref() 66 | .map(|AccountKey { src, controller }| { 67 | Bound::exclusive((src.port_id.as_str(), src.channel_id.as_str(), controller.as_str())) 68 | }); 69 | 70 | paginate_map(&ACCOUNTS, deps.storage, start, limit, |(port_id, channel_id, controller), address| { 71 | Ok(AccountResponse { 72 | address: address.into(), 73 | src: IbcEndpoint { port_id, channel_id }, 74 | controller, 75 | }) 76 | }) 77 | } 78 | 79 | pub fn active_channel(deps: Deps, connection_id: String) -> StdResult { 80 | Ok(ActiveChannelResponse { 81 | endpoint: ACTIVE_CHANNELS.load(deps.storage, &connection_id)?, 82 | connection_id, 83 | }) 84 | } 85 | 86 | pub fn active_channels( 87 | deps: Deps, 88 | start_after: Option, 89 | limit: Option, 90 | ) -> StdResult> { 91 | let start = start_after.as_ref().map(|cid| Bound::exclusive(cid.as_str())); 92 | paginate_map(&ACTIVE_CHANNELS, deps.storage, start, limit, |connection_id, endpoint| { 93 | Ok(ActiveChannelResponse { 94 | connection_id, 95 | endpoint, 96 | }) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /contracts/core/src/state.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{msg::Config, transfer::TraceItem}, 3 | cosmwasm_std::{Addr, IbcEndpoint}, 4 | cw_storage_plus::{Item, Map}, 5 | }; 6 | 7 | pub const CONFIG: Item = Item::new("cfg"); 8 | 9 | // (port_id, channel_id, controller_addr) => account_addr 10 | pub const ACCOUNTS: Map<(&str, &str, &str), Addr> = Map::new("acc"); 11 | 12 | // denom => denom_trace 13 | pub const DENOM_TRACES: Map<&str, TraceItem> = Map::new("dt"); 14 | 15 | // connection_id => ibc_endpoint 16 | pub const ACTIVE_CHANNELS: Map<&str, IbcEndpoint> = Map::new("actchan"); 17 | -------------------------------------------------------------------------------- /contracts/core/src/transfer/helpers.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::error::{Error, Result}, 3 | cosmwasm_std::{attr, Attribute, BankMsg, Coin, CosmosMsg, QuerierWrapper}, 4 | osmosis_std::types::{ 5 | cosmos::base::v1beta1::Coin as ProtoCoin, osmosis::tokenfactory::v1beta1 as tokenfactory, 6 | }, 7 | }; 8 | 9 | pub fn mint( 10 | sender: impl Into + Clone, 11 | to: impl Into, 12 | coin: Coin, 13 | msgs: &mut Vec, 14 | attrs: &mut Vec, 15 | ) { 16 | attrs.push(attr("coin", coin.to_string())); 17 | attrs.push(attr("action", "mint")); 18 | msgs.push( 19 | tokenfactory::MsgMint { 20 | sender: sender.clone().into(), 21 | mint_to_address: sender.into(), 22 | amount: Some(into_proto_coin(coin.clone())), 23 | } 24 | .into(), 25 | ); 26 | msgs.push( 27 | BankMsg::Send { 28 | to_address: to.into(), 29 | amount: vec![coin], 30 | } 31 | .into(), 32 | ); 33 | } 34 | 35 | pub fn burn( 36 | sender: impl Into + Clone, 37 | coin: Coin, 38 | msgs: &mut Vec, 39 | attrs: &mut Vec, 40 | ) { 41 | attrs.push(attr("coin", coin.to_string())); 42 | attrs.push(attr("action", "burn")); 43 | msgs.push( 44 | tokenfactory::MsgBurn { 45 | sender: sender.clone().into(), 46 | burn_from_address: sender.into(), 47 | amount: Some(into_proto_coin(coin)), 48 | } 49 | .into(), 50 | ); 51 | } 52 | 53 | pub fn release( 54 | coin: Coin, 55 | to: impl Into, 56 | msgs: &mut Vec, 57 | attrs: &mut Vec, 58 | ) { 59 | attrs.push(attr("coin", coin.to_string())); 60 | attrs.push(attr("action", "release")); 61 | msgs.push( 62 | BankMsg::Send { 63 | to_address: to.into(), 64 | amount: vec![coin], 65 | } 66 | .into(), 67 | ); 68 | } 69 | 70 | pub fn escrow(coin: &Coin, attrs: &mut Vec) { 71 | attrs.push(attr("coin", coin.to_string())); 72 | attrs.push(attr("action", "escrow")); 73 | } 74 | 75 | /// Combine a creator address and a subdenom into the tokenfactory full denom 76 | pub fn construct_denom(creator: &str, subdenom: &str) -> String { 77 | format!("factory/{creator}/{subdenom}") 78 | } 79 | 80 | /// Convert a cosmwasm_std::Coin into a /cosmos.base.v1beta1.coin 81 | pub fn into_proto_coin(coin: Coin) -> ProtoCoin { 82 | ProtoCoin { 83 | denom: coin.denom, 84 | amount: coin.amount.to_string(), 85 | } 86 | } 87 | 88 | /// Assert that denom creation fee is zero. 89 | /// 90 | /// We don't have the money to pay the fee. If the fee is non-zero then we 91 | /// simply refuse to complete the transfer. 92 | pub fn assert_free_denom_creation(querier: &QuerierWrapper) -> Result<()> { 93 | let fee = tokenfactory::TokenfactoryQuerier::new(querier) 94 | .params()? 95 | .params 96 | .expect("params response does not contain params") 97 | .denom_creation_fee; 98 | 99 | if !fee.is_empty() { 100 | return Err(Error::NonZeroTokenCreationFee); 101 | } 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /contracts/core/src/transfer/mod.rs: -------------------------------------------------------------------------------- 1 | mod helpers; 2 | mod trace; 3 | 4 | pub use {helpers::*, trace::TraceItem}; 5 | -------------------------------------------------------------------------------- /contracts/core/src/transfer/trace.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::cw_serde, 3 | cosmwasm_std::{HexBinary, IbcEndpoint}, 4 | ics999::Trace, 5 | ripemd::{Digest, Ripemd160}, 6 | }; 7 | 8 | /// Similar to one_types::Trace ("full trace"), but without the `denom` field 9 | /// (which will be used as the key in contract storage). Also implements some 10 | /// helper methods. 11 | #[cw_serde] 12 | pub struct TraceItem { 13 | pub base_denom: String, 14 | pub path: Vec, 15 | } 16 | 17 | impl From<&Trace> for TraceItem { 18 | fn from(trace: &Trace) -> Self { 19 | Self { 20 | base_denom: trace.base_denom.clone(), 21 | path: trace.path.clone(), 22 | } 23 | } 24 | } 25 | 26 | impl TraceItem { 27 | /// Create a new trace item with an empty path 28 | pub fn new(base_denom: &str) -> Self { 29 | Self { 30 | base_denom: base_denom.to_owned(), 31 | path: vec![], 32 | } 33 | } 34 | 35 | /// Combine the trace item with the denom on the current chain to form the 36 | /// full trace. 37 | pub fn into_full_trace(self, denom: &str) -> Trace { 38 | Trace { 39 | denom: denom.to_owned(), 40 | base_denom: self.base_denom, 41 | path: self.path, 42 | } 43 | } 44 | 45 | /// Hash the trace. The resulting hash is used as the subdenom of the 46 | /// voucher token. 47 | /// 48 | /// We use RIPEMD-160 instead of SHA-256 because with the latter, the token 49 | /// factory denom will be longer than cosmos-sdk's max allowed denom length. 50 | /// - max length: 128 characters 51 | /// - with SHA-256: 137 chars 52 | /// - with RIPEMD-160: 113 chars 53 | pub fn hash(&self) -> HexBinary { 54 | let mut hasher = Ripemd160::new(); 55 | hasher.update(self.base_denom.as_bytes()); 56 | for step in &self.path { 57 | hasher.update(step.port_id.as_bytes()); 58 | hasher.update(step.channel_id.as_bytes()); 59 | } 60 | hasher.finalize().to_vec().into() 61 | } 62 | 63 | /// The reverse of receiver_chain_is_source. 64 | /// 65 | /// We make this function public instead because it's easier to wrap head 66 | /// around (ibc-go does the same). 67 | /// 68 | /// NOTE: Regardless of where is function is run -- the sender chain or the 69 | /// receiver chain -- it always takes the sender chain IBC endpoint (`src`)! 70 | pub fn sender_is_source(&self, src: &IbcEndpoint) -> bool { 71 | !self.receiver_is_source(src) 72 | } 73 | 74 | /// Given the sender endpoint of a packet, return: 75 | /// - true, if the denom originally came from the receiving chain, or 76 | /// - false, if otherwise. 77 | /// 78 | /// The receiver is the source, if the path is not empty, and the channel 79 | /// connecting the receiving chain is the very last step in the path. 80 | fn receiver_is_source(&self, src: &IbcEndpoint) -> bool { 81 | let Some(last_step) = self.path.last() else { 82 | return false; 83 | }; 84 | 85 | src == last_step 86 | } 87 | } 88 | 89 | // ----------------------------------- Tests ----------------------------------- 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::*; 94 | 95 | #[test] 96 | fn deriving_hash() { 97 | let trace = TraceItem { 98 | base_denom: "ujuno".into(), 99 | path: vec![ 100 | IbcEndpoint { 101 | port_id: "transfer".into(), 102 | channel_id: "channel-0".into(), 103 | }, 104 | IbcEndpoint { 105 | port_id: "ics999".into(), 106 | channel_id: "channel-12345".into(), 107 | }, 108 | ], 109 | }; 110 | assert_eq!(trace.hash().to_hex(), "88a388f8b33bf58238ed9600360c471707db9eab"); 111 | } 112 | 113 | #[test] 114 | fn determining_source() { 115 | let mock_src = IbcEndpoint { 116 | port_id: "ics999".into(), 117 | channel_id: "channel_0".into(), 118 | }; 119 | 120 | // if path is empty, then sender is source 121 | { 122 | let trace = TraceItem { 123 | base_denom: "uatom".into(), 124 | path: vec![], 125 | }; 126 | assert!(trace.sender_is_source(&mock_src)); 127 | } 128 | 129 | // if path is not empty, but the very last step is not the receiver 130 | // chain, then sender is source 131 | { 132 | let trace = TraceItem { 133 | base_denom: "uatom".into(), 134 | path: vec![ 135 | IbcEndpoint { 136 | port_id: "test".into(), 137 | channel_id: "channel-1".into(), 138 | }, 139 | ], 140 | }; 141 | assert!(trace.sender_is_source(&mock_src)); 142 | } 143 | 144 | // if path is not empty, and the very last step is the receiver chain, 145 | // then receiver is the source 146 | { 147 | let trace = TraceItem { 148 | base_denom: "uatom".into(), 149 | path: vec![ 150 | IbcEndpoint { 151 | port_id: "test".into(), 152 | channel_id: "channel-1".into(), 153 | }, 154 | mock_src.clone(), 155 | ], 156 | }; 157 | assert!(trace.receiver_is_source(&mock_src)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /contracts/core/src/utils/coins.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_std::{Coin, OverflowError, Uint128}, 3 | std::{collections::BTreeMap, fmt}, 4 | }; 5 | 6 | // denom => amount 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub struct Coins(BTreeMap); 9 | 10 | // UNSAFE: because we don't check for duplicate denoms or zero amounts 11 | // only use this for trusted coin vecs, such as MessageInfo::funds 12 | impl From> for Coins { 13 | fn from(coin_vec: Vec) -> Self { 14 | Self(coin_vec 15 | .into_iter() 16 | .map(|coin| (coin.denom, coin.amount)) 17 | .collect()) 18 | } 19 | } 20 | 21 | // NOTE: the output vec is guaranteed to be ordered alphabetically ascendingly 22 | // by the denoms 23 | impl From for Vec { 24 | fn from(coins: Coins) -> Self { 25 | coins 26 | .0 27 | .into_iter() 28 | .map(|(denom, amount)| Coin { 29 | denom, 30 | amount, 31 | }) 32 | .collect() 33 | } 34 | } 35 | 36 | impl fmt::Display for Coins { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | if self.is_empty() { 39 | return write!(f, "[]"); 40 | } 41 | 42 | let s = self 43 | .0 44 | .iter() 45 | .map(|(denom, amount)| format!("{amount}{denom}")) 46 | .collect::>() 47 | .join(","); 48 | write!(f, "{s}") 49 | } 50 | } 51 | 52 | impl Coins { 53 | pub fn empty() -> Self { 54 | Self(BTreeMap::new()) 55 | } 56 | 57 | pub fn is_empty(&self) -> bool { 58 | self.0.is_empty() 59 | } 60 | 61 | pub fn add(&mut self, new_coin: Coin) -> Result<(), OverflowError> { 62 | let amount = self.0.entry(new_coin.denom).or_insert_with(Uint128::zero); 63 | *amount = amount.checked_add(new_coin.amount)?; 64 | Ok(()) 65 | } 66 | } 67 | 68 | // ----------------------------------- Tests ----------------------------------- 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use cosmwasm_std::coin; 73 | 74 | use super::*; 75 | 76 | #[test] 77 | fn adding() { 78 | let mut coins = Coins::empty(); 79 | 80 | coins.add(coin(12345, "umars")).unwrap(); 81 | coins.add(coin(23456, "uastro")).unwrap(); 82 | coins.add(coin(34567, "uosmo")).unwrap(); 83 | coins.add(coin(88888, "umars")).unwrap(); 84 | 85 | let vec: Vec = coins.into(); 86 | 87 | assert_eq!( 88 | vec, 89 | vec![coin(23456, "uastro"), coin(12345 + 88888, "umars"), coin(34567, "uosmo")], 90 | ); 91 | } 92 | 93 | #[test] 94 | fn comparing() { 95 | let coins1 = Coins::from(vec![ 96 | coin(23456, "uastro"), 97 | coin(88888, "umars"), 98 | coin(34567, "uosmo"), 99 | ]); 100 | 101 | let mut coins2 = coins1.clone(); 102 | assert_eq!(coins1, coins2); 103 | 104 | coins2.add(coin(1, "umars")).unwrap(); 105 | assert_ne!(coins1, coins2); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /contracts/core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod coins; 2 | 3 | pub use self::coins::Coins; 4 | -------------------------------------------------------------------------------- /contracts/mocks/account-factory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock-account-factory" 3 | description = "A mock contract that implements ICS-999's account factory interface, used for testing" 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # for more explicit tests, cargo test --features=backtraces 20 | backtraces = ["cosmwasm-std/backtraces"] 21 | 22 | [dependencies] 23 | cosmwasm-schema = { workspace = true } 24 | cosmwasm-std = { workspace = true } 25 | cw-storage-plus = { workspace = true } 26 | cw-utils = { workspace = true } 27 | ics999 = { path = "../../../packages/ics999" } 28 | thiserror = { workspace = true } 29 | -------------------------------------------------------------------------------- /contracts/mocks/account-factory/README.md: -------------------------------------------------------------------------------- 1 | # mock-account-factory 2 | 3 | This is a mock contract used for testing ICS-999's "custom factory" functionality, proposed by [this PR](https://github.com/larry0x/ics999/pull/5). 4 | 5 | It implements a whitelist: only a specific controller account from a specific IBC endpoint is allowed to register. 6 | 7 | ## License 8 | 9 | (c) larry0x, 2023 - [All rights reserved](../../../LICENSE). 10 | -------------------------------------------------------------------------------- /contracts/mocks/account-factory/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::cw_serde, 3 | cosmwasm_std::{ 4 | entry_point, from_binary, to_binary, Binary, Deps, DepsMut, Empty, Env, IbcEndpoint, 5 | MessageInfo, Reply, Response, StdError, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, 6 | }, 7 | cw_storage_plus::Item, 8 | cw_utils::{parse_instantiate_response_data, ParseReplyError}, 9 | ics999::{FactoryExecuteMsg, FactoryMsg, FactoryResponse}, 10 | }; 11 | 12 | pub const CONFIG: Item = Item::new("cfg"); 13 | 14 | const AFTER_INSTANTIATE: u64 = 1; 15 | 16 | #[cw_serde] 17 | pub struct Config { 18 | pub one_core: String, 19 | pub allowed_endpoint: IbcEndpoint, 20 | pub allowed_controller: String, 21 | } 22 | 23 | #[cw_serde] 24 | pub struct InstantiateData { 25 | pub code_id: u64, 26 | pub instantiate_msg: Binary, 27 | } 28 | 29 | #[derive(Debug, thiserror::Error)] 30 | pub enum Error { 31 | #[error(transparent)] 32 | Std(#[from] StdError), 33 | 34 | #[error(transparent)] 35 | ParseReply(#[from] ParseReplyError), 36 | 37 | #[error("sender is not ics999 core contract")] 38 | NotIcs999, 39 | 40 | #[error("not allowed to register from this connection")] 41 | NotAllowedSource, 42 | 43 | #[error("not allowed to register with this controller account")] 44 | NotAllowedController, 45 | 46 | #[error("instantiate data not provided")] 47 | MissingInstantiateData, 48 | 49 | #[error("failed to extract instantiate response data from reply")] 50 | MissingInstantiateResponse, 51 | } 52 | 53 | pub type Result = core::result::Result; 54 | 55 | #[entry_point] 56 | pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, cfg: Config) -> Result { 57 | CONFIG.save(deps.storage, &cfg)?; 58 | 59 | Ok(Response::new()) 60 | } 61 | 62 | #[entry_point] 63 | pub fn execute( 64 | deps: DepsMut, 65 | env: Env, 66 | info: MessageInfo, 67 | msg: FactoryExecuteMsg, 68 | ) -> Result { 69 | match msg { 70 | FactoryExecuteMsg::Ics999(FactoryMsg { endpoint, controller, data }) => { 71 | let cfg = CONFIG.load(deps.storage)?; 72 | 73 | if info.sender != cfg.one_core { 74 | return Err(Error::NotIcs999); 75 | } 76 | 77 | if endpoint != cfg.allowed_endpoint { 78 | return Err(Error::NotAllowedSource); 79 | } 80 | 81 | if controller != cfg.allowed_controller { 82 | return Err(Error::NotAllowedController); 83 | } 84 | 85 | let Some(data_bytes) = data else { 86 | return Err(Error::MissingInstantiateData); 87 | }; 88 | 89 | let InstantiateData { code_id, instantiate_msg } = from_binary(&data_bytes)?; 90 | 91 | Ok(Response::new().add_submessage(SubMsg::reply_on_success( 92 | WasmMsg::Instantiate { 93 | code_id, 94 | msg: instantiate_msg, 95 | funds: vec![], 96 | admin: Some(env.contract.address.into()), 97 | label: "mock-label".into(), 98 | }, 99 | AFTER_INSTANTIATE, 100 | ))) 101 | } 102 | } 103 | } 104 | 105 | #[entry_point] 106 | pub fn reply(_: DepsMut, _: Env, reply: Reply) -> Result { 107 | match reply.id { 108 | AFTER_INSTANTIATE => { 109 | let SubMsgResult::Ok(SubMsgResponse { data: Some(instantiate_res_bytes), .. }) = reply.result else { 110 | return Err(Error::MissingInstantiateResponse); 111 | }; 112 | 113 | let instantiate_res = parse_instantiate_response_data(&instantiate_res_bytes)?; 114 | 115 | let data = to_binary(&FactoryResponse { 116 | address: instantiate_res.contract_address, 117 | })?; 118 | 119 | Ok(Response::new().set_data(data)) 120 | }, 121 | id => unreachable!("unexpected reply id: {id}"), 122 | } 123 | } 124 | 125 | #[entry_point] 126 | pub fn query(_: Deps, _: Env, _: Empty) -> Result { 127 | unreachable!("this contract does not implement any query method"); 128 | } 129 | -------------------------------------------------------------------------------- /contracts/mocks/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock-counter" 3 | description = "A contract that maintains a single integer which can be incremented on demand. Used for testing purpose." 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # for more explicit tests, cargo test --features=backtraces 20 | backtraces = ["cosmwasm-std/backtraces"] 21 | 22 | [dependencies] 23 | cosmwasm-schema = { workspace = true } 24 | cosmwasm-std = { workspace = true } 25 | cw-storage-plus = { workspace = true } 26 | thiserror = { workspace = true } 27 | -------------------------------------------------------------------------------- /contracts/mocks/counter/README.md: -------------------------------------------------------------------------------- 1 | # mock-counter 2 | 3 | A contract that maintains a single integer which can be incremented on demand. Used for testing purpose. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../../LICENSE). 8 | -------------------------------------------------------------------------------- /contracts/mocks/counter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::{cw_serde, QueryResponses}, 3 | cosmwasm_std::{ 4 | entry_point, to_binary, Binary, Coin, Deps, DepsMut, Empty, Env, MessageInfo, Response, 5 | StdError, StdResult, 6 | }, 7 | cw_storage_plus::Item, 8 | }; 9 | 10 | pub const NUMBER: Item = Item::new("number"); 11 | 12 | #[cw_serde] 13 | pub enum ExecuteMsg { 14 | /// Increment the number by 1 15 | Increment {}, 16 | 17 | /// Attempt to increment the number by 1, but intentionally fail by the end. 18 | /// 19 | /// Used to test that state changes effected by failed submessages will not 20 | /// be committed. 21 | IncrementButFail {}, 22 | } 23 | 24 | #[cw_serde] 25 | pub struct IncrementResult { 26 | pub new_number: u64, 27 | } 28 | 29 | #[cw_serde] 30 | #[derive(QueryResponses)] 31 | pub enum QueryMsg { 32 | /// Query the current number stored in the contract 33 | #[returns(NumberResponse)] 34 | Number {}, 35 | } 36 | 37 | #[cw_serde] 38 | pub struct NumberResponse { 39 | pub number: u64, 40 | } 41 | 42 | #[entry_point] 43 | pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, _: Empty) -> StdResult { 44 | NUMBER.save(deps.storage, &0)?; 45 | 46 | Ok(Response::new()) 47 | } 48 | 49 | #[entry_point] 50 | pub fn execute(deps: DepsMut, _: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { 51 | match msg { 52 | ExecuteMsg::Increment {} => { 53 | let new_number = NUMBER.update(deps.storage, |number| -> StdResult<_> { 54 | Ok(number + 1) 55 | })?; 56 | 57 | let data = to_binary(&IncrementResult { 58 | new_number, 59 | })?; 60 | 61 | Ok(Response::new() 62 | .set_data(data) 63 | .add_attribute("new_number", new_number.to_string()) 64 | .add_attribute("user", info.sender) 65 | .add_attribute("funds", stringify_funds(&info.funds))) 66 | }, 67 | ExecuteMsg::IncrementButFail {} => { 68 | // attempt to increment the number, but we throw an error later so 69 | // this should have no effect 70 | NUMBER.update(deps.storage, |number| -> StdResult<_> { 71 | Ok(number + 1) 72 | })?; 73 | 74 | Err(StdError::generic_err("intentional error instructed by user")) 75 | }, 76 | } 77 | } 78 | 79 | fn stringify_funds(funds: &[Coin]) -> String { 80 | if funds.is_empty() { 81 | return "[]".into(); 82 | } 83 | 84 | funds.iter().map(|coin| coin.to_string()).collect::>().join(",") 85 | } 86 | 87 | #[entry_point] 88 | pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { 89 | match msg { 90 | QueryMsg::Number {} => { 91 | let number = NUMBER.load(deps.storage)?; 92 | to_binary(&NumberResponse { 93 | number, 94 | }) 95 | }, 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /contracts/mocks/dex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock-dex" 3 | description = "A contract that receives token A and returns the same amount of token B, mimicking the behavior of a DEX. Used for testing purpose." 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # for more explicit tests, cargo test --features=backtraces 20 | backtraces = ["cosmwasm-std/backtraces"] 21 | 22 | [dependencies] 23 | cosmwasm-schema = { workspace = true } 24 | cosmwasm-std = { workspace = true } 25 | cw-storage-plus = { workspace = true } 26 | cw-utils = { workspace = true } 27 | thiserror = { workspace = true } 28 | -------------------------------------------------------------------------------- /contracts/mocks/dex/README.md: -------------------------------------------------------------------------------- 1 | # mock-dex 2 | 3 | A contract that receives token A and returns the same amount of token B, mimicking the behavior of a DEX. Used for testing purpose. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../../LICENSE). 8 | 9 | -------------------------------------------------------------------------------- /contracts/mocks/dex/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::{cw_serde, QueryResponses}, 3 | cosmwasm_std::{ 4 | coins, entry_point, to_binary, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, Response, 5 | StdError, StdResult, 6 | }, 7 | cw_storage_plus::Item, 8 | cw_utils::PaymentError, 9 | }; 10 | 11 | pub const CONFIG: Item = Item::new("config"); 12 | 13 | #[cw_serde] 14 | pub struct Config { 15 | denom_in: String, 16 | denom_out: String, 17 | } 18 | 19 | pub type InstantiateMsg = Config; 20 | 21 | #[cw_serde] 22 | pub enum ExecuteMsg { 23 | Swap {}, 24 | } 25 | 26 | #[cw_serde] 27 | #[derive(QueryResponses)] 28 | pub enum QueryMsg { 29 | #[returns(Config)] 30 | Config {}, 31 | } 32 | 33 | #[derive(Debug, thiserror::Error)] 34 | pub enum ContractError { 35 | #[error(transparent)] 36 | Std(#[from] StdError), 37 | 38 | #[error(transparent)] 39 | Payment(#[from] PaymentError), 40 | } 41 | 42 | #[entry_point] 43 | pub fn instantiate( 44 | deps: DepsMut, 45 | _: Env, 46 | _: MessageInfo, 47 | msg: InstantiateMsg, 48 | ) -> Result { 49 | CONFIG.save(deps.storage, &msg)?; 50 | 51 | Ok(Response::new()) 52 | } 53 | 54 | #[entry_point] 55 | pub fn execute( 56 | deps: DepsMut, 57 | _: Env, 58 | info: MessageInfo, 59 | msg: ExecuteMsg, 60 | ) -> Result { 61 | match msg { 62 | ExecuteMsg::Swap {} => { 63 | let cfg = CONFIG.load(deps.storage)?; 64 | let amount_in = cw_utils::must_pay(&info, &cfg.denom_in)?; 65 | 66 | Ok(Response::new().add_message(BankMsg::Send { 67 | to_address: info.sender.into(), 68 | // send back denom_out of the same amount 69 | amount: coins(amount_in.u128(), cfg.denom_out), 70 | })) 71 | }, 72 | } 73 | } 74 | 75 | #[entry_point] 76 | pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> StdResult { 77 | match msg { 78 | QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/mocks/sender/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock-sender" 3 | description = "A mockup contract to be used as the action sender in E2E tests." 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | crate-type = ["cdylib", "rlib"] 16 | doctest = false 17 | 18 | [features] 19 | # for more explicit tests, cargo test --features=backtraces 20 | backtraces = ["cosmwasm-std/backtraces"] 21 | 22 | [dependencies] 23 | cosmwasm-schema = { workspace = true } 24 | cosmwasm-std = { workspace = true } 25 | cw-paginate = { workspace = true } 26 | cw-storage-plus = { workspace = true } 27 | ics999 = { path = "../../../packages/ics999" } 28 | one-core = { path = "../../core", features = ["library"] } 29 | thiserror = { workspace = true } 30 | -------------------------------------------------------------------------------- /contracts/mocks/sender/README.md: -------------------------------------------------------------------------------- 1 | # mock-sender 2 | 3 | A mockup contract to be used as the action sender in E2E tests. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../../LICENSE). 8 | 9 | -------------------------------------------------------------------------------- /contracts/mocks/sender/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::{cw_serde, QueryResponses}, 3 | cosmwasm_std::{ 4 | coin, entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env, IbcEndpoint, MessageInfo, 5 | OverflowError, Response, StdResult, WasmMsg, 6 | }, 7 | cw_paginate::paginate_map, 8 | cw_storage_plus::{Bound, Item, Map}, 9 | ics999::{Action, CallbackMsg, PacketOutcome}, 10 | one_core::utils::Coins, 11 | }; 12 | 13 | pub const ONE_CORE: Item = Item::new("one_core"); 14 | 15 | // we save the outcome of the packet in contract store during callbacks 16 | // we then verify the outcomes are correct 17 | // 18 | // (port_id, channel_id, sequence) => outcome 19 | pub const OUTCOMES: Map<(&str, &str, u64), PacketOutcome> = Map::new("outcomes"); 20 | 21 | #[cw_serde] 22 | pub struct InstantiateMsg { 23 | /// Address of the one-core contract 24 | one_core: String, 25 | } 26 | 27 | #[cw_serde] 28 | pub enum ExecuteMsg { 29 | /// Send some actions to a remote chain via one-core 30 | Send { 31 | connection_id: String, 32 | actions: Vec, 33 | }, 34 | 35 | /// Respond to packet ack or timeout. Required by one-core. 36 | Ics999(CallbackMsg), 37 | } 38 | 39 | #[cw_serde] 40 | #[derive(QueryResponses)] 41 | pub enum QueryMsg { 42 | /// Query a single packet acknowledgement 43 | #[returns(OutcomeResponse)] 44 | Outcome(OutcomeKey), 45 | 46 | /// Iterate all stored packet acknowledgements 47 | #[returns(Vec)] 48 | Outcomes { 49 | start_after: Option, 50 | limit: Option, 51 | }, 52 | } 53 | 54 | #[cw_serde] 55 | pub struct OutcomeKey { 56 | pub dest: IbcEndpoint, 57 | pub sequence: u64, 58 | } 59 | 60 | #[cw_serde] 61 | pub struct OutcomeResponse { 62 | pub dest: IbcEndpoint, 63 | pub sequence: u64, 64 | pub outcome: PacketOutcome, 65 | } 66 | 67 | #[entry_point] 68 | pub fn instantiate( 69 | deps: DepsMut, 70 | _: Env, 71 | _: MessageInfo, 72 | msg: InstantiateMsg, 73 | ) -> StdResult { 74 | let one_core_addr = deps.api.addr_validate(&msg.one_core)?; 75 | ONE_CORE.save(deps.storage, &one_core_addr)?; 76 | 77 | Ok(Response::new()) 78 | } 79 | 80 | #[entry_point] 81 | pub fn execute(deps: DepsMut, _: Env, _: MessageInfo, msg: ExecuteMsg) -> StdResult { 82 | match msg { 83 | ExecuteMsg::Send { 84 | connection_id, 85 | actions, 86 | } => { 87 | let one_core_addr = ONE_CORE.load(deps.storage)?; 88 | 89 | // compute the total amount of coins to be sent to one-core 90 | // must equal to the sum of all the amounts in transfer actions 91 | let funds = actions.iter().try_fold( 92 | Coins::empty(), 93 | |mut funds, action| -> Result<_, OverflowError> { 94 | if let Action::Transfer { denom, amount, .. } = action { 95 | funds.add(coin(amount.u128(), denom))?; 96 | } 97 | Ok(funds) 98 | }, 99 | )?; 100 | 101 | Ok(Response::new() 102 | .add_attribute("method", "send") 103 | .add_attribute("connection_id", &connection_id) 104 | .add_attribute("num_actions", actions.len().to_string()) 105 | .add_message(WasmMsg::Execute { 106 | contract_addr: one_core_addr.into(), 107 | msg: to_binary(&one_core::msg::ExecuteMsg::Dispatch { 108 | connection_id, 109 | actions, 110 | timeout: None, // use the default timeout set by one-core 111 | })?, 112 | funds: funds.into(), 113 | })) 114 | }, 115 | 116 | ExecuteMsg::Ics999(CallbackMsg { 117 | endpoint, 118 | sequence, 119 | outcome, 120 | }) => { 121 | OUTCOMES.save(deps.storage, (&endpoint.port_id, &endpoint.channel_id, sequence), &outcome)?; 122 | 123 | Ok(Response::new() 124 | .add_attribute("method", "packet_callback") 125 | .add_attribute("port_id", endpoint.port_id) 126 | .add_attribute("channel_id", endpoint.channel_id) 127 | .add_attribute("sequence", sequence.to_string()) 128 | .add_attribute("outcome", outcome.ty())) 129 | }, 130 | } 131 | } 132 | 133 | #[entry_point] 134 | pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> StdResult { 135 | match msg { 136 | QueryMsg::Outcome(OutcomeKey { 137 | dest, 138 | sequence, 139 | }) => { 140 | let res = OutcomeResponse { 141 | outcome: OUTCOMES.load(deps.storage, (&dest.port_id, &dest.channel_id, sequence))?, 142 | dest, 143 | sequence, 144 | }; 145 | 146 | to_binary(&res) 147 | }, 148 | 149 | QueryMsg::Outcomes { 150 | start_after, 151 | limit, 152 | } => { 153 | let start = start_after 154 | .as_ref() 155 | .map(|OutcomeKey { dest, sequence }| { 156 | Bound::exclusive((dest.port_id.as_str(), dest.channel_id.as_str(), *sequence)) 157 | }); 158 | 159 | let res = paginate_map( 160 | &OUTCOMES, 161 | deps.storage, 162 | start, 163 | limit, 164 | |(port_id, channel_id, sequence), outcome| -> StdResult<_> { 165 | Ok(OutcomeResponse { 166 | dest: IbcEndpoint { port_id, channel_id }, 167 | sequence, 168 | outcome, 169 | }) 170 | }, 171 | )?; 172 | 173 | to_binary(&res) 174 | }, 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /docs/1-overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ICS-999 is an IBC application layer protocol with fungible token transfer, interchain account, and interchain query capabilities. 4 | 5 | That's a lot of jargons! Let's break them down. 6 | 7 | ## IBC architecture 8 | 9 | [IBC][ibc] (**I**nter-**B**lockchain **C**ommunication) is a general-purpose cross-chain messaging protocol developed in the [Cosmos ecosystem][cosmos]. 10 | 11 | IBC's architecture consists of two layers: 12 | 13 | - **transport layer** 14 | 15 | Handles the transportation of data packets between two blockchains. 16 | 17 | After a packet is emitted by the sender chain, an actor known as the **relayer** will post the packet on the receiver chain, along with a Merkle proof that proves the packet is authentic. The receiver chain will verify the proof, and then hands the packet over to the application layer to be processed. 18 | 19 | A key characteristic of the transport layer is that the data packets are in the form of raw binary (0s and 1s). The transport layer does not attempt to interpret the meaning of those bits; that is the job of the app layer. 20 | 21 | - **application layer** 22 | 23 | On receiving a verified packet from the transport layer, the app layer interprets the packet and processes it. 24 | 25 | There are many app layer protocols, each doing a different job. They can be implemented either as [Cosmos SDK][cosmos-sdk] modules or [CosmWasm][cosmwasm] contracts. The app layer identifies which protocol the packet is meant for and dispatches the packet to it. 26 | 27 | There are a lotta more technical details we need to know in order to fully understand how IBC works, such as light clients, connections, ports, channels, handshake, acknowledgement, timeout... We'll cover them in the following chapters of this guide series. 28 | 29 | ICS-999 specifically, is an app layer protocol. 30 | 31 | ## IBC app layer protocols 32 | 33 | There are 3 major IBC app layer protocols currently in use today, each with an ICS (**I**nter**c**hain **S**tandard) identifier: 34 | 35 | | protocol | what is does | 36 | | --------------- | ------------------------------------------------------------------------- | 37 | | [ICS-20][ics20] | send tokens across chains | 38 | | [ICS-27][ics27] | execute state-mutating actions on another chain, aka "interchain account" | 39 | | [ICS-31][ics31] | exeucte state non-mutating queries on another chain | 40 | 41 | ICS-999 is an _all-in-one protocol_ that has the capabilities of all of them, and therefore competes with them at the same time. 42 | 43 | Now, the obvious question is why building ICS-999 when there are already working protocols that do the same things? The answer is that those protocols are... simply not very good. I explained specifically why they aren't good in my [AwesomWasm 2023][awesomwasm] tech talk (**VIDEO LINK TBD**), but the chart below gives a brief summary of 8 things that ICS-20/27/31 do poorly but ICS-999 does well: 44 | 45 | | ICS-20/27/31 | ICS-999 | 46 | | ------------------------------------------------------- | ---------------------------------------------------- | 47 | | ❌ multiple packets needed to perform complex actions | ✅ a single packet | 48 | | ❌ impossible to enforce order between channels | ✅ actions within the single packet are ordered | 49 | | ❌ not atomic | ✅ atomic | 50 | | ❌ does not provides callback | ✅ provides callback | 51 | | ❌ only 1 token per packet | ✅ send as many tokens as you want in a single packet | 52 | | ❌ multiple channels between the same two chains allowed | ✅ only 1 channel allowed, no user confusion | 53 | | ❌ no CW bindings, protobuf difficult to work with | ✅ built entirely in CW, packet data in json | 54 | | ❌ ordered channel, easily drop dead | ✅ unordered channel that can never be closed | 55 | 56 | ## The mandate of ICS-999 57 | 58 | **ICS-999 aims to be the _only_ IBC app layer protocol you will ever need to develop interchain smart contract systems, with superior developer experience than the alternatives.** 59 | 60 | Let's continue in the following chapters to see how ICS-999 works, and how to integrate it into your project. 61 | 62 | [awesomwasm]: https://www.awesomwasm.com/ 63 | [cosmos]: https://cosmos.network/ 64 | [cosmos-sdk]: https://github.com/cosmos/cosmos-sdk 65 | [cosmwasm]: https://cosmwasm.com/ 66 | [ibc]: https://ibcprotocol.org/ 67 | [ics20]: https://github.com/cosmos/ibc/tree/main/spec/app/ics-020-fungible-token-transfer 68 | [ics27]: https://github.com/cosmos/ibc/tree/main/spec/app/ics-027-interchain-accounts 69 | [ics31]: https://github.com/cosmos/ibc/tree/main/spec/app/ics-031-crosschain-queries 70 | -------------------------------------------------------------------------------- /docs/ICS-999_awesomwasm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larry0x/ics999/d0c22cf083ad73827ac1a1b55efb62219761a6fc/docs/ICS-999_awesomwasm.pdf -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | Documentations for ICS-999 4 | 5 | ## Guides 6 | 7 | If you wish to understand how ICS-999 works, or create an interchain protocol leveraging ICS-999, we recommand reading the following guide articles one-by-one: 8 | 9 | | | Title | 10 | | --- | --------------------------- | 11 | | 1 | [Overview](./1-overview.md) | 12 | 13 | ## Audits 14 | 15 | | Auditor | Date | Link | 16 | | ------- | ---- | ---- | 17 | 18 | ## Videos 19 | 20 | | Title | Date | Link | 21 | | ----------------------------------- | -------------- | ----------------------------------------------------------------------------------------- | 22 | | Presentation at AwesomWasm 2023 | July 12, 2023 | [YouTube](https://www.youtube.com/@larryengineer268) • [slides](./ICS-999_awesomwasm.pdf) | 23 | | ICS-999 Demo #3 - Send and Delegate | April 6, 2023 | [YouTube](https://youtu.be/IEnezrkjSRU) | 24 | | ICS-999 demo #2 - Multisend | April 5, 2023 | [YouTube](https://youtu.be/iq_rQ-h7s48) | 25 | | ICS-999 demo #1 | March 23, 2023 | [YouTube](https://youtu.be/NTi5_ZwuJ-Q) | 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ics999 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/CosmWasm/token-factory v0.0.0-20230301150822-47dc2d5ae369 7 | github.com/CosmWasm/wasmd v0.32.1 8 | github.com/CosmWasm/wasmvm v1.2.4 9 | github.com/cosmos/cosmos-sdk v0.45.15 10 | github.com/cosmos/ibc-go/v4 v4.3.0 11 | github.com/golang/protobuf v1.5.3 12 | github.com/golangci/golangci-lint v1.53.3 13 | github.com/stretchr/testify v1.8.4 14 | golang.org/x/crypto v0.9.0 15 | ) 16 | 17 | require ( 18 | 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 19 | 4d63.com/gochecknoglobals v0.2.1 // indirect 20 | cosmossdk.io/api v0.2.6 // indirect 21 | cosmossdk.io/core v0.5.1 // indirect 22 | cosmossdk.io/depinject v1.0.0-alpha.3 // indirect 23 | filippo.io/edwards25519 v1.0.0-rc.1 // indirect 24 | github.com/4meepo/tagalign v1.2.2 // indirect 25 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect 26 | github.com/99designs/keyring v1.2.1 // indirect 27 | github.com/Abirdcfly/dupword v0.0.11 // indirect 28 | github.com/Antonboom/errname v0.1.10 // indirect 29 | github.com/Antonboom/nilnil v0.1.5 // indirect 30 | github.com/BurntSushi/toml v1.3.2 // indirect 31 | github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect 32 | github.com/DataDog/zstd v1.5.0 // indirect 33 | github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect 34 | github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect 35 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 36 | github.com/Masterminds/semver v1.5.0 // indirect 37 | github.com/OpenPeeDeeP/depguard/v2 v2.1.0 // indirect 38 | github.com/Workiva/go-datastructures v1.0.53 // indirect 39 | github.com/alexkohler/nakedret/v2 v2.0.2 // indirect 40 | github.com/alexkohler/prealloc v1.0.0 // indirect 41 | github.com/alingse/asasalint v0.0.11 // indirect 42 | github.com/armon/go-metrics v0.4.1 // indirect 43 | github.com/ashanbrown/forbidigo v1.5.3 // indirect 44 | github.com/ashanbrown/makezero v1.1.1 // indirect 45 | github.com/beorn7/perks v1.0.1 // indirect 46 | github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 // indirect 47 | github.com/bkielbasa/cyclop v1.2.1 // indirect 48 | github.com/blizzy78/varnamelen v0.8.0 // indirect 49 | github.com/bombsimon/wsl/v3 v3.4.0 // indirect 50 | github.com/breml/bidichk v0.2.4 // indirect 51 | github.com/breml/errchkjson v0.3.1 // indirect 52 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect 53 | github.com/butuzov/ireturn v0.2.0 // indirect 54 | github.com/butuzov/mirror v1.1.0 // indirect 55 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 56 | github.com/cespare/xxhash v1.1.0 // indirect 57 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 58 | github.com/charithe/durationcheck v0.0.10 // indirect 59 | github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect 60 | github.com/cockroachdb/errors v1.9.1 // indirect 61 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 62 | github.com/cockroachdb/pebble v0.0.0-20220817183557-09c6e030a677 // indirect 63 | github.com/cockroachdb/redact v1.1.3 // indirect 64 | github.com/coinbase/rosetta-sdk-go v0.7.9 // indirect 65 | github.com/cometbft/cometbft-db v0.7.0 // indirect 66 | github.com/confio/ics23/go v0.9.0 // indirect 67 | github.com/cosmos/btcutil v1.0.4 // indirect 68 | github.com/cosmos/cosmos-db v0.0.0-20221226095112-f3c38ecb5e32 // indirect 69 | github.com/cosmos/cosmos-proto v1.0.0-beta.2 // indirect 70 | github.com/cosmos/go-bip39 v1.0.0 // indirect 71 | github.com/cosmos/gogoproto v1.4.6 // indirect 72 | github.com/cosmos/gorocksdb v1.2.0 // indirect 73 | github.com/cosmos/iavl v0.19.5 // indirect 74 | github.com/cosmos/interchain-accounts v0.2.6 // indirect 75 | github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect 76 | github.com/creachadair/taskgroup v0.3.2 // indirect 77 | github.com/curioswitch/go-reassign v0.2.0 // indirect 78 | github.com/daixiang0/gci v0.10.1 // indirect 79 | github.com/danieljoos/wincred v1.1.2 // indirect 80 | github.com/davecgh/go-spew v1.1.1 // indirect 81 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 82 | github.com/denis-tingaikin/go-header v0.4.3 // indirect 83 | github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect 84 | github.com/dgraph-io/badger/v2 v2.2007.4 // indirect 85 | github.com/dgraph-io/badger/v3 v3.2103.2 // indirect 86 | github.com/dgraph-io/ristretto v0.1.0 // indirect 87 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 88 | github.com/docker/distribution v2.8.1+incompatible // indirect 89 | github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect 90 | github.com/dvsekhvalnov/jose2go v1.5.0 // indirect 91 | github.com/esimonov/ifshort v1.0.4 // indirect 92 | github.com/ettle/strcase v0.1.1 // indirect 93 | github.com/fatih/color v1.15.0 // indirect 94 | github.com/fatih/structtag v1.2.0 // indirect 95 | github.com/felixge/httpsnoop v1.0.1 // indirect 96 | github.com/firefart/nonamedreturns v1.0.4 // indirect 97 | github.com/fsnotify/fsnotify v1.6.0 // indirect 98 | github.com/fzipp/gocyclo v0.6.0 // indirect 99 | github.com/getsentry/sentry-go v0.17.0 // indirect 100 | github.com/go-critic/go-critic v0.8.1 // indirect 101 | github.com/go-kit/kit v0.12.0 // indirect 102 | github.com/go-kit/log v0.2.1 // indirect 103 | github.com/go-logfmt/logfmt v0.5.1 // indirect 104 | github.com/go-toolsmith/astcast v1.1.0 // indirect 105 | github.com/go-toolsmith/astcopy v1.1.0 // indirect 106 | github.com/go-toolsmith/astequal v1.1.0 // indirect 107 | github.com/go-toolsmith/astfmt v1.1.0 // indirect 108 | github.com/go-toolsmith/astp v1.1.0 // indirect 109 | github.com/go-toolsmith/strparse v1.1.0 // indirect 110 | github.com/go-toolsmith/typep v1.1.0 // indirect 111 | github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect 112 | github.com/gobwas/glob v0.2.3 // indirect 113 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 114 | github.com/gofrs/flock v0.8.1 // indirect 115 | github.com/gogo/gateway v1.1.0 // indirect 116 | github.com/gogo/protobuf v1.3.3 // indirect 117 | github.com/golang/glog v1.0.0 // indirect 118 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 119 | github.com/golang/snappy v0.0.4 // indirect 120 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect 121 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect 122 | github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect 123 | github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect 124 | github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect 125 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect 126 | github.com/golangci/misspell v0.4.0 // indirect 127 | github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect 128 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect 129 | github.com/google/btree v1.1.2 // indirect 130 | github.com/google/flatbuffers v1.12.1 // indirect 131 | github.com/google/go-cmp v0.5.9 // indirect 132 | github.com/google/gofuzz v1.2.0 // indirect 133 | github.com/google/orderedcode v0.0.1 // indirect 134 | github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect 135 | github.com/gorilla/handlers v1.5.1 // indirect 136 | github.com/gorilla/mux v1.8.0 // indirect 137 | github.com/gorilla/websocket v1.5.0 // indirect 138 | github.com/gostaticanalysis/analysisutil v0.7.1 // indirect 139 | github.com/gostaticanalysis/comment v1.4.2 // indirect 140 | github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect 141 | github.com/gostaticanalysis/nilerr v0.1.1 // indirect 142 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 143 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 144 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 145 | github.com/gtank/merlin v0.1.1 // indirect 146 | github.com/gtank/ristretto255 v0.1.2 // indirect 147 | github.com/hashicorp/errwrap v1.1.0 // indirect 148 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 149 | github.com/hashicorp/go-multierror v1.1.1 // indirect 150 | github.com/hashicorp/go-version v1.6.0 // indirect 151 | github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect 152 | github.com/hashicorp/hcl v1.0.0 // indirect 153 | github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect 154 | github.com/hexops/gotextdiff v1.0.3 // indirect 155 | github.com/improbable-eng/grpc-web v0.15.0 // indirect 156 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 157 | github.com/jgautheron/goconst v1.5.1 // indirect 158 | github.com/jingyugao/rowserrcheck v1.1.1 // indirect 159 | github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect 160 | github.com/jmhodges/levigo v1.0.0 // indirect 161 | github.com/julz/importas v0.1.0 // indirect 162 | github.com/kisielk/errcheck v1.6.3 // indirect 163 | github.com/kisielk/gotool v1.0.0 // indirect 164 | github.com/kkHAIKE/contextcheck v1.1.4 // indirect 165 | github.com/klauspost/compress v1.16.3 // indirect 166 | github.com/kr/pretty v0.3.1 // indirect 167 | github.com/kr/text v0.2.0 // indirect 168 | github.com/kulti/thelper v0.6.3 // indirect 169 | github.com/kunwardeep/paralleltest v1.0.7 // indirect 170 | github.com/kyoh86/exportloopref v0.1.11 // indirect 171 | github.com/ldez/gomoddirectives v0.2.3 // indirect 172 | github.com/ldez/tagliatelle v0.5.0 // indirect 173 | github.com/leonklingele/grouper v1.1.1 // indirect 174 | github.com/lib/pq v1.10.9 // indirect 175 | github.com/libp2p/go-buffer-pool v0.1.0 // indirect 176 | github.com/linxGnu/grocksdb v1.7.10 // indirect 177 | github.com/lufeee/execinquery v1.2.1 // indirect 178 | github.com/magiconair/properties v1.8.6 // indirect 179 | github.com/maratori/testableexamples v1.0.0 // indirect 180 | github.com/maratori/testpackage v1.1.1 // indirect 181 | github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect 182 | github.com/mattn/go-colorable v0.1.13 // indirect 183 | github.com/mattn/go-isatty v0.0.17 // indirect 184 | github.com/mattn/go-runewidth v0.0.9 // indirect 185 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 186 | github.com/mbilski/exhaustivestruct v1.2.0 // indirect 187 | github.com/mgechev/revive v1.3.2 // indirect 188 | github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect 189 | github.com/minio/highwayhash v1.0.2 // indirect 190 | github.com/mitchellh/go-homedir v1.1.0 // indirect 191 | github.com/mitchellh/mapstructure v1.5.0 // indirect 192 | github.com/moricho/tparallel v0.3.1 // indirect 193 | github.com/mtibben/percent v0.2.1 // indirect 194 | github.com/nakabonne/nestif v0.3.1 // indirect 195 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect 196 | github.com/nishanths/exhaustive v0.11.0 // indirect 197 | github.com/nishanths/predeclared v0.2.2 // indirect 198 | github.com/nunnatsa/ginkgolinter v0.12.1 // indirect 199 | github.com/olekukonko/tablewriter v0.0.5 // indirect 200 | github.com/opencontainers/go-digest v1.0.0 // indirect 201 | github.com/pelletier/go-toml v1.9.5 // indirect 202 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 203 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect 204 | github.com/pkg/errors v0.9.1 // indirect 205 | github.com/pmezard/go-difflib v1.0.0 // indirect 206 | github.com/polyfloyd/go-errorlint v1.4.2 // indirect 207 | github.com/prometheus/client_golang v1.15.0 // indirect 208 | github.com/prometheus/client_model v0.3.0 // indirect 209 | github.com/prometheus/common v0.42.0 // indirect 210 | github.com/prometheus/procfs v0.9.0 // indirect 211 | github.com/quasilyte/go-ruleguard v0.3.19 // indirect 212 | github.com/quasilyte/gogrep v0.5.0 // indirect 213 | github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect 214 | github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect 215 | github.com/rakyll/statik v0.1.7 // indirect 216 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 217 | github.com/regen-network/cosmos-proto v0.3.1 // indirect 218 | github.com/rogpeppe/go-internal v1.10.0 // indirect 219 | github.com/rs/cors v1.8.2 // indirect 220 | github.com/rs/zerolog v1.27.0 // indirect 221 | github.com/ryancurrah/gomodguard v1.3.0 // indirect 222 | github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect 223 | github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect 224 | github.com/sasha-s/go-deadlock v0.3.1 // indirect 225 | github.com/sashamelentyev/interfacebloat v1.1.0 // indirect 226 | github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect 227 | github.com/securego/gosec/v2 v2.16.0 // indirect 228 | github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect 229 | github.com/sirupsen/logrus v1.9.3 // indirect 230 | github.com/sivchari/containedctx v1.0.3 // indirect 231 | github.com/sivchari/nosnakecase v1.7.0 // indirect 232 | github.com/sivchari/tenv v1.7.1 // indirect 233 | github.com/sonatard/noctx v0.0.2 // indirect 234 | github.com/sourcegraph/go-diff v0.7.0 // indirect 235 | github.com/spf13/afero v1.9.2 // indirect 236 | github.com/spf13/cast v1.5.0 // indirect 237 | github.com/spf13/cobra v1.7.0 // indirect 238 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 239 | github.com/spf13/pflag v1.0.5 // indirect 240 | github.com/spf13/viper v1.14.0 // indirect 241 | github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect 242 | github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect 243 | github.com/stretchr/objx v0.5.0 // indirect 244 | github.com/subosito/gotenv v1.4.1 // indirect 245 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 246 | github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect 247 | github.com/tdakkota/asciicheck v0.2.0 // indirect 248 | github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect 249 | github.com/tendermint/go-amino v0.16.0 // indirect 250 | github.com/tendermint/tendermint v0.34.27 // indirect 251 | github.com/tendermint/tm-db v0.6.8-0.20220506192307-f628bb5dc95b // indirect 252 | github.com/tetafro/godot v1.4.11 // indirect 253 | github.com/tidwall/btree v1.5.0 // indirect 254 | github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect 255 | github.com/timonwong/loggercheck v0.9.4 // indirect 256 | github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect 257 | github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect 258 | github.com/ultraware/funlen v0.0.3 // indirect 259 | github.com/ultraware/whitespace v0.0.5 // indirect 260 | github.com/uudashr/gocognit v1.0.6 // indirect 261 | github.com/xen0n/gosmopolitan v1.2.1 // indirect 262 | github.com/yagipy/maintidx v1.0.0 // indirect 263 | github.com/yeya24/promlinter v0.2.0 // indirect 264 | github.com/ykadowak/zerologlint v0.1.2 // indirect 265 | github.com/zondax/hid v0.9.1 // indirect 266 | github.com/zondax/ledger-go v0.14.1 // indirect 267 | gitlab.com/bosi/decorder v0.2.3 // indirect 268 | go.etcd.io/bbolt v1.3.7 // indirect 269 | go.opencensus.io v0.23.0 // indirect 270 | go.tmz.dev/musttag v0.7.0 // indirect 271 | go.uber.org/atomic v1.10.0 // indirect 272 | go.uber.org/multierr v1.8.0 // indirect 273 | go.uber.org/zap v1.24.0 // indirect 274 | golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea // indirect 275 | golang.org/x/exp/typeparams v0.0.0-20230224173230-c95f2b4c22f2 // indirect 276 | golang.org/x/mod v0.10.0 // indirect 277 | golang.org/x/net v0.10.0 // indirect 278 | golang.org/x/sync v0.2.0 // indirect 279 | golang.org/x/sys v0.8.0 // indirect 280 | golang.org/x/term v0.8.0 // indirect 281 | golang.org/x/text v0.9.0 // indirect 282 | golang.org/x/tools v0.9.3 // indirect 283 | google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect 284 | google.golang.org/grpc v1.53.0 // indirect 285 | google.golang.org/protobuf v1.30.0 // indirect 286 | gopkg.in/ini.v1 v1.67.0 // indirect 287 | gopkg.in/yaml.v2 v2.4.0 // indirect 288 | gopkg.in/yaml.v3 v3.0.1 // indirect 289 | honnef.co/go/tools v0.4.3 // indirect 290 | mvdan.cc/gofumpt v0.5.0 // indirect 291 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect 292 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect 293 | mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect 294 | nhooyr.io/websocket v1.8.6 // indirect 295 | ) 296 | 297 | replace ( 298 | // use my fork of wasmd 299 | // 300 | // use the v0.32.x-tf branch, with the following changes: 301 | // - add tokenfactory module 302 | // - whitelist tokenfactory-related stargate queries 303 | // - increase default gas limit used in ibctesting (from 3M to 15M) 304 | // - add a raw query method to TestChain 305 | // 306 | // note that ics999 requires wasmd >=0.32 to work, specifically it needs this 307 | // PR to be included: 308 | // https://github.com/CosmWasm/wasmd/pull/1225 309 | github.com/CosmWasm/wasmd => github.com/larry0x/wasmd v0.32.1-tf 310 | 311 | // use cosmos flavored gogo/protobuf 312 | // https://github.com/cosmos/cosmos-sdk/issues/8469 313 | github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 314 | 315 | // use informal system fork of tendermint 316 | // See https://twitter.com/informalinc/status/1613580954383040512 317 | github.com/tendermint/tendermint => github.com/cometbft/cometbft v0.34.27 318 | ) 319 | -------------------------------------------------------------------------------- /go.work: -------------------------------------------------------------------------------- 1 | go 1.20 2 | 3 | use . 4 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | rust-check: 2 | cargo check --target wasm32-unknown-unknown 3 | 4 | rust-lint: 5 | cargo +nightly clippy --tests 6 | 7 | rust-test: 8 | cargo test 9 | 10 | go-lint: 11 | go run github.com/golangci/golangci-lint/cmd/golangci-lint run --timeout=10m 12 | 13 | go-test: 14 | go test ./... 15 | 16 | optimize: 17 | if [[ $(uname -m) =~ "arm64" ]]; then \ 18 | just optimize-arm; else \ 19 | just optimize-x86; fi 20 | 21 | optimize-arm: 22 | docker run --rm -v "$(pwd)":/code \ 23 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ 24 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 25 | --platform linux/arm64 \ 26 | cosmwasm/workspace-optimizer-arm64:0.13.0 27 | 28 | optimize-x86: 29 | docker run --rm -v "$(pwd)":/code \ 30 | --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ 31 | --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ 32 | --platform linux/amd64 \ 33 | cosmwasm/workspace-optimizer:0.13.0 34 | -------------------------------------------------------------------------------- /packages/ics999/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ics999" 3 | description = "Definition of ICS-999 packet data, acknowledgement, callback, and other types" 4 | version = { workspace = true } 5 | authors = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | homepage = { workspace = true } 9 | repository = { workspace = true } 10 | documentation = { workspace = true } 11 | keywords = { workspace = true } 12 | rust-version = { workspace = true } 13 | 14 | [lib] 15 | doctest = false 16 | 17 | [dependencies] 18 | cosmwasm-schema = { workspace = true } 19 | cosmwasm-std = { workspace = true } 20 | -------------------------------------------------------------------------------- /packages/ics999/README.md: -------------------------------------------------------------------------------- 1 | # ics999 2 | 3 | Definition of ICS-999 packet data, acknowledgement, callback, and other types. 4 | 5 | ## License 6 | 7 | (c) larry0x, 2023 - [All rights reserved](../../LICENSE). 8 | -------------------------------------------------------------------------------- /packages/ics999/src/lib.rs: -------------------------------------------------------------------------------- 1 | use { 2 | cosmwasm_schema::cw_serde, 3 | cosmwasm_std::{Binary, IbcEndpoint, IbcOrder, Uint128}, 4 | }; 5 | 6 | // ---------------------------------- channel ---------------------------------- 7 | 8 | /// Expected channel packet ordering rule 9 | pub const ORDER: IbcOrder = IbcOrder::Unordered; 10 | 11 | /// Expected channel version string 12 | pub const VERSION: &str = "ics999-1"; 13 | 14 | // ---------------------------------- packet ----------------------------------- 15 | 16 | /// ICS-999 packet data structure 17 | #[cw_serde] 18 | pub struct PacketData { 19 | /// The account who sends this packet 20 | pub controller: String, 21 | 22 | /// Actions to take. 23 | /// The actions will be executed in order and atomically. 24 | pub actions: Vec, 25 | 26 | /// Traces of each token that is being transferred. 27 | /// Receiver chain uses this to determine whether it's the sender or sink. 28 | /// Must include ALL tokens that are being transferred. 29 | pub traces: Vec, 30 | } 31 | 32 | #[cw_serde] 33 | pub enum Action { 34 | /// Send one or more tokens to a recipient 35 | Transfer { 36 | denom: String, 37 | amount: Uint128, 38 | /// If not provided, default to the ICA controlled by the sender 39 | recipient: Option, 40 | }, 41 | 42 | /// Register an interchain account. 43 | /// 44 | /// The user provides a `RegisterOptions` data indicating how the account is 45 | /// to be registered. It can be one of two ways: 46 | /// - use the default account contract 47 | /// - the user to provide a custom "factory" contract which performs the 48 | /// instantiation 49 | RegisterAccount(RegisterOptions), 50 | 51 | /// Call the ICA contract's execute entry point. 52 | /// 53 | /// The message is to be in raw binary format. The ICA contract is 54 | /// responsible for implementing logics to interpret and handle this message. 55 | Execute(Binary), 56 | 57 | /// Call the ICA contract's query entry point. 58 | /// 59 | /// The message is to be in raw binary format. The ICA contract is 60 | /// responsible for implementing logics to interpret and handle this message. 61 | Query(Binary), 62 | } 63 | 64 | #[cw_serde] 65 | pub enum RegisterOptions { 66 | /// Register the account with the default account contract. 67 | /// 68 | /// The only data that the user needs to provide is a salt (0 - 64 bytes) 69 | /// which is used in deriving the account address. 70 | Default { 71 | /// The interchain account's address is chosen deterministically using 72 | /// wasmd's Instantiate2 method. 73 | /// 74 | /// We need to prevent the attack where an attacker predicts the ICA's 75 | /// address ahead of time, and create an account with the same address. 76 | /// (this happened on Cosmos Hub which prevented Quicksilver from 77 | /// registering their ICA). 78 | /// 79 | /// To achieve this, we let the user pick the salt. If not given, use 80 | /// the controller address's UTF-8 bytes as the salt. 81 | salt: Option, 82 | }, 83 | 84 | /// If more sophisticated logics are needed for registering the account, the 85 | /// user may implement such logics as a "factory" contract. 86 | /// 87 | /// To register the account, the user provides the factory contract's 88 | /// address, and optional data to be provided to the factory. 89 | /// 90 | /// Ics999 contract will attempt to call the factory contract using the 91 | /// `FactoryExecuteMsg` defined below. 92 | CustomFactory { 93 | address: String, 94 | data: Option, 95 | }, 96 | } 97 | 98 | // ------------------------------------ ack ------------------------------------ 99 | 100 | /// ICS-999 packet acknowledgement 101 | /// 102 | /// Mostly based on the format recommended by ICS-4, but not exactly the same: 103 | /// https://github.com/cosmos/ibc/tree/main/spec/core/ics-004-channel-and-packet-semantics#acknowledgement-envelope 104 | #[cw_serde] 105 | pub enum PacketAck { 106 | /// All actions were executed successfully. In this case, we return the 107 | /// result of each action. 108 | Success(Vec), 109 | 110 | /// One of the actions failed to execute. In this case, the entire queue of 111 | /// actions is considered failed altogether. We inform the sender of the 112 | /// error message. 113 | /// 114 | /// NOTE: currently, wasmd redacts error messages due to concern of 115 | /// non-determinism: https://github.com/CosmWasm/wasmd/issues/759 116 | /// 117 | /// Therefore, although we return a String here, in reality it will only 118 | /// include the error code, not the message. It will look something like 119 | /// this: 120 | /// 121 | /// ```json 122 | /// {"failed":"codespace: wasm, code: 5"} 123 | /// ``` 124 | Failed(String), 125 | } 126 | 127 | #[cw_serde] 128 | pub enum ActionResult { 129 | /// Result of a successfully executed `transfer` action. 130 | Transfer { 131 | /// IBC denom of the coin that was transferred 132 | denom: String, 133 | 134 | /// Whether a new token was created using the tokenfactory module as the 135 | /// result of this transfer 136 | new_token: bool, 137 | 138 | /// The recipient address (in case the sender did not provide an address, 139 | /// they can get it here) 140 | recipient: String, 141 | }, 142 | 143 | /// Result of a successfully executed `register_account` action. 144 | RegisterAccount { 145 | /// The address of the account that was registered 146 | address: String, 147 | }, 148 | 149 | /// Result of a successfully executed `execute` action. 150 | Execute { 151 | /// The data returned by the ICA contract 152 | data: Option, 153 | }, 154 | 155 | /// Result of a successful query 156 | Query { 157 | /// The querying contract is responsible for decoding the response 158 | response: Binary, 159 | }, 160 | } 161 | 162 | // ----------------------------------- trace ----------------------------------- 163 | 164 | /// Trace includes the token's original denom and the path it had travelled to 165 | /// arrive at the current chain. 166 | /// 167 | /// It is used to derive the voucher denom in such a way that there's a unique 168 | /// voucher denom for each token and each path. 169 | #[cw_serde] 170 | pub struct Trace { 171 | /// The token's denom on the packet sender chain 172 | pub denom: String, 173 | 174 | /// The token's original denom 175 | pub base_denom: String, 176 | 177 | /// The path the token took to arrived to the current chain. 178 | /// 179 | /// At each stop, the chain is appended to the end of the array. For example, 180 | /// consider a token being transferred via this path: 181 | /// 182 | /// chainA --> chainB --> chainC 183 | /// 184 | /// - on chain B, the path is \[A\] 185 | /// - on chain C, the path is \[A, B\] 186 | /// 187 | /// Note, this is different from ICS-20, where the latest chain is prefixed 188 | /// (instead of appended) to the beginning of the trace. 189 | pub path: Vec, 190 | } 191 | 192 | // --------------------------- third party: factory ---------------------------- 193 | 194 | #[cw_serde] 195 | pub enum FactoryExecuteMsg { 196 | Ics999(FactoryMsg), 197 | } 198 | 199 | #[cw_serde] 200 | pub struct FactoryMsg { 201 | pub endpoint: IbcEndpoint, 202 | pub controller: String, 203 | pub data: Option, 204 | } 205 | 206 | #[cw_serde] 207 | pub struct FactoryResponse { 208 | pub address: String, 209 | } 210 | 211 | // -------------------------- third party: controller -------------------------- 212 | 213 | #[cw_serde] 214 | pub enum ControllerExecuteMsg { 215 | Ics999(CallbackMsg), 216 | } 217 | 218 | #[cw_serde] 219 | pub struct CallbackMsg { 220 | pub endpoint: IbcEndpoint, 221 | pub sequence: u64, 222 | pub outcome: PacketOutcome, 223 | } 224 | 225 | #[cw_serde] 226 | pub enum PacketOutcome { 227 | Success(Vec), 228 | Failed(String), 229 | Timeout {}, 230 | } 231 | 232 | impl From> for PacketOutcome { 233 | fn from(maybe_ack: Option) -> Self { 234 | match maybe_ack { 235 | Some(PacketAck::Success(results)) => PacketOutcome::Success(results), 236 | Some(PacketAck::Failed(error)) => PacketOutcome::Failed(error), 237 | None => PacketOutcome::Timeout {}, 238 | } 239 | } 240 | } 241 | 242 | impl PacketOutcome { 243 | pub fn ty(&self) -> &str { 244 | match self { 245 | PacketOutcome::Success(_) => "success", 246 | PacketOutcome::Failed(_) => "failed", 247 | PacketOutcome::Timeout {} => "timeout", 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://rust-lang.github.io/rustfmt 2 | format_code_in_doc_comments = true # nightly 3 | group_imports = "StdExternalCrate" # nightly 4 | imports_granularity = "One" # nightly 5 | match_block_trailing_comma = true 6 | max_width = 100 7 | use_small_heuristics = "off" 8 | -------------------------------------------------------------------------------- /tests/factory_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | wasmibctesting "github.com/CosmWasm/wasmd/x/wasm/ibctesting" 9 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 10 | 11 | "ics999/tests/types" 12 | ) 13 | 14 | type invalidData struct { 15 | Foo string `json:"foo"` 16 | Bar string `json:"bar"` 17 | } 18 | 19 | func (suite *testSuite) TestRegisterCustomFactory() { 20 | correctData, err := json.Marshal(&types.FactoryData{ 21 | CodeID: suite.chainB.accountCodeID, 22 | InstantiateMsg: []byte("{}"), 23 | }) 24 | require.NoError(suite.T(), err) 25 | 26 | incorrectData, err := json.Marshal(&invalidData{ 27 | Foo: "fuzz", 28 | Bar: "buzz", 29 | }) 30 | require.NoError(suite.T(), err) 31 | 32 | // instantiate factory contract on chainB, with a config such that only the 33 | // sender contract on chainA can register 34 | factoryStoreRes := suite.chainB.StoreCodeFile("../artifacts/mock_account_factory.wasm") 35 | factoryInstantiateMsg, err := json.Marshal(&types.FactoryConfig{ 36 | OneCore: suite.chainB.coreAddr.String(), 37 | AllowedEndpoint: wasmvmtypes.IBCEndpoint{ 38 | PortID: suite.pathAB.EndpointB.ChannelConfig.PortID, 39 | ChannelID: suite.pathAB.EndpointB.ChannelID, 40 | }, 41 | AllowedController: suite.chainA.senderAddr.String(), 42 | }) 43 | require.NoError(suite.T(), err) 44 | factoryAddr := suite.chainB.InstantiateContract(factoryStoreRes.CodeID, factoryInstantiateMsg) 45 | 46 | for _, tc := range []struct { 47 | desc string 48 | senderChain *testChain 49 | path *wasmibctesting.Path 50 | data []byte 51 | expSuccess bool 52 | }{ 53 | { 54 | desc: "disallowed source", 55 | senderChain: suite.chainC, 56 | path: reversePath(suite.pathBC), 57 | data: correctData, 58 | expSuccess: false, 59 | }, 60 | { 61 | desc: "empty instantiate data", 62 | senderChain: suite.chainA, 63 | path: suite.pathAB, 64 | data: nil, 65 | expSuccess: false, 66 | }, 67 | { 68 | desc: "invalid instantiate data", 69 | senderChain: suite.chainA, 70 | path: suite.pathAB, 71 | data: incorrectData, 72 | expSuccess: false, 73 | }, 74 | { 75 | desc: "allowed source and sender, correct instantiate data", 76 | senderChain: suite.chainA, 77 | path: suite.pathAB, 78 | data: correctData, 79 | expSuccess: true, 80 | }, 81 | } { 82 | _, ack, err := send(tc.senderChain, tc.path, []types.Action{ 83 | { 84 | RegisterAccount: &types.RegisterAccountAction{ 85 | CustomFactory: &types.RegisterAccountCustomFactory{ 86 | Address: factoryAddr.String(), 87 | Data: tc.data, 88 | }, 89 | }, 90 | }, 91 | }) 92 | require.NoError(suite.T(), err) 93 | 94 | if tc.expSuccess { 95 | requirePacketSuccess(suite.T(), ack) 96 | 97 | // check if an account has been registered, and its address matches that 98 | // returned in the packet ack 99 | accountAddr, err := queryAccount( 100 | suite.chainB, 101 | suite.pathAB.EndpointB.ChannelConfig.PortID, 102 | suite.pathAB.EndpointB.ChannelID, 103 | suite.chainA.senderAddr.String(), 104 | ) 105 | require.NoError(suite.T(), err) 106 | require.Equal(suite.T(), ack.Success[0].RegisterAccount.Address, accountAddr.String()) 107 | } else { 108 | requirePacketFailed(suite.T(), ack) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/ica_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | //lint:ignore SA1019 cosmos-sdk uses deprecated dependency, not my problem 7 | "github.com/golang/protobuf/proto" 8 | "github.com/stretchr/testify/require" 9 | 10 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 11 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 12 | 13 | "ics999/tests/types" 14 | ) 15 | 16 | // TestRegisterAccount in this test, we do a single action which is to register 17 | // an account. We verify the account contract is instantiated with the correct 18 | // configuration. 19 | func (suite *testSuite) TestRegisterAccount() { 20 | // invoke ExecuteMsg::Act on chainA with a single action - RegisterAccount 21 | _, ack1, err := send(suite.chainA, suite.pathAB, []types.Action{ 22 | { 23 | RegisterAccount: &types.RegisterAccountAction{ 24 | Default: &types.RegisterAccountDefault{}, 25 | }, 26 | }, 27 | }) 28 | require.NoError(suite.T(), err) 29 | requirePacketSuccess(suite.T(), ack1) 30 | 31 | // check if an account has been registered, and its address matches that 32 | // returned in the packet ack 33 | accountAddr, err := queryAccount( 34 | suite.chainB, 35 | suite.pathAB.EndpointB.ChannelConfig.PortID, 36 | suite.pathAB.EndpointB.ChannelID, 37 | suite.chainA.senderAddr.String(), 38 | ) 39 | require.NoError(suite.T(), err) 40 | require.Equal(suite.T(), ack1.Success[0].RegisterAccount.Address, accountAddr.String()) 41 | 42 | // query the account contract info 43 | accountInfo := suite.chainB.ContractInfo(accountAddr) 44 | require.Equal(suite.T(), suite.chainB.accountCodeID, accountInfo.CodeID) 45 | require.Equal(suite.T(), suite.chainB.coreAddr.String(), accountInfo.Admin) 46 | require.Equal( 47 | suite.T(), 48 | fmt.Sprintf("one-account/%s/%s", suite.pathAB.EndpointB.ChannelID, suite.chainA.senderAddr.String()), 49 | accountInfo.Label, 50 | ) 51 | 52 | // make sure the ICA contract's ownership is properly set 53 | requireOwnershipEqual( 54 | suite.T(), 55 | suite.chainB, 56 | accountAddr, 57 | types.Ownership{ 58 | Owner: suite.chainB.coreAddr.String(), 59 | PendingOwner: "", 60 | PendingExpiry: nil, 61 | }, 62 | ) 63 | 64 | // attempt to register account again, should fail 65 | _, ack2, err := send(suite.chainA, suite.pathAB, []types.Action{ 66 | { 67 | RegisterAccount: &types.RegisterAccountAction{ 68 | Default: &types.RegisterAccountDefault{}, 69 | }, 70 | }, 71 | }) 72 | require.NoError(suite.T(), err) 73 | requirePacketFailed(suite.T(), ack2) 74 | } 75 | 76 | // TestExecuteWasm in this test, we deploy the mock-counter contract and use the 77 | // interchain account to increment its number. 78 | func (suite *testSuite) TestExecuteWasm() { 79 | // test 1 - register account and increment counter once in a single packet 80 | _, ack1, err := send(suite.chainA, suite.pathAB, []types.Action{ 81 | { 82 | RegisterAccount: &types.RegisterAccountAction{ 83 | Default: &types.RegisterAccountDefault{}, 84 | }, 85 | }, 86 | { 87 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 88 | Wasm: &wasmvmtypes.WasmMsg{ 89 | Execute: &wasmvmtypes.ExecuteMsg{ 90 | ContractAddr: suite.chainB.counterAddr.String(), 91 | Msg: []byte(`{"increment":{}}`), 92 | Funds: wasmvmtypes.Coins{}, 93 | }, 94 | }, 95 | }), 96 | }, 97 | }) 98 | require.NoError(suite.T(), err) 99 | requirePacketSuccess(suite.T(), ack1) 100 | 101 | // check the ack includes the correct result 102 | res := wasmtypes.MsgExecuteContractResponse{} 103 | err = proto.Unmarshal(ack1.Success[1].Execute.Data, &res) 104 | require.NoError(suite.T(), err) 105 | require.Equal(suite.T(), []byte(`{"new_number":1}`), res.Data) 106 | 107 | // check if the number has been correctly incremented once 108 | requireNumberEqual(suite.T(), suite.chainB, 1) 109 | 110 | // test 2 - increment the number more times in a single packet 111 | _, ack2, err := send(suite.chainA, suite.pathAB, []types.Action{ 112 | { 113 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 114 | Wasm: &wasmvmtypes.WasmMsg{ 115 | Execute: &wasmvmtypes.ExecuteMsg{ 116 | ContractAddr: suite.chainB.counterAddr.String(), 117 | Msg: []byte(`{"increment":{}}`), 118 | Funds: wasmvmtypes.Coins{}, 119 | }, 120 | }, 121 | }), 122 | }, 123 | { 124 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 125 | Wasm: &wasmvmtypes.WasmMsg{ 126 | Execute: &wasmvmtypes.ExecuteMsg{ 127 | ContractAddr: suite.chainB.counterAddr.String(), 128 | Msg: []byte(`{"increment":{}}`), 129 | Funds: wasmvmtypes.Coins{}, 130 | }, 131 | }, 132 | }), 133 | }, 134 | { 135 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 136 | Wasm: &wasmvmtypes.WasmMsg{ 137 | Execute: &wasmvmtypes.ExecuteMsg{ 138 | ContractAddr: suite.chainB.counterAddr.String(), 139 | Msg: []byte(`{"increment":{}}`), 140 | Funds: wasmvmtypes.Coins{}, 141 | }, 142 | }, 143 | }), 144 | }, 145 | }) 146 | require.NoError(suite.T(), err) 147 | requirePacketSuccess(suite.T(), ack2) 148 | 149 | // check if the number has been correctly incremented two more times 150 | requireNumberEqual(suite.T(), suite.chainB, 4) 151 | } 152 | 153 | func (suite *testSuite) TestQuery() { 154 | // we query the number (both raw and smart), increase the counter once, then 155 | // query again 156 | // note: we require an ICA to be registered even for queries 157 | _, ack, err := send(suite.chainA, suite.pathAB, []types.Action{ 158 | { 159 | RegisterAccount: &types.RegisterAccountAction{ 160 | Default: &types.RegisterAccountDefault{}, 161 | }, 162 | }, 163 | { 164 | Query: mustMarshalJSON(suite.T(), &wasmvmtypes.QueryRequest{ 165 | Wasm: &wasmvmtypes.WasmQuery{ 166 | Raw: &wasmvmtypes.RawQuery{ 167 | ContractAddr: suite.chainB.counterAddr.String(), 168 | Key: []byte("number"), 169 | }, 170 | }, 171 | }), 172 | }, 173 | { 174 | Query: mustMarshalJSON(suite.T(), &wasmvmtypes.QueryRequest{ 175 | Wasm: &wasmvmtypes.WasmQuery{ 176 | Smart: &wasmvmtypes.SmartQuery{ 177 | ContractAddr: suite.chainB.counterAddr.String(), 178 | Msg: []byte(`{"number":{}}`), 179 | }, 180 | }, 181 | }), 182 | }, 183 | { 184 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 185 | Wasm: &wasmvmtypes.WasmMsg{ 186 | Execute: &wasmvmtypes.ExecuteMsg{ 187 | ContractAddr: suite.chainB.counterAddr.String(), 188 | Msg: []byte(`{"increment":{}}`), 189 | Funds: wasmvmtypes.Coins{}, 190 | }, 191 | }, 192 | }), 193 | }, 194 | { 195 | Query: mustMarshalJSON(suite.T(), &wasmvmtypes.QueryRequest{ 196 | Wasm: &wasmvmtypes.WasmQuery{ 197 | Raw: &wasmvmtypes.RawQuery{ 198 | ContractAddr: suite.chainB.counterAddr.String(), 199 | Key: []byte("number"), 200 | }, 201 | }, 202 | }), 203 | }, 204 | { 205 | Query: mustMarshalJSON(suite.T(), &wasmvmtypes.QueryRequest{ 206 | Wasm: &wasmvmtypes.WasmQuery{ 207 | Smart: &wasmvmtypes.SmartQuery{ 208 | ContractAddr: suite.chainB.counterAddr.String(), 209 | Msg: []byte(`{"number":{}}`), 210 | }, 211 | }, 212 | }), 213 | }, 214 | }) 215 | require.NoError(suite.T(), err) 216 | fmt.Println(string(mustMarshalJSON(suite.T(), ack))) 217 | require.Equal(suite.T(), []byte("0"), ack.Success[1].Query.Response) 218 | require.Equal(suite.T(), []byte(`{"number":0}`), ack.Success[2].Query.Response) 219 | require.Equal(suite.T(), []byte("1"), ack.Success[4].Query.Response) 220 | require.Equal(suite.T(), []byte(`{"number":1}`), ack.Success[5].Query.Response) 221 | } 222 | 223 | func (suite *testSuite) TestCallback() { 224 | // register an account, increment the counter, and query the number 225 | packet1, ack1, err := send(suite.chainA, suite.pathAB, []types.Action{ 226 | { 227 | RegisterAccount: &types.RegisterAccountAction{ 228 | Default: &types.RegisterAccountDefault{}, 229 | }, 230 | }, 231 | { 232 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 233 | Wasm: &wasmvmtypes.WasmMsg{ 234 | Execute: &wasmvmtypes.ExecuteMsg{ 235 | ContractAddr: suite.chainB.counterAddr.String(), 236 | Msg: []byte(`{"increment":{}}`), 237 | Funds: wasmvmtypes.Coins{}, 238 | }, 239 | }, 240 | }), 241 | }, 242 | { 243 | Query: mustMarshalJSON(suite.T(), &wasmvmtypes.QueryRequest{ 244 | Wasm: &wasmvmtypes.WasmQuery{ 245 | Smart: &wasmvmtypes.SmartQuery{ 246 | ContractAddr: suite.chainB.counterAddr.String(), 247 | Msg: []byte(`{"number":{}}`), 248 | }, 249 | }, 250 | }), 251 | }, 252 | }) 253 | require.NoError(suite.T(), err) 254 | requirePacketSuccess(suite.T(), ack1) 255 | 256 | // the mock-sender contract should have stored the packet outcome during the 257 | // callback. let's grab this outcome 258 | requireOutcomeSuccess(suite.T(), suite.chainA, packet1.SourcePort, packet1.SourceChannel, packet1.Sequence) 259 | 260 | // do the same thing but with an intentionally failed packet 261 | packet2, ack2, err := send(suite.chainA, suite.pathAB, []types.Action{ 262 | { 263 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 264 | Wasm: &wasmvmtypes.WasmMsg{ 265 | Execute: &wasmvmtypes.ExecuteMsg{ 266 | ContractAddr: suite.chainB.counterAddr.String(), 267 | Msg: []byte(`{"increment_but_fail":{}}`), 268 | Funds: wasmvmtypes.Coins{}, 269 | }, 270 | }, 271 | }), 272 | }, 273 | }) 274 | require.NoError(suite.T(), err) 275 | requirePacketFailed(suite.T(), ack2) 276 | 277 | // mock-sender should have recorded the correct packet outcome 278 | requireOutcomeFailed(suite.T(), suite.chainA, packet2.SourcePort, packet2.SourceChannel, packet2.Sequence) 279 | } 280 | -------------------------------------------------------------------------------- /tests/suite_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | "github.com/stretchr/testify/suite" 12 | 13 | //lint:ignore SA1019 yeah we use ripemd160 14 | "golang.org/x/crypto/ripemd160" 15 | 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" 18 | ibctesting "github.com/cosmos/ibc-go/v4/testing" 19 | 20 | tokenfactorytypes "github.com/CosmWasm/token-factory/x/tokenfactory/types" 21 | wasmibctesting "github.com/CosmWasm/wasmd/x/wasm/ibctesting" 22 | wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" 23 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 24 | 25 | "ics999/tests/types" 26 | ) 27 | 28 | type testSuite struct { 29 | suite.Suite 30 | 31 | coordinator *wasmibctesting.Coordinator 32 | 33 | chainA *testChain 34 | chainB *testChain 35 | chainC *testChain 36 | 37 | pathAB *wasmibctesting.Path 38 | pathBC *wasmibctesting.Path 39 | } 40 | 41 | func (suite *testSuite) SetupTest() { 42 | suite.coordinator = wasmibctesting.NewCoordinator(suite.T(), 3) 43 | 44 | suite.chainA = setupChain( 45 | suite.T(), 46 | suite.coordinator.GetChain(wasmibctesting.GetChainID(0)), 47 | sdk.NewCoin("uastro", sdk.NewInt(mockInitialBalance)), 48 | sdk.NewCoin("umars", sdk.NewInt(mockInitialBalance)), 49 | ) 50 | suite.chainB = setupChain(suite.T(), suite.coordinator.GetChain(wasmibctesting.GetChainID(1))) 51 | suite.chainC = setupChain(suite.T(), suite.coordinator.GetChain(wasmibctesting.GetChainID(2))) 52 | 53 | suite.pathAB = setupConnection(suite.coordinator, suite.chainA, suite.chainB) 54 | suite.pathBC = setupConnection(suite.coordinator, suite.chainB, suite.chainC) 55 | } 56 | 57 | type testChain struct { 58 | *wasmibctesting.TestChain 59 | 60 | coreAddr sdk.AccAddress 61 | senderAddr sdk.AccAddress 62 | counterAddr sdk.AccAddress 63 | 64 | accountCodeID uint64 65 | } 66 | 67 | func setupChain(t *testing.T, chain *wasmibctesting.TestChain, coins ...sdk.Coin) *testChain { 68 | // store contract codes 69 | // 70 | // NOTE: wasmd 0.30 uses the gas limit of 3,000,000 for simulation txs. 71 | // however, our StoreCode txs easily go over this limit. we had to manually 72 | // increase it. for tests to work. 73 | // this will no longer be a problem with wasmd 0.31, which uses 74 | // simtestutil.DefaultGenTxGas which is 10M. 75 | coreStoreRes := chain.StoreCodeFile("../artifacts/one_core.wasm") 76 | require.Equal(t, uint64(1), coreStoreRes.CodeID) 77 | accountStoreRes := chain.StoreCodeFile("../artifacts/one_account.wasm") 78 | require.Equal(t, uint64(2), accountStoreRes.CodeID) 79 | senderStoreRes := chain.StoreCodeFile("../artifacts/mock_sender.wasm") 80 | require.Equal(t, uint64(3), senderStoreRes.CodeID) 81 | counterStoreRes := chain.StoreCodeFile("../artifacts/mock_counter.wasm") 82 | require.Equal(t, uint64(4), counterStoreRes.CodeID) 83 | 84 | // instantiate one-core contract 85 | coreInstantiateMsg, err := json.Marshal(&types.CoreConfig{ 86 | DefaultAccountCodeID: accountStoreRes.CodeID, 87 | DefaultTimeoutSecs: 600, // 10 mins 88 | }) 89 | require.NoError(t, err) 90 | core := chain.InstantiateContract(coreStoreRes.CodeID, coreInstantiateMsg) 91 | 92 | // instantiate mock-sender contract 93 | senderInstantiateMsg, err := json.Marshal(&types.SenderInstantiateMsg{ 94 | OneCore: core.String(), 95 | }) 96 | require.NoError(t, err) 97 | sender := chain.InstantiateContract(senderStoreRes.CodeID, senderInstantiateMsg) 98 | 99 | // instantiate mock-counter contract 100 | counter := chain.InstantiateContract(counterStoreRes.CodeID, []byte("{}")) 101 | 102 | // mint coins to the sender contract 103 | mintCoinsToAccount(chain, sender, coins...) 104 | 105 | // important: set denom creation fee to zero (default is 10000000stake) 106 | chain.App.TokenFactoryKeeper.SetParams(chain.GetContext(), tokenfactorytypes.NewParams(sdk.NewCoins())) 107 | 108 | return &testChain{ 109 | TestChain: chain, 110 | coreAddr: core, 111 | senderAddr: sender, 112 | counterAddr: counter, 113 | accountCodeID: accountStoreRes.CodeID, 114 | } 115 | } 116 | 117 | func mintCoinsToAccount(chain *wasmibctesting.TestChain, recipient sdk.AccAddress, coins ...sdk.Coin) { 118 | // the bank keeper only supports minting coins to module accounts 119 | // 120 | // in order to mint coins to a base account, we need to mint to a random 121 | // module account first, then transfer that to the base account 122 | // 123 | // this module account must have authtypes.Minter permission in app.go 124 | randomModuleName := "mint" 125 | 126 | chain.App.BankKeeper.MintCoins(chain.GetContext(), randomModuleName, coins) 127 | chain.App.BankKeeper.SendCoinsFromModuleToAccount(chain.GetContext(), randomModuleName, recipient, coins) 128 | } 129 | 130 | func setupConnection(coordinator *wasmibctesting.Coordinator, chainA, chainB *testChain) *wasmibctesting.Path { 131 | path := wasmibctesting.NewPath(chainA.TestChain, chainB.TestChain) 132 | path.EndpointA.ChannelConfig = &ibctesting.ChannelConfig{ 133 | PortID: chainA.ContractInfo(chainA.coreAddr).IBCPortID, 134 | Order: types.Order, 135 | Version: types.Version, 136 | } 137 | path.EndpointB.ChannelConfig = &ibctesting.ChannelConfig{ 138 | PortID: chainB.ContractInfo(chainB.coreAddr).IBCPortID, 139 | Order: types.Order, 140 | Version: types.Version, 141 | } 142 | 143 | coordinator.SetupConnections(path) 144 | coordinator.CreateChannels(path) 145 | 146 | return path 147 | } 148 | 149 | // relaySinglePacket relays a single packet from EndpointA to EndpointB. 150 | // To relayer a packet from B to A, do: relaySinglePacket(reversePath(path)). 151 | // 152 | // We choose to write our own relaying instead of using coordinator.RelayAndAckPendingPackets 153 | // because we want to grab the original packet and ack and assert their contents 154 | // are correct 155 | func relaySinglePacket(path *wasmibctesting.Path) (*channeltypes.Packet, []byte, error) { 156 | // in this function, we relay from EndpointA --> EndpointB 157 | src := path.EndpointA 158 | dest := path.EndpointB 159 | 160 | if len(src.Chain.PendingSendPackets) < 1 { 161 | return nil, nil, errors.New("no packet to relay") 162 | } 163 | 164 | // grab the first pending packet 165 | packet := src.Chain.PendingSendPackets[0] 166 | src.Chain.PendingSendPackets = src.Chain.PendingSendPackets[1:] 167 | 168 | if err := dest.UpdateClient(); err != nil { 169 | return nil, nil, err 170 | } 171 | 172 | res, err := dest.RecvPacketWithResult(packet) 173 | if err != nil { 174 | return nil, nil, err 175 | } 176 | 177 | // print out the events for debugging purpose 178 | // TODO: delete this 179 | events := res.GetEvents() 180 | for _, event := range events { 181 | fmt.Println("event_type:", event.Type) 182 | for _, attr := range event.Attributes { 183 | fmt.Println(" - key:", string(attr.Key)) 184 | fmt.Println(" value:", string(attr.Value)) 185 | } 186 | } 187 | 188 | ack, err := ibctesting.ParseAckFromEvents(res.GetEvents()) 189 | if err != nil { 190 | return nil, nil, err 191 | } 192 | 193 | if err = src.AcknowledgePacket(packet, ack); err != nil { 194 | return nil, nil, err 195 | } 196 | 197 | return &packet, ack, err 198 | } 199 | 200 | // reversePath change the order of EndpointA and EndpointB in a path 201 | // 202 | //lint:ignore U1000 will be used later 203 | func reversePath(path *wasmibctesting.Path) *wasmibctesting.Path { 204 | return &wasmibctesting.Path{ 205 | EndpointA: path.EndpointB, 206 | EndpointB: path.EndpointA, 207 | } 208 | } 209 | 210 | func send(src *testChain, path *wasmibctesting.Path, actions []types.Action) (*channeltypes.Packet, *types.PacketAck, error) { 211 | // compose the executeMsg 212 | executeMsg, err := json.Marshal(types.SenderExecuteMsg{ 213 | Send: &types.Send{ 214 | ConnectionID: path.EndpointA.ConnectionID, 215 | Actions: actions, 216 | }, 217 | }) 218 | if err != nil { 219 | return nil, nil, err 220 | } 221 | 222 | // executes mock-sender contract on chainA 223 | if _, err = src.SendMsgs(&wasmtypes.MsgExecuteContract{ 224 | Sender: src.SenderAccount.GetAddress().String(), 225 | Contract: src.senderAddr.String(), 226 | Msg: executeMsg, 227 | Funds: []sdk.Coin{}, 228 | }); err != nil { 229 | return nil, nil, err 230 | } 231 | 232 | // relay the packet 233 | packet, ackBytes, err := relaySinglePacket(path) 234 | if err != nil { 235 | return nil, nil, err 236 | } 237 | 238 | ack := &types.PacketAck{} 239 | if err = json.Unmarshal(ackBytes, ack); err != nil { 240 | return nil, nil, err 241 | } 242 | 243 | return packet, ack, nil 244 | } 245 | 246 | func queryAccount(chain *testChain, portID, channelID, controller string) (sdk.AccAddress, error) { 247 | accountRes := types.AccountResponse{} 248 | if err := chain.SmartQuery( 249 | chain.coreAddr.String(), 250 | types.CoreQueryMsg{ 251 | Account: &types.AccountKey{ 252 | Src: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID}, 253 | Controller: controller, 254 | }, 255 | }, 256 | &accountRes, 257 | ); err != nil { 258 | return nil, err 259 | } 260 | 261 | accountAddr, err := sdk.AccAddressFromBech32(accountRes.Address) 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | return accountAddr, nil 267 | } 268 | 269 | func queryOutcome(chain *testChain, portID, channelID string, sequence uint64) (*types.PacketOutcome, error) { 270 | outcomeRes := types.OutcomeResponse{} 271 | if err := chain.SmartQuery( 272 | chain.senderAddr.String(), 273 | &types.SenderQueryMsg{ 274 | Outcome: &types.OutcomeKey{ 275 | Dest: wasmvmtypes.IBCEndpoint{PortID: portID, ChannelID: channelID}, 276 | Sequence: sequence, 277 | }, 278 | }, 279 | &outcomeRes, 280 | ); err != nil { 281 | return nil, err 282 | } 283 | 284 | return &outcomeRes.Outcome, nil 285 | } 286 | 287 | func deriveVoucherDenom(chain *testChain, testPaths []*wasmibctesting.Path, baseDenom string) string { 288 | // convert ibctesting.Endpoint to wasmvmtypes.IBCEndpoint 289 | path := []wasmvmtypes.IBCEndpoint{} 290 | for _, testPath := range testPaths { 291 | path = append(path, wasmvmtypes.IBCEndpoint{ 292 | PortID: testPath.EndpointB.ChannelConfig.PortID, 293 | ChannelID: testPath.EndpointB.ChannelID, 294 | }) 295 | } 296 | 297 | denomHash := denomHashFromTrace(types.Trace{ 298 | BaseDenom: baseDenom, 299 | Path: path, 300 | }) 301 | 302 | return fmt.Sprintf("factory/%s/%s", chain.coreAddr, denomHash) 303 | } 304 | 305 | func denomHashFromTrace(trace types.Trace) string { 306 | hasher := ripemd160.New() 307 | 308 | hasher.Write([]byte(trace.BaseDenom)) 309 | 310 | for _, step := range trace.Path { 311 | hasher.Write([]byte(step.PortID)) 312 | hasher.Write([]byte(step.ChannelID)) 313 | } 314 | 315 | return hex.EncodeToString(hasher.Sum(nil)) 316 | } 317 | 318 | func requirePacketSuccess(t *testing.T, ack *types.PacketAck) { 319 | require.NotEmpty(t, ack.Success) 320 | require.Empty(t, ack.Failed) 321 | } 322 | 323 | func requirePacketFailed(t *testing.T, ack *types.PacketAck) { 324 | require.Empty(t, ack.Success) 325 | require.NotEmpty(t, ack.Failed) 326 | } 327 | 328 | func requireBalanceEqual(t *testing.T, chain *testChain, addr sdk.AccAddress, denom string, expBalance int64) { 329 | balance := chain.Balance(addr, denom) 330 | require.Equal(t, sdk.NewInt(expBalance), balance.Amount) 331 | } 332 | 333 | func requireTraceEqual(t *testing.T, chain *testChain, denom string, expTrace types.Trace) { 334 | traceResp := types.DenomTraceResponse{} 335 | err := chain.SmartQuery( 336 | chain.coreAddr.String(), 337 | &types.CoreQueryMsg{ 338 | DenomTrace: &types.DenomTraceQuery{ 339 | Denom: denom, 340 | }, 341 | }, 342 | &traceResp, 343 | ) 344 | require.NoError(t, err) 345 | require.Equal(t, expTrace.Denom, traceResp.Denom) 346 | require.Equal(t, expTrace.BaseDenom, traceResp.BaseDenom) 347 | require.Equal(t, expTrace.Path, traceResp.Path) 348 | } 349 | 350 | func requireNumberEqual(t *testing.T, chain *testChain, expNumber uint64) { 351 | numberRes := types.NumberResponse{} 352 | err := chain.SmartQuery( 353 | chain.counterAddr.String(), 354 | &types.CounterQueryMsg{ 355 | Number: &types.NumberQuery{}, 356 | }, 357 | &numberRes, 358 | ) 359 | require.NoError(t, err) 360 | require.Equal(t, expNumber, numberRes.Number) 361 | } 362 | 363 | func requireOutcomeSuccess(t *testing.T, chain *testChain, portID, channelID string, sequence uint64) { 364 | outcome, err := queryOutcome(chain, portID, channelID, sequence) 365 | require.NoError(t, err) 366 | require.NotNil(t, outcome.Success) 367 | require.Empty(t, outcome.Failed) 368 | require.Nil(t, outcome.Timeout) 369 | } 370 | 371 | func requireOutcomeFailed(t *testing.T, chain *testChain, portID, channelID string, sequence uint64) { 372 | outcome, err := queryOutcome(chain, portID, channelID, sequence) 373 | require.NoError(t, err) 374 | require.Nil(t, outcome.Success) 375 | require.NotEmpty(t, outcome.Failed) 376 | require.Nil(t, outcome.Timeout) 377 | } 378 | 379 | func requireOwnershipEqual(t *testing.T, chain *testChain, contractAddr sdk.Address, expOwnership types.Ownership) { 380 | ownershipBin, err := chain.RawQuery(contractAddr.String(), []byte("ownership")) 381 | require.NoError(t, err) 382 | 383 | var ownership types.Ownership 384 | err = json.Unmarshal(ownershipBin, &ownership) 385 | require.NoError(t, err) 386 | 387 | require.Equal(t, expOwnership, ownership) 388 | } 389 | 390 | func mustMarshalJSON(t *testing.T, i interface{}) []byte { 391 | bz, err := json.Marshal(i) 392 | require.NoError(t, err) 393 | 394 | return bz 395 | } 396 | 397 | func Test(t *testing.T) { 398 | suite.Run(t, new(testSuite)) 399 | } 400 | -------------------------------------------------------------------------------- /tests/transfer_test.go: -------------------------------------------------------------------------------- 1 | package e2e_test 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | 10 | wasmibctesting "github.com/CosmWasm/wasmd/x/wasm/ibctesting" 11 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 12 | 13 | "ics999/tests/types" 14 | ) 15 | 16 | var ( 17 | mockRecipient, _ = sdk.AccAddressFromBech32("cosmos1z926ax906k0ycsuckele6x5hh66e2m4mjchwmp") 18 | mockInitialBalance int64 = 100_000_000 19 | ) 20 | 21 | // TestMultipleTransfers tests sending multiple coins to multiple recipients in 22 | // a single packet. 23 | func (suite *testSuite) TestMultipleTransfers() { 24 | // the first two transfers we specify a recipient 25 | // the other two we don't specify a recipient; should default to the ICA 26 | _, ack, err := send(suite.chainA, suite.pathAB, []types.Action{ 27 | { 28 | Transfer: &types.TransferAction{ 29 | Denom: "uastro", 30 | Amount: sdk.NewInt(888_888), 31 | Recipient: mockRecipient.String(), 32 | }, 33 | }, 34 | { 35 | Transfer: &types.TransferAction{ 36 | Denom: "umars", 37 | Amount: sdk.NewInt(69_420), 38 | Recipient: mockRecipient.String(), 39 | }, 40 | }, 41 | { 42 | RegisterAccount: &types.RegisterAccountAction{ 43 | Default: &types.RegisterAccountDefault{}, 44 | }, 45 | }, 46 | { 47 | Transfer: &types.TransferAction{ 48 | Denom: "uastro", 49 | Amount: sdk.NewInt(987_654), 50 | }, 51 | }, 52 | { 53 | Transfer: &types.TransferAction{ 54 | Denom: "umars", 55 | Amount: sdk.NewInt(1_111_111), 56 | }, 57 | }, 58 | }) 59 | require.NoError(suite.T(), err) 60 | requirePacketSuccess(suite.T(), ack) 61 | 62 | // predict what the denom would be 63 | astroVoucherDenom := deriveVoucherDenom(suite.chainB, []*wasmibctesting.Path{suite.pathAB}, "uastro") 64 | marsVoucherDenom := deriveVoucherDenom(suite.chainB, []*wasmibctesting.Path{suite.pathAB}, "umars") 65 | 66 | // recipient unspecified, default to the ICA 67 | icaAddr, err := sdk.AccAddressFromBech32(ack.Success[2].RegisterAccount.Address) 68 | require.NoError(suite.T(), err) 69 | 70 | // sender balance on chainA should have been reduced 71 | // recipient balance on chainB should have been increased 72 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, "uastro", mockInitialBalance-888_888-987_654) 73 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, "umars", mockInitialBalance-69_420-1_111_111) 74 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.coreAddr, "uastro", 888_888+987_654) 75 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.coreAddr, "umars", 69_420+1_111_111) 76 | requireBalanceEqual(suite.T(), suite.chainB, mockRecipient, astroVoucherDenom, 888_888) 77 | requireBalanceEqual(suite.T(), suite.chainB, mockRecipient, marsVoucherDenom, 69_420) 78 | requireBalanceEqual(suite.T(), suite.chainB, icaAddr, astroVoucherDenom, 987_654) 79 | requireBalanceEqual(suite.T(), suite.chainB, icaAddr, marsVoucherDenom, 1_111_111) 80 | } 81 | 82 | // TestSequentialTransfers in this test, to do the following transfer: 83 | // 84 | // chainA --> chainB --> chainC --> chainB 85 | // 86 | // The objective is to test in the last step, whether the voucher tokens are 87 | // properly burned and escrowed tokens released. 88 | func (suite *testSuite) TestSequentialTransfers() { 89 | var ( 90 | // astro token's denom on chainB 91 | astroB = deriveVoucherDenom(suite.chainB, []*wasmibctesting.Path{suite.pathAB}, "uastro") 92 | // astro token's denom on chainC 93 | astroC = deriveVoucherDenom(suite.chainC, []*wasmibctesting.Path{suite.pathAB, suite.pathBC}, "uastro") 94 | 95 | // how many astro to send chainA --> chainB 96 | amountAB int64 = 12345 97 | // how many astro to send chainB --> chainC 98 | amountBC int64 = 10000 99 | // how many astro to send chainC --> chainB 100 | amountCB int64 = 8964 101 | ) 102 | 103 | // chainA --> chainB 104 | _, ack, err := send(suite.chainA, suite.pathAB, []types.Action{ 105 | { 106 | Transfer: &types.TransferAction{ 107 | Denom: "uastro", 108 | Amount: sdk.NewInt(amountAB), 109 | Recipient: suite.chainB.senderAddr.String(), 110 | }, 111 | }, 112 | }) 113 | require.NoError(suite.T(), err) 114 | requirePacketSuccess(suite.T(), ack) 115 | 116 | // chainB --> chainC 117 | _, ack, err = send(suite.chainB, suite.pathBC, []types.Action{ 118 | { 119 | Transfer: &types.TransferAction{ 120 | Denom: astroB, 121 | Amount: sdk.NewInt(amountBC), 122 | Recipient: suite.chainC.senderAddr.String(), 123 | }, 124 | }, 125 | }) 126 | require.NoError(suite.T(), err) 127 | requirePacketSuccess(suite.T(), ack) 128 | 129 | // astro of amountAB should have been escrowed on chainA 130 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, "uastro", mockInitialBalance-amountAB) 131 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.coreAddr, "uastro", amountAB) 132 | // astroB of amountAB should have been minted on chainB 133 | // astroB of amountBC should have been escrowed on chainB 134 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainB.senderAddr, astroB, amountAB-amountBC) 135 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainB.coreAddr, astroB, amountBC) 136 | // astroC of amountBC should have been minted on chainC 137 | requireBalanceEqual(suite.T(), suite.chainC, suite.chainC.senderAddr, astroC, amountBC) 138 | requireBalanceEqual(suite.T(), suite.chainC, suite.chainC.coreAddr, astroC, 0) 139 | 140 | // verify denom traces 141 | requireTraceEqual(suite.T(), suite.chainB, astroB, types.Trace{ 142 | Denom: astroB, 143 | BaseDenom: "uastro", 144 | Path: []wasmvmtypes.IBCEndpoint{ 145 | { 146 | PortID: suite.pathAB.EndpointB.ChannelConfig.PortID, 147 | ChannelID: suite.pathAB.EndpointB.ChannelID, 148 | }, 149 | }, 150 | }) 151 | requireTraceEqual(suite.T(), suite.chainC, astroC, types.Trace{ 152 | Denom: astroC, 153 | BaseDenom: "uastro", 154 | Path: []wasmvmtypes.IBCEndpoint{ 155 | { 156 | PortID: suite.pathAB.EndpointB.ChannelConfig.PortID, 157 | ChannelID: suite.pathAB.EndpointB.ChannelID, 158 | }, 159 | { 160 | PortID: suite.pathBC.EndpointB.ChannelConfig.PortID, 161 | ChannelID: suite.pathBC.EndpointB.ChannelID, 162 | }, 163 | }, 164 | }) 165 | 166 | // chainC --> chainB 167 | _, ack, err = send(suite.chainC, reversePath(suite.pathBC), []types.Action{ 168 | { 169 | Transfer: &types.TransferAction{ 170 | Denom: astroC, 171 | Amount: sdk.NewInt(amountCB), 172 | Recipient: mockRecipient.String(), 173 | }, 174 | }, 175 | }) 176 | require.NoError(suite.T(), err) 177 | requirePacketSuccess(suite.T(), ack) 178 | 179 | // astroC of amountCB should have been burned on chainC 180 | requireBalanceEqual(suite.T(), suite.chainC, suite.chainC.senderAddr, astroC, amountBC-amountCB) 181 | requireBalanceEqual(suite.T(), suite.chainC, suite.chainC.coreAddr, astroC, 0) 182 | // astroB of amountCB should have been released from escrow 183 | requireBalanceEqual(suite.T(), suite.chainB, mockRecipient, astroB, amountCB) 184 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainB.coreAddr, astroB, amountBC-amountCB) 185 | } 186 | 187 | // TestRefund tests the funds escrowed on the sender chain is properly refunded 188 | // if the packet fails to execute. 189 | func (suite *testSuite) TestRefund() { 190 | // attempt to transfer tokens without specifying a recipient while not having 191 | // an ICA already registered. should fail 192 | _, ack, err := send(suite.chainA, suite.pathAB, []types.Action{ 193 | { 194 | Transfer: &types.TransferAction{ 195 | Denom: "uastro", 196 | Amount: sdk.NewInt(12345), 197 | }, 198 | }, 199 | }) 200 | require.NoError(suite.T(), err) 201 | requirePacketFailed(suite.T(), ack) 202 | 203 | // escrowed tokens should have been refuneded. user and core contracts' token 204 | // balances should have been the same as if the escrow never happened 205 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, "uastro", mockInitialBalance) 206 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.coreAddr, "uastro", 0) 207 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainB.senderAddr, "uastro", 0) 208 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainB.coreAddr, "uastro", 0) 209 | } 210 | 211 | // TestSwap the most complex test - we send coins from chainA to chainB, make a 212 | // swap at a DEX contract on chainB, then send the proceedings back to chainA, 213 | // all in the same packet. 214 | func (suite *testSuite) TestSwap() { 215 | var ( 216 | // astro token's denom on chainB 217 | astroB = deriveVoucherDenom(suite.chainB, []*wasmibctesting.Path{suite.pathAB}, "uastro") 218 | 219 | // usdc token's denom on chainA 220 | usdcA = deriveVoucherDenom(suite.chainA, []*wasmibctesting.Path{reversePath(suite.pathAB)}, "uusdc") 221 | 222 | // how many astro to send chainA --> chainB and be swapped 223 | amountAB int64 = 12345 224 | 225 | // how many USDC the seed the DEX 226 | dexInitialBalance int64 = 23456 227 | ) 228 | 229 | // deploy mock-dex contract on chainB 230 | dexStoreRes := suite.chainB.StoreCodeFile("../artifacts/mock_dex.wasm") 231 | dexInstantiateMsg, err := json.Marshal(&types.DexInstantiateMsg{ 232 | DenomIn: astroB, 233 | DenomOut: "uusdc", 234 | }) 235 | require.NoError(suite.T(), err) 236 | dexAddr := suite.chainB.InstantiateContract(dexStoreRes.CodeID, dexInstantiateMsg) 237 | 238 | // fund the dex with USDC 239 | mintCoinsToAccount(suite.chainB.TestChain, dexAddr, sdk.NewCoin("uusdc", sdk.NewInt(dexInitialBalance))) 240 | 241 | // execute the actions: 242 | // - register an interchain account 243 | // - send ASTRO to the ICA 244 | // - swap ASTRO for USDC 245 | // - send USDC back 246 | swapMsg, err := json.Marshal(&types.DexExecuteMsg{ 247 | Swap: &types.DexSwap{}, 248 | }) 249 | require.NoError(suite.T(), err) 250 | 251 | sendBackMsg, err := json.Marshal(&types.CoreExecuteMsg{ 252 | Dispatch: &types.Dispatch{ 253 | ConnectionID: suite.pathAB.EndpointB.ConnectionID, 254 | Actions: []types.Action{ 255 | { 256 | Transfer: &types.TransferAction{ 257 | Denom: "uusdc", 258 | Amount: sdk.NewInt(amountAB), 259 | Recipient: suite.chainA.senderAddr.String(), 260 | }, 261 | }, 262 | }, 263 | }, 264 | }) 265 | require.NoError(suite.T(), err) 266 | 267 | _, ack, err := send(suite.chainA, suite.pathAB, []types.Action{ 268 | { 269 | RegisterAccount: &types.RegisterAccountAction{ 270 | Default: &types.RegisterAccountDefault{}, 271 | }, 272 | }, 273 | { 274 | Transfer: &types.TransferAction{ 275 | Denom: "uastro", 276 | Amount: sdk.NewInt(amountAB), 277 | }, 278 | }, 279 | { 280 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 281 | Wasm: &wasmvmtypes.WasmMsg{ 282 | Execute: &wasmvmtypes.ExecuteMsg{ 283 | ContractAddr: dexAddr.String(), 284 | Msg: swapMsg, 285 | Funds: []wasmvmtypes.Coin{wasmvmtypes.NewCoin(uint64(amountAB), astroB)}, 286 | }, 287 | }, 288 | }), 289 | }, 290 | { 291 | Execute: mustMarshalJSON(suite.T(), &wasmvmtypes.CosmosMsg{ 292 | Wasm: &wasmvmtypes.WasmMsg{ 293 | Execute: &wasmvmtypes.ExecuteMsg{ 294 | ContractAddr: suite.chainB.coreAddr.String(), 295 | Msg: sendBackMsg, 296 | Funds: []wasmvmtypes.Coin{wasmvmtypes.NewCoin(uint64(amountAB), "uusdc")}, 297 | }, 298 | }, 299 | }), 300 | }, 301 | }) 302 | require.NoError(suite.T(), err) 303 | requirePacketSuccess(suite.T(), ack) 304 | 305 | // relay the packet that send the USDC back to chainA 306 | _, ackBytes, err := relaySinglePacket(reversePath(suite.pathAB)) 307 | require.NoError(suite.T(), err) 308 | 309 | ack = &types.PacketAck{} 310 | err = json.Unmarshal(ackBytes, ack) 311 | require.NoError(suite.T(), err) 312 | 313 | requirePacketSuccess(suite.T(), ack) 314 | 315 | // verify balances are correct 316 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, "uastro", mockInitialBalance-amountAB) 317 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.senderAddr, usdcA, amountAB) 318 | requireBalanceEqual(suite.T(), suite.chainA, suite.chainA.coreAddr, "uastro", amountAB) 319 | requireBalanceEqual(suite.T(), suite.chainB, suite.chainA.coreAddr, "uusdc", amountAB) 320 | requireBalanceEqual(suite.T(), suite.chainB, dexAddr, astroB, amountAB) 321 | requireBalanceEqual(suite.T(), suite.chainB, dexAddr, "uusdc", dexInitialBalance-amountAB) 322 | } 323 | -------------------------------------------------------------------------------- /tests/types/core.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import wasmvmtypes "github.com/CosmWasm/wasmvm/types" 4 | 5 | type CoreConfig struct { 6 | DefaultAccountCodeID uint64 `json:"default_account_code_id"` 7 | DefaultTimeoutSecs uint64 `json:"default_timeout_secs"` 8 | } 9 | 10 | type CoreExecuteMsg struct { 11 | Dispatch *Dispatch `json:"dispatch,omitempty"` 12 | Handle *Handle `json:"handle,omitempty"` 13 | } 14 | 15 | type Dispatch struct { 16 | ConnectionID string `json:"connection_id"` 17 | Actions []Action `json:"actions"` 18 | Timeout *wasmvmtypes.IBCTimeout `json:"timeout,omitempty"` 19 | } 20 | 21 | type Handle struct { 22 | CounterpartyEndpoint wasmvmtypes.IBCEndpoint `json:"counterparty_endpoint"` 23 | Endpoint wasmvmtypes.IBCEndpoint `json:"endpoint"` 24 | Controller string `json:"controller"` 25 | Actions []Action `json:"actions"` 26 | Traces []Trace `json:"traces"` 27 | } 28 | 29 | type CoreQueryMsg struct { 30 | Config *ConfigQuery `json:"config,omitempty"` 31 | DenomHash *DenomHashQuery `json:"denom_hash,omitempty"` 32 | DenomTrace *DenomTraceQuery `json:"denom_trace,omitempty"` 33 | DenomTraces *DenomTracesQuery `json:"denom_traces,omitempty"` 34 | Account *AccountKey `json:"account,omitempty"` 35 | Accounts *AccountsQuery `json:"accounts,omitempty"` 36 | ActiveChannel *ActiveChannelQuery `json:"active_channel,omitempty"` 37 | ActiveChannels *ActiveChannelsQuery `json:"active_channels,omitempty"` 38 | } 39 | 40 | type ConfigQuery struct{} 41 | 42 | type DenomHashQuery struct { 43 | Trace TraceItem `json:"trace"` 44 | } 45 | 46 | type DenomHashResponse struct { 47 | Hash string `json:"hash"` // hex-encoded string 48 | } 49 | 50 | type DenomTraceQuery struct { 51 | Denom string `json:"denom"` 52 | } 53 | 54 | type DenomTraceResponse Trace 55 | 56 | type DenomTracesQuery struct { 57 | StartAfter *string `json:"start_after,omitempty"` 58 | Limit *uint32 `json:"limit,omitempty"` 59 | } 60 | 61 | type DenomTracesResponse []Trace 62 | 63 | type AccountKey struct { 64 | Src wasmvmtypes.IBCEndpoint `json:"src"` 65 | Controller string `json:"controller"` 66 | } 67 | 68 | type AccountResponse struct { 69 | Src wasmvmtypes.IBCEndpoint `json:"src"` 70 | Controller string `json:"controller"` 71 | Address string `json:"address"` 72 | } 73 | 74 | type AccountsQuery struct { 75 | StartAfter *AccountKey `json:"start_after,omitempty"` 76 | Limit *uint32 `json:"limit,omitempty"` 77 | } 78 | 79 | type AccountsResponse []AccountResponse 80 | 81 | type ActiveChannelQuery struct { 82 | ConnectionID string `json:"connection_id"` 83 | } 84 | 85 | type ActiveChannelResponse struct { 86 | ConnectionID string `json:"connection_id"` 87 | Endpoint wasmvmtypes.IBCEndpoint `json:"endpoint"` 88 | } 89 | 90 | type ActiveChannelsQuery struct { 91 | StartAfter *string `json:"start_after,omitempty"` 92 | Limit *uint32 `json:"limit,omitempty"` 93 | } 94 | 95 | type ActiveChannelsResponse []ActiveChannelResponse 96 | -------------------------------------------------------------------------------- /tests/types/mocks.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import wasmvmtypes "github.com/CosmWasm/wasmvm/types" 4 | 5 | // --------------------------- mock-account-factory ---------------------------- 6 | 7 | type FactoryConfig struct { 8 | OneCore string `json:"one_core"` 9 | AllowedEndpoint wasmvmtypes.IBCEndpoint `json:"allowed_endpoint"` 10 | AllowedController string `json:"allowed_controller"` 11 | } 12 | 13 | type FactoryData struct { 14 | CodeID uint64 `json:"code_id"` 15 | InstantiateMsg []byte `json:"instantiate_msg"` 16 | } 17 | 18 | // -------------------------------- mock-sender -------------------------------- 19 | 20 | type SenderInstantiateMsg struct { 21 | OneCore string `json:"one_core"` 22 | } 23 | 24 | type SenderExecuteMsg struct { 25 | Send *Send `json:"send,omitempty"` 26 | ICS999 *CallbackMsg `json:"ics999,omitempty"` 27 | } 28 | 29 | type Send struct { 30 | ConnectionID string `json:"connection_id"` 31 | Actions []Action `json:"actions"` 32 | } 33 | 34 | type SenderQueryMsg struct { 35 | Outcome *OutcomeKey `json:"outcome,omitempty"` 36 | Outcomes *OutcomesQuery `json:"outcomes,omitempty"` 37 | } 38 | 39 | type OutcomeKey struct { 40 | Dest wasmvmtypes.IBCEndpoint `json:"dest"` 41 | Sequence uint64 `json:"sequence"` 42 | } 43 | 44 | type OutcomesQuery struct { 45 | StartAfter *OutcomeKey `json:"start_after,omitempty"` 46 | Limit *uint32 `json:"limit,omitempty"` 47 | } 48 | 49 | type OutcomeResponse struct { 50 | Dest wasmvmtypes.IBCEndpoint `json:"dest"` 51 | Sequence uint64 `json:"sequence"` 52 | Outcome PacketOutcome `json:"outcome"` 53 | } 54 | 55 | // ------------------------------- mock-counter -------------------------------- 56 | 57 | type CounterExecuteMsg struct { 58 | Increment *Increment `json:"increment,omitempty"` 59 | IncrementButFail *IncrementButFail `json:"increment_but_fail,omitempty"` 60 | } 61 | 62 | type Increment struct{} 63 | 64 | type IncrementButFail struct{} 65 | 66 | type IncrementResult struct { 67 | NewNumber uint64 `json:"new_number"` 68 | } 69 | 70 | type CounterQueryMsg struct { 71 | Number *NumberQuery `json:"number,omitempty"` 72 | } 73 | 74 | type NumberQuery struct{} 75 | 76 | type NumberResponse struct { 77 | Number uint64 `json:"number"` 78 | } 79 | 80 | // --------------------------------- mock-dex ---------------------------------- 81 | 82 | type DexInstantiateMsg struct { 83 | DenomIn string `json:"denom_in"` 84 | DenomOut string `json:"denom_out"` 85 | } 86 | 87 | type DexExecuteMsg struct { 88 | Swap *DexSwap `json:"swap,omitempty"` 89 | } 90 | 91 | type DexSwap struct{} 92 | 93 | type DexQueryMsg struct { 94 | Config *DexConfigQuery `json:"Config"` 95 | } 96 | 97 | type DexConfigQuery struct{} 98 | 99 | type DexConfigResponse DexInstantiateMsg 100 | -------------------------------------------------------------------------------- /tests/types/ownable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type Ownership struct { 4 | Owner string `json:"owner,omitempty"` 5 | PendingOwner string `json:"pending_owner,omitempty"` 6 | PendingExpiry *Expiration `json:"pending_expiry,omitempty"` 7 | } 8 | 9 | type Expiration struct { 10 | AtHeight uint64 `json:"at_height,omitempty"` 11 | AtTime uint64 `json:"at_time,string,omitempty"` 12 | Never *Never `json:"never,omitempty"` 13 | } 14 | 15 | type Never struct{} 16 | -------------------------------------------------------------------------------- /tests/types/packet.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | 6 | channeltypes "github.com/cosmos/ibc-go/v4/modules/core/04-channel/types" 7 | 8 | wasmvmtypes "github.com/CosmWasm/wasmvm/types" 9 | ) 10 | 11 | // ---------------------------------- channel ---------------------------------- 12 | 13 | const ( 14 | Order = channeltypes.UNORDERED 15 | Version = "ics999-1" 16 | ) 17 | 18 | // ---------------------------------- packet ----------------------------------- 19 | 20 | type PacketData struct { 21 | Controller string `json:"controller"` 22 | Actions []Action `json:"actions"` 23 | Traces []Trace `json:"traces"` 24 | } 25 | 26 | type Action struct { 27 | Transfer *TransferAction `json:"transfer,omitempty"` 28 | RegisterAccount *RegisterAccountAction `json:"register_account,omitempty"` 29 | Execute []byte `json:"execute,omitempty"` 30 | Query []byte `json:"query,omitempty"` 31 | } 32 | 33 | type TransferAction struct { 34 | Denom string `json:"denom"` 35 | Amount sdk.Int `json:"amount"` 36 | Recipient string `json:"recipient,omitempty"` 37 | } 38 | 39 | type RegisterAccountAction struct { 40 | Default *RegisterAccountDefault `json:"default,omitempty"` 41 | CustomFactory *RegisterAccountCustomFactory `json:"custom_factory,omitempty"` 42 | } 43 | 44 | type RegisterAccountDefault struct { 45 | Salt []byte `json:"salt,omitempty"` 46 | } 47 | 48 | type RegisterAccountCustomFactory struct { 49 | Address string `json:"address,omitempty"` 50 | Data []byte `json:"data,omitempty"` 51 | } 52 | 53 | // ------------------------------------ ack ------------------------------------ 54 | 55 | type PacketAck struct { 56 | Success []ActionResult `json:"success,omitempty"` 57 | Failed string `json:"failed,omitempty"` 58 | } 59 | 60 | type ActionResult struct { 61 | Transfer *TransferResult `json:"transfer,omitempty"` 62 | RegisterAccount *RegisterAccountResult `json:"register_account,omitempty"` 63 | Execute *ExecuteResult `json:"execute,omitempty"` 64 | Query *QueryResult `json:"query,omitempty"` 65 | } 66 | 67 | type TransferResult struct { 68 | Denom string `json:"denom"` 69 | NewToken bool `json:"new_token"` 70 | Recipient string `json:"recipient"` 71 | } 72 | 73 | type RegisterAccountResult struct { 74 | Address string `json:"address"` 75 | } 76 | 77 | type ExecuteResult struct { 78 | Data []byte `json:"data,omitempty"` 79 | } 80 | 81 | type QueryResult struct { 82 | Response []byte `json:"response"` 83 | } 84 | 85 | // ----------------------------------- trace ----------------------------------- 86 | 87 | type Trace struct { 88 | Denom string `json:"denom"` 89 | BaseDenom string `json:"base_denom"` 90 | Path []wasmvmtypes.IBCEndpoint `json:"path"` 91 | } 92 | 93 | type TraceItem struct { 94 | BaseDenom string `json:"base_denom"` 95 | Path []wasmvmtypes.IBCEndpoint `json:"path"` 96 | } 97 | 98 | // --------------------------- third party: factory ---------------------------- 99 | 100 | type FactoryExecuteMsg struct { 101 | ICS999 *FactoryMsg `json:"ics999,omitempty"` 102 | } 103 | 104 | type FactoryMsg struct { 105 | Src wasmvmtypes.IBCEndpoint `json:"src"` 106 | Controller string `json:"controller"` 107 | Data []byte `json:"data,omitempty"` 108 | } 109 | 110 | type FactoryResponse struct { 111 | Host string `json:"host"` 112 | } 113 | 114 | // -------------------------- third party: controller -------------------------- 115 | 116 | type ControllerExecuteMsg struct { 117 | ICS999 *CallbackMsg `json:"ics999,omitempty"` 118 | } 119 | 120 | type CallbackMsg struct { 121 | Dest wasmvmtypes.IBCEndpoint `json:"dest"` 122 | Sequence uint64 `json:"sequence"` 123 | Outcome PacketOutcome `json:"outcome"` 124 | } 125 | 126 | type PacketOutcome struct { 127 | Success []ActionResult `json:"success,omitempty"` 128 | Failed string `json:"failed,omitempty"` 129 | Timeout *Timeout `json:"timeout,omitempty"` 130 | } 131 | 132 | type Timeout struct{} 133 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // This file uses the recommended method for tracking developer tools in a Go 5 | // module. 6 | // 7 | // REF: https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 8 | package tools 9 | 10 | import _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 11 | --------------------------------------------------------------------------------