├── .gitattributes ├── .github └── workflows │ ├── add-to-devrel.yml │ └── tests.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── README.md ├── rust-toolchain.toml ├── src └── lib.rs └── tests ├── common.rs ├── contracts └── defi │ ├── Cargo.toml │ ├── res │ └── defi.wasm │ └── src │ └── lib.rs ├── storage.rs ├── supply.rs └── transfer.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock -diff 2 | -------------------------------------------------------------------------------- /.github/workflows/add-to-devrel.yml: -------------------------------------------------------------------------------- 1 | name: 'Add to DevRel Project' 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | name: Add issue/PR to project 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/add-to-project@v1.0.0 19 | with: 20 | # add to DevRel Project #117 21 | project-url: https://github.com/orgs/near/projects/117 22 | github-token: ${{ secrets.PROJECT_GH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: push 3 | jobs: 4 | tests: 5 | strategy: 6 | matrix: 7 | platform: [ubuntu-latest, macos-latest] 8 | runs-on: ${{ matrix.platform }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Install and test modules 12 | run: | 13 | cargo test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # Developer note: near.gitignore will be renamed to .gitignore upon project creation 3 | # dependencies 4 | package-lock.json 5 | *.lock 6 | **/node_modules 7 | /.pnp 8 | .pnp.js 9 | **/out 10 | 11 | #keys 12 | **/neardev 13 | 14 | # testing 15 | /coverage 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | .testnet 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | **/target 33 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fungible-token" 3 | version = "1.0.0" 4 | authors = ["Near Inc "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | near-sdk = "5.5.0" 12 | near-contract-standards = "5.5.0" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.5.0", features = ["unit-testing"] } 16 | near-workspaces = { version = "0.14.1", features = ["unstable"] } 17 | anyhow = "1.0" 18 | tokio = { version = "1.41.0", features = ["full"] } 19 | cargo-near-build = "0.3.2" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 NEAR Inc 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fungible Token (FT) Example 👋 2 | 3 | [![](https://img.shields.io/badge/⋈%20Examples-Basics-green)](https://docs.near.org/tutorials/welcome) 4 | [![](https://img.shields.io/badge/Contract-Rust-red)](contract-rs) 5 | 6 | This repository contains an example implementation of a [fungible token] contract in Rust which uses [near-contract-standards] and workspaces-rs tests. 7 | 8 | [fungible token]: https://nomicon.io/Standards/FungibleToken/Core 9 | [near-contract-standards]: https://github.com/near/near-sdk-rs/tree/master/near-contract-standards 10 | [near-workspaces-rs]: https://github.com/near/near-workspaces-rs 11 | 12 |
13 | 14 | ## How to Build Locally? 15 | 16 | Install [`cargo-near`](https://github.com/near/cargo-near) and run: 17 | 18 | ```bash 19 | cargo near build 20 | ``` 21 | 22 | ## How to Test Locally? 23 | 24 | ```bash 25 | cargo test 26 | ``` 27 | 28 | ## How to Deploy? 29 | 30 | To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: 31 | 32 | ```bash 33 | # Create a new account 34 | cargo near create-dev-account 35 | 36 | # Deploy the contract on it 37 | cargo near deploy 38 | 39 | # Initialize the contract 40 | near call new '{"owner_id": "", "total_supply": "1000000000000000", "metadata": { "spec": "ft-1.0.0", "name": "Example Token Name", "symbol": "EXLT", "decimals": 8 }}' --accountId 41 | ``` 42 | 43 | ## Basic methods 44 | ```bash 45 | # View metadata 46 | near view ft_metadata 47 | 48 | # Make a storage deposit 49 | near call storage_deposit '' --accountId --amount 0.00125 50 | 51 | # View balance 52 | near view ft_balance_of '{"account_id": ""}' 53 | 54 | # Transfer tokens 55 | near call ft_transfer '{"receiver_id": "", "amount": "19"}' --accountId --amount 0.000000000000000000000001 56 | ``` 57 | 58 | ## Notes 59 | 60 | - The maximum balance value is limited by U128 (`2**128 - 1`). 61 | - JSON calls should pass U128 as a base-10 string. E.g. "100". 62 | - This does not include escrow functionality, as `ft_transfer_call` provides a superior approach. An escrow system can, of course, be added as a separate contract or additional functionality within this contract. 63 | 64 | ## Useful Links 65 | 66 | - [cargo-near](https://github.com/near/cargo-near) - NEAR smart contract development toolkit for Rust 67 | - [near CLI](https://near.cli.rs) - Iteract with NEAR blockchain from command line 68 | - [NEAR Rust SDK Documentation](https://docs.near.org/sdk/rust/introduction) 69 | - [NEAR Documentation](https://docs.near.org) 70 | - [NEAR StackOverflow](https://stackoverflow.com/questions/tagged/nearprotocol) 71 | - [NEAR Discord](https://near.chat) 72 | - [NEAR Telegram Developers Community Group](https://t.me/neardev) 73 | - NEAR DevHub: [Telegram](https://t.me/neardevhub), [Twitter](https://twitter.com/neardevhub) -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt"] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Fungible Token implementation with JSON serialization. 3 | NOTES: 4 | - The maximum balance value is limited by U128 (2**128 - 1). 5 | - JSON calls should pass U128 as a base-10 string. E.g. "100". 6 | - The contract optimizes the inner trie structure by hashing account IDs. It will prevent some 7 | abuse of deep tries. Shouldn't be an issue, once NEAR clients implement full hashing of keys. 8 | - The contract tracks the change in storage before and after the call. If the storage increases, 9 | the contract requires the caller of the contract to attach enough deposit to the function call 10 | to cover the storage cost. 11 | This is done to prevent a denial of service attack on the contract by taking all available storage. 12 | If the storage decreases, the contract will issue a refund for the cost of the released storage. 13 | The unused tokens from the attached deposit are also refunded, so it's safe to 14 | attach more deposit than required. 15 | - To prevent the deployed contract from being modified or deleted, it should not have any access 16 | keys on its account. 17 | */ 18 | use near_contract_standards::fungible_token::metadata::{ 19 | FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC, 20 | }; 21 | use near_contract_standards::fungible_token::{ 22 | FungibleToken, FungibleTokenCore, FungibleTokenResolver, 23 | }; 24 | use near_contract_standards::storage_management::{ 25 | StorageBalance, StorageBalanceBounds, StorageManagement, 26 | }; 27 | use near_sdk::borsh::BorshSerialize; 28 | use near_sdk::collections::LazyOption; 29 | use near_sdk::json_types::U128; 30 | use near_sdk::{ 31 | env, log, near, require, AccountId, BorshStorageKey, NearToken, PanicOnDefault, PromiseOrValue, 32 | }; 33 | 34 | #[derive(PanicOnDefault)] 35 | #[near(contract_state)] 36 | pub struct Contract { 37 | token: FungibleToken, 38 | metadata: LazyOption, 39 | } 40 | 41 | const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E"; 42 | 43 | #[derive(BorshSerialize, BorshStorageKey)] 44 | #[borsh(crate = "near_sdk::borsh")] 45 | enum StorageKey { 46 | FungibleToken, 47 | Metadata, 48 | } 49 | 50 | #[near] 51 | impl Contract { 52 | /// Initializes the contract with the given total supply owned by the given `owner_id` with 53 | /// default metadata (for example purposes only). 54 | #[init] 55 | pub fn new_default_meta(owner_id: AccountId, total_supply: U128) -> Self { 56 | Self::new( 57 | owner_id, 58 | total_supply, 59 | FungibleTokenMetadata { 60 | spec: FT_METADATA_SPEC.to_string(), 61 | name: "Example NEAR fungible token".to_string(), 62 | symbol: "EXAMPLE".to_string(), 63 | icon: Some(DATA_IMAGE_SVG_NEAR_ICON.to_string()), 64 | reference: None, 65 | reference_hash: None, 66 | decimals: 24, 67 | }, 68 | ) 69 | } 70 | 71 | /// Initializes the contract with the given total supply owned by the given `owner_id` with 72 | /// the given fungible token metadata. 73 | #[init] 74 | pub fn new(owner_id: AccountId, total_supply: U128, metadata: FungibleTokenMetadata) -> Self { 75 | require!(!env::state_exists(), "Already initialized"); 76 | metadata.assert_valid(); 77 | let mut this = Self { 78 | token: FungibleToken::new(StorageKey::FungibleToken), 79 | metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)), 80 | }; 81 | this.token.internal_register_account(&owner_id); 82 | this.token.internal_deposit(&owner_id, total_supply.into()); 83 | 84 | near_contract_standards::fungible_token::events::FtMint { 85 | owner_id: &owner_id, 86 | amount: total_supply, 87 | memo: Some("new tokens are minted"), 88 | } 89 | .emit(); 90 | 91 | this 92 | } 93 | } 94 | 95 | #[near] 96 | impl FungibleTokenCore for Contract { 97 | #[payable] 98 | fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option) { 99 | self.token.ft_transfer(receiver_id, amount, memo) 100 | } 101 | 102 | #[payable] 103 | fn ft_transfer_call( 104 | &mut self, 105 | receiver_id: AccountId, 106 | amount: U128, 107 | memo: Option, 108 | msg: String, 109 | ) -> PromiseOrValue { 110 | self.token.ft_transfer_call(receiver_id, amount, memo, msg) 111 | } 112 | 113 | fn ft_total_supply(&self) -> U128 { 114 | self.token.ft_total_supply() 115 | } 116 | 117 | fn ft_balance_of(&self, account_id: AccountId) -> U128 { 118 | self.token.ft_balance_of(account_id) 119 | } 120 | } 121 | 122 | #[near] 123 | impl FungibleTokenResolver for Contract { 124 | #[private] 125 | fn ft_resolve_transfer( 126 | &mut self, 127 | sender_id: AccountId, 128 | receiver_id: AccountId, 129 | amount: U128, 130 | ) -> U128 { 131 | let (used_amount, burned_amount) = 132 | self.token 133 | .internal_ft_resolve_transfer(&sender_id, receiver_id, amount); 134 | if burned_amount > 0 { 135 | log!("Account @{} burned {}", sender_id, burned_amount); 136 | } 137 | used_amount.into() 138 | } 139 | } 140 | 141 | #[near] 142 | impl StorageManagement for Contract { 143 | #[payable] 144 | fn storage_deposit( 145 | &mut self, 146 | account_id: Option, 147 | registration_only: Option, 148 | ) -> StorageBalance { 149 | self.token.storage_deposit(account_id, registration_only) 150 | } 151 | 152 | #[payable] 153 | fn storage_withdraw(&mut self, amount: Option) -> StorageBalance { 154 | self.token.storage_withdraw(amount) 155 | } 156 | 157 | #[payable] 158 | fn storage_unregister(&mut self, force: Option) -> bool { 159 | #[allow(unused_variables)] 160 | if let Some((account_id, balance)) = self.token.internal_storage_unregister(force) { 161 | log!("Closed @{} with {}", account_id, balance); 162 | true 163 | } else { 164 | false 165 | } 166 | } 167 | 168 | fn storage_balance_bounds(&self) -> StorageBalanceBounds { 169 | self.token.storage_balance_bounds() 170 | } 171 | 172 | fn storage_balance_of(&self, account_id: AccountId) -> Option { 173 | self.token.storage_balance_of(account_id) 174 | } 175 | } 176 | 177 | #[near] 178 | impl FungibleTokenMetadataProvider for Contract { 179 | fn ft_metadata(&self) -> FungibleTokenMetadata { 180 | self.metadata.get().unwrap() 181 | } 182 | } 183 | 184 | #[cfg(all(test, not(target_arch = "wasm32")))] 185 | mod tests { 186 | use near_contract_standards::fungible_token::Balance; 187 | use near_sdk::test_utils::{accounts, VMContextBuilder}; 188 | use near_sdk::{testing_env, Gas}; 189 | 190 | use super::*; 191 | 192 | const TOTAL_SUPPLY: Balance = 1_000_000_000_000_000; 193 | 194 | fn current() -> AccountId { 195 | accounts(0) 196 | } 197 | 198 | fn owner() -> AccountId { 199 | accounts(1) 200 | } 201 | 202 | fn user1() -> AccountId { 203 | accounts(2) 204 | } 205 | 206 | fn user2() -> AccountId { 207 | accounts(3) 208 | } 209 | 210 | fn setup() -> (Contract, VMContextBuilder) { 211 | let mut context = VMContextBuilder::new(); 212 | 213 | let contract = Contract::new_default_meta(owner(), TOTAL_SUPPLY.into()); 214 | 215 | context.storage_usage(env::storage_usage()); 216 | context.current_account_id(current()); 217 | 218 | testing_env!(context.build()); 219 | 220 | (contract, context) 221 | } 222 | 223 | #[test] 224 | fn test_new() { 225 | let (contract, _) = setup(); 226 | 227 | assert_eq!(contract.ft_total_supply().0, TOTAL_SUPPLY); 228 | assert_eq!(contract.ft_balance_of(owner()).0, TOTAL_SUPPLY); 229 | } 230 | 231 | #[test] 232 | fn test_metadata() { 233 | let (contract, _) = setup(); 234 | 235 | assert_eq!(contract.ft_metadata().decimals, 24); 236 | assert!(contract.ft_metadata().icon.is_some()); 237 | assert!(!contract.ft_metadata().spec.is_empty()); 238 | assert!(!contract.ft_metadata().name.is_empty()); 239 | assert!(!contract.ft_metadata().symbol.is_empty()); 240 | } 241 | 242 | #[test] 243 | #[should_panic(expected = "The contract is not initialized")] 244 | fn test_default_panics() { 245 | Contract::default(); 246 | } 247 | 248 | #[test] 249 | fn test_deposit() { 250 | let (mut contract, mut context) = setup(); 251 | 252 | testing_env!(context 253 | .predecessor_account_id(user1()) 254 | .attached_deposit(contract.storage_balance_bounds().min) 255 | .build()); 256 | 257 | assert!(contract.storage_balance_of(user1()).is_none()); 258 | 259 | contract.storage_deposit(None, None); 260 | 261 | let storage_balance = contract.storage_balance_of(user1()).unwrap(); 262 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 263 | assert!(storage_balance.available.is_zero()); 264 | } 265 | 266 | #[test] 267 | fn test_deposit_on_behalf_of_another_user() { 268 | let (mut contract, mut context) = setup(); 269 | 270 | testing_env!(context 271 | .predecessor_account_id(user1()) 272 | .attached_deposit(contract.storage_balance_bounds().min) 273 | .build()); 274 | 275 | assert!(contract.storage_balance_of(user2()).is_none()); 276 | 277 | // predecessor is user1, but deposit is for user2 278 | contract.storage_deposit(Some(user2()), None); 279 | 280 | let storage_balance = contract.storage_balance_of(user2()).unwrap(); 281 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 282 | assert!(storage_balance.available.is_zero()); 283 | 284 | // ensure that user1's storage wasn't affected 285 | assert!(contract.storage_balance_of(user1()).is_none()); 286 | } 287 | 288 | #[should_panic] 289 | #[test] 290 | fn test_deposit_panics_on_less_amount() { 291 | let (mut contract, mut context) = setup(); 292 | 293 | testing_env!(context 294 | .predecessor_account_id(user1()) 295 | .attached_deposit(NearToken::from_yoctonear(100)) 296 | .build()); 297 | 298 | assert!(contract.storage_balance_of(user1()).is_none()); 299 | 300 | // this panics 301 | contract.storage_deposit(None, None); 302 | } 303 | 304 | #[test] 305 | fn test_deposit_account_twice() { 306 | let (mut contract, mut context) = setup(); 307 | 308 | testing_env!(context 309 | .predecessor_account_id(user1()) 310 | .attached_deposit(contract.storage_balance_bounds().min) 311 | .build()); 312 | 313 | // this registers the predecessor 314 | contract.storage_deposit(None, None); 315 | 316 | let storage_balance = contract.storage_balance_of(user1()).unwrap(); 317 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 318 | 319 | // this doesn't panic, and just refunds the deposit as the account is registered already 320 | contract.storage_deposit(None, None); 321 | 322 | // this indicates that total balance hasn't changed 323 | let storage_balance = contract.storage_balance_of(user1()).unwrap(); 324 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 325 | } 326 | 327 | #[test] 328 | fn test_unregister() { 329 | let (mut contract, mut context) = setup(); 330 | 331 | testing_env!(context 332 | .predecessor_account_id(user1()) 333 | .attached_deposit(contract.storage_balance_bounds().min) 334 | .build()); 335 | 336 | contract.storage_deposit(None, None); 337 | 338 | assert!(contract.storage_balance_of(user1()).is_some()); 339 | 340 | testing_env!(context 341 | .predecessor_account_id(user1()) 342 | .attached_deposit(NearToken::from_yoctonear(1)) 343 | .build()); 344 | 345 | assert_eq!(contract.storage_unregister(None), true); 346 | 347 | assert!(contract.storage_balance_of(user1()).is_none()); 348 | } 349 | 350 | #[should_panic] 351 | #[test] 352 | fn test_unregister_panics_on_zero_deposit() { 353 | let (mut contract, mut context) = setup(); 354 | 355 | testing_env!(context 356 | .predecessor_account_id(user1()) 357 | .attached_deposit(contract.storage_balance_bounds().min) 358 | .build()); 359 | 360 | contract.storage_deposit(None, None); 361 | 362 | assert!(contract.storage_balance_of(user1()).is_some()); 363 | 364 | testing_env!(context 365 | .predecessor_account_id(user1()) 366 | .attached_deposit(NearToken::from_yoctonear(0)) 367 | .build()); 368 | 369 | contract.storage_unregister(None); 370 | } 371 | 372 | #[test] 373 | fn test_unregister_of_non_registered_account() { 374 | let (mut contract, mut context) = setup(); 375 | 376 | testing_env!(context 377 | .predecessor_account_id(user1()) 378 | .attached_deposit(NearToken::from_yoctonear(1)) 379 | .build()); 380 | 381 | // "false" indicates that the account wasn't registered 382 | assert_eq!(contract.storage_unregister(None), false); 383 | } 384 | 385 | #[should_panic] 386 | #[test] 387 | fn test_unregister_panics_on_non_zero_balance() { 388 | let (mut contract, mut context) = setup(); 389 | 390 | testing_env!(context 391 | .predecessor_account_id(user1()) 392 | .attached_deposit(contract.storage_balance_bounds().min) 393 | .build()); 394 | 395 | contract.storage_deposit(None, None); 396 | 397 | assert!(contract.storage_balance_of(user1()).is_some()); 398 | 399 | testing_env!(context 400 | .predecessor_account_id(owner()) 401 | .attached_deposit(NearToken::from_yoctonear(1)) 402 | .build()); 403 | let transfer_amount = TOTAL_SUPPLY / 10; 404 | 405 | contract.ft_transfer(user1(), transfer_amount.into(), None); 406 | 407 | testing_env!(context 408 | .predecessor_account_id(user1()) 409 | .attached_deposit(NearToken::from_yoctonear(1)) 410 | .build()); 411 | 412 | contract.storage_unregister(None); 413 | } 414 | 415 | #[test] 416 | fn test_unregister_with_force() { 417 | let (mut contract, mut context) = setup(); 418 | 419 | testing_env!(context 420 | .predecessor_account_id(user1()) 421 | .attached_deposit(contract.storage_balance_bounds().min) 422 | .build()); 423 | 424 | contract.storage_deposit(None, None); 425 | 426 | assert!(contract.storage_balance_of(user1()).is_some()); 427 | 428 | testing_env!(context 429 | .predecessor_account_id(owner()) 430 | .attached_deposit(NearToken::from_yoctonear(1)) 431 | .build()); 432 | let transfer_amount = TOTAL_SUPPLY / 10; 433 | 434 | contract.ft_transfer(user1(), transfer_amount.into(), None); 435 | 436 | testing_env!(context 437 | .predecessor_account_id(user1()) 438 | .attached_deposit(NearToken::from_yoctonear(1)) 439 | .build()); 440 | 441 | // force to unregister no matter what 442 | // this reduces total supply because user's tokens are burnt 443 | assert_eq!(contract.storage_unregister(Some(true)), true); 444 | 445 | assert!(contract.storage_balance_of(user1()).is_none()); 446 | assert_eq!(contract.ft_balance_of(user1()).0, 0); 447 | assert_eq!(contract.ft_total_supply().0, TOTAL_SUPPLY - transfer_amount); 448 | } 449 | 450 | #[test] 451 | fn test_withdraw() { 452 | let (mut contract, mut context) = setup(); 453 | 454 | testing_env!(context 455 | .predecessor_account_id(user1()) 456 | .attached_deposit(contract.storage_balance_bounds().min) 457 | .build()); 458 | 459 | contract.storage_deposit(None, None); 460 | 461 | testing_env!(context 462 | .predecessor_account_id(user1()) 463 | .attached_deposit(NearToken::from_yoctonear(1)) 464 | .build()); 465 | 466 | // Basic Fungible Token implementation never transfers Near to caller 467 | // See: https://github.com/near/near-sdk-rs/blob/5a4c595125364ffe8d7866aa0418a3c92b1c3a6a/near-contract-standards/src/fungible_token/storage_impl.rs#L82 468 | let storage_balance = contract.storage_withdraw(None); 469 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 470 | assert!(storage_balance.available.is_zero()); 471 | 472 | // Basic Fungible Token implementation never transfers Near to caller 473 | // See: https://github.com/near/near-sdk-rs/blob/5a4c595125364ffe8d7866aa0418a3c92b1c3a6a/near-contract-standards/src/fungible_token/storage_impl.rs#L82 474 | let storage_balance = contract.storage_withdraw(None); 475 | assert_eq!(storage_balance.total, contract.storage_balance_bounds().min); 476 | assert!(storage_balance.available.is_zero()); 477 | } 478 | 479 | #[should_panic] 480 | #[test] 481 | fn test_withdraw_panics_on_non_registered_account() { 482 | let (mut contract, mut context) = setup(); 483 | 484 | testing_env!(context 485 | .predecessor_account_id(user1()) 486 | .attached_deposit(NearToken::from_yoctonear(1)) 487 | .build()); 488 | 489 | contract.storage_withdraw(None); 490 | } 491 | 492 | #[should_panic] 493 | #[test] 494 | fn test_withdraw_panics_on_zero_deposit() { 495 | let (mut contract, mut context) = setup(); 496 | 497 | testing_env!(context 498 | .predecessor_account_id(user1()) 499 | .attached_deposit(NearToken::from_yoctonear(0)) 500 | .build()); 501 | 502 | contract.storage_withdraw(None); 503 | } 504 | 505 | #[should_panic] 506 | #[test] 507 | fn test_withdraw_panics_on_amount_greater_than_zero() { 508 | let (mut contract, mut context) = setup(); 509 | 510 | testing_env!(context 511 | .predecessor_account_id(user1()) 512 | .attached_deposit(NearToken::from_yoctonear(1)) 513 | .build()); 514 | 515 | // Basic Fungible Token implementation sets storage_balance_bounds.min == storage_balance_bounds.max 516 | // which means available balance will always be 0 517 | // See: https://github.com/near/near-sdk-rs/blob/5a4c595125364ffe8d7866aa0418a3c92b1c3a6a/near-contract-standards/src/fungible_token/storage_impl.rs#L82 518 | contract.storage_withdraw(Some(NearToken::from_yoctonear(1))); 519 | } 520 | 521 | #[test] 522 | fn test_transfer() { 523 | let (mut contract, mut context) = setup(); 524 | 525 | testing_env!(context 526 | .predecessor_account_id(user1()) 527 | .attached_deposit(contract.storage_balance_bounds().min) 528 | .build()); 529 | 530 | // Paying for account registration of user1, aka storage deposit 531 | contract.storage_deposit(None, None); 532 | 533 | testing_env!(context 534 | .predecessor_account_id(owner()) 535 | .attached_deposit(NearToken::from_yoctonear(1)) 536 | .build()); 537 | let transfer_amount = TOTAL_SUPPLY / 10; 538 | 539 | contract.ft_transfer(user1(), transfer_amount.into(), None); 540 | 541 | assert_eq!( 542 | contract.ft_balance_of(owner()).0, 543 | (TOTAL_SUPPLY - transfer_amount) 544 | ); 545 | assert_eq!(contract.ft_balance_of(user1()).0, transfer_amount); 546 | } 547 | 548 | #[should_panic] 549 | #[test] 550 | fn test_transfer_panics_on_self_receiver() { 551 | let (mut contract, mut context) = setup(); 552 | 553 | testing_env!(context 554 | .predecessor_account_id(user1()) 555 | .attached_deposit(contract.storage_balance_bounds().min) 556 | .build()); 557 | 558 | // Paying for account registration of user1, aka storage deposit 559 | contract.storage_deposit(None, None); 560 | 561 | testing_env!(context 562 | .predecessor_account_id(owner()) 563 | .attached_deposit(NearToken::from_yoctonear(1)) 564 | .build()); 565 | let transfer_amount = TOTAL_SUPPLY / 10; 566 | 567 | contract.ft_transfer(owner(), transfer_amount.into(), None); 568 | } 569 | 570 | #[should_panic] 571 | #[test] 572 | fn test_transfer_panics_on_zero_amount() { 573 | let (mut contract, mut context) = setup(); 574 | 575 | testing_env!(context 576 | .predecessor_account_id(user1()) 577 | .attached_deposit(contract.storage_balance_bounds().min) 578 | .build()); 579 | 580 | // Paying for account registration of user1, aka storage deposit 581 | contract.storage_deposit(None, None); 582 | 583 | testing_env!(context 584 | .predecessor_account_id(owner()) 585 | .attached_deposit(NearToken::from_yoctonear(1)) 586 | .build()); 587 | 588 | contract.ft_transfer(user1(), 0.into(), None); 589 | } 590 | 591 | #[should_panic] 592 | #[test] 593 | fn test_transfer_panics_on_zero_deposit() { 594 | let (mut contract, mut context) = setup(); 595 | 596 | testing_env!(context 597 | .predecessor_account_id(user1()) 598 | .attached_deposit(contract.storage_balance_bounds().min) 599 | .build()); 600 | 601 | // Paying for account registration of user1, aka storage deposit 602 | contract.storage_deposit(None, None); 603 | 604 | testing_env!(context 605 | .predecessor_account_id(owner()) 606 | .attached_deposit(NearToken::from_yoctonear(0)) 607 | .build()); 608 | 609 | let transfer_amount = TOTAL_SUPPLY / 10; 610 | contract.ft_transfer(user1(), transfer_amount.into(), None); 611 | } 612 | 613 | #[should_panic] 614 | #[test] 615 | fn test_transfer_panics_on_non_registered_sender() { 616 | let (mut contract, mut context) = setup(); 617 | 618 | testing_env!(context 619 | .predecessor_account_id(user1()) 620 | .attached_deposit(NearToken::from_yoctonear(1)) 621 | .build()); 622 | 623 | let transfer_amount = TOTAL_SUPPLY / 10; 624 | contract.ft_transfer(user1(), transfer_amount.into(), None); 625 | } 626 | 627 | #[should_panic] 628 | #[test] 629 | fn test_transfer_panics_on_non_registered_receiver() { 630 | let (mut contract, mut context) = setup(); 631 | 632 | testing_env!(context 633 | .predecessor_account_id(owner()) 634 | .attached_deposit(NearToken::from_yoctonear(1)) 635 | .build()); 636 | 637 | let transfer_amount = TOTAL_SUPPLY / 10; 638 | contract.ft_transfer(user1(), transfer_amount.into(), None); 639 | } 640 | 641 | #[should_panic] 642 | #[test] 643 | fn test_transfer_panics_on_amount_greater_than_balance() { 644 | let (mut contract, mut context) = setup(); 645 | 646 | testing_env!(context 647 | .predecessor_account_id(user1()) 648 | .attached_deposit(contract.storage_balance_bounds().min) 649 | .build()); 650 | 651 | // Paying for account registration of user1, aka storage deposit 652 | contract.storage_deposit(None, None); 653 | 654 | testing_env!(context 655 | .predecessor_account_id(owner()) 656 | .attached_deposit(NearToken::from_yoctonear(1)) 657 | .build()); 658 | 659 | let transfer_amount = TOTAL_SUPPLY + 10; 660 | contract.ft_transfer(user1(), transfer_amount.into(), None); 661 | } 662 | 663 | #[test] 664 | fn test_transfer_call() { 665 | let (mut contract, mut context) = setup(); 666 | 667 | testing_env!(context 668 | .predecessor_account_id(user1()) 669 | .attached_deposit(contract.storage_balance_bounds().min) 670 | .build()); 671 | 672 | // Paying for account registration of user1, aka storage deposit 673 | contract.storage_deposit(None, None); 674 | 675 | testing_env!(context 676 | .predecessor_account_id(owner()) 677 | .attached_deposit(NearToken::from_yoctonear(1)) 678 | .build()); 679 | let transfer_amount = TOTAL_SUPPLY / 10; 680 | 681 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 682 | 683 | assert_eq!( 684 | contract.ft_balance_of(owner()).0, 685 | (TOTAL_SUPPLY - transfer_amount) 686 | ); 687 | assert_eq!(contract.ft_balance_of(user1()).0, transfer_amount); 688 | } 689 | 690 | #[should_panic] 691 | #[test] 692 | fn test_transfer_call_panics_on_self_receiver() { 693 | let (mut contract, mut context) = setup(); 694 | 695 | testing_env!(context 696 | .predecessor_account_id(user1()) 697 | .attached_deposit(contract.storage_balance_bounds().min) 698 | .build()); 699 | 700 | // Paying for account registration of user1, aka storage deposit 701 | contract.storage_deposit(None, None); 702 | 703 | testing_env!(context 704 | .predecessor_account_id(owner()) 705 | .attached_deposit(NearToken::from_yoctonear(1)) 706 | .build()); 707 | let transfer_amount = TOTAL_SUPPLY / 10; 708 | 709 | contract.ft_transfer_call(owner(), transfer_amount.into(), None, "".to_string()); 710 | } 711 | 712 | #[should_panic] 713 | #[test] 714 | fn test_transfer_call_panics_on_zero_amount() { 715 | let (mut contract, mut context) = setup(); 716 | 717 | testing_env!(context 718 | .predecessor_account_id(user1()) 719 | .attached_deposit(contract.storage_balance_bounds().min) 720 | .build()); 721 | 722 | // Paying for account registration of user1, aka storage deposit 723 | contract.storage_deposit(None, None); 724 | 725 | testing_env!(context 726 | .predecessor_account_id(owner()) 727 | .attached_deposit(NearToken::from_yoctonear(1)) 728 | .build()); 729 | 730 | contract.ft_transfer_call(user1(), 0.into(), None, "".to_string()); 731 | } 732 | 733 | #[should_panic] 734 | #[test] 735 | fn test_transfer_call_panics_on_zero_deposit() { 736 | let (mut contract, mut context) = setup(); 737 | 738 | testing_env!(context 739 | .predecessor_account_id(user1()) 740 | .attached_deposit(contract.storage_balance_bounds().min) 741 | .build()); 742 | 743 | // Paying for account registration of user1, aka storage deposit 744 | contract.storage_deposit(None, None); 745 | 746 | testing_env!(context 747 | .predecessor_account_id(owner()) 748 | .attached_deposit(NearToken::from_yoctonear(0)) 749 | .build()); 750 | 751 | let transfer_amount = TOTAL_SUPPLY / 10; 752 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 753 | } 754 | 755 | #[should_panic] 756 | #[test] 757 | fn test_transfer_call_panics_on_non_registered_sender() { 758 | let (mut contract, mut context) = setup(); 759 | 760 | testing_env!(context 761 | .predecessor_account_id(user1()) 762 | .attached_deposit(NearToken::from_yoctonear(1)) 763 | .build()); 764 | 765 | let transfer_amount = TOTAL_SUPPLY / 10; 766 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 767 | } 768 | 769 | #[should_panic] 770 | #[test] 771 | fn test_transfer_call_panics_on_non_registered_receiver() { 772 | let (mut contract, mut context) = setup(); 773 | 774 | testing_env!(context 775 | .predecessor_account_id(owner()) 776 | .attached_deposit(NearToken::from_yoctonear(1)) 777 | .build()); 778 | 779 | let transfer_amount = TOTAL_SUPPLY / 10; 780 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 781 | } 782 | 783 | #[should_panic] 784 | #[test] 785 | fn test_transfer_call_panics_on_amount_greater_than_balance() { 786 | let (mut contract, mut context) = setup(); 787 | 788 | testing_env!(context 789 | .predecessor_account_id(user1()) 790 | .attached_deposit(contract.storage_balance_bounds().min) 791 | .build()); 792 | 793 | // Paying for account registration of user1, aka storage deposit 794 | contract.storage_deposit(None, None); 795 | 796 | testing_env!(context 797 | .predecessor_account_id(owner()) 798 | .attached_deposit(NearToken::from_yoctonear(1)) 799 | .build()); 800 | 801 | let transfer_amount = TOTAL_SUPPLY + 10; 802 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 803 | } 804 | #[should_panic] 805 | #[test] 806 | fn test_transfer_call_panics_on_unsufficient_gas() { 807 | let (mut contract, mut context) = setup(); 808 | 809 | testing_env!(context 810 | .predecessor_account_id(user1()) 811 | .attached_deposit(contract.storage_balance_bounds().min) 812 | .build()); 813 | 814 | // Paying for account registration of user1, aka storage deposit 815 | contract.storage_deposit(None, None); 816 | 817 | testing_env!(context 818 | .predecessor_account_id(owner()) 819 | .attached_deposit(NearToken::from_yoctonear(1)) 820 | .prepaid_gas(Gas::from_tgas(10)) 821 | .build()); 822 | let transfer_amount = TOTAL_SUPPLY / 10; 823 | 824 | contract.ft_transfer_call(user1(), transfer_amount.into(), None, "".to_string()); 825 | } 826 | } 827 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use cargo_near_build::BuildOpts; 4 | use near_sdk::{json_types::U128, AccountId, NearToken}; 5 | use near_workspaces::{Account, Contract, DevNetwork, Worker}; 6 | 7 | const INITIAL_BALANCE: NearToken = NearToken::from_near(30); 8 | pub const ONE_YOCTO: NearToken = NearToken::from_yoctonear(1); 9 | 10 | static FUNGIBLE_TOKEN_CONTRACT_WASM: LazyLock> = LazyLock::new(|| { 11 | let artifact = cargo_near_build::build(BuildOpts { 12 | no_abi: true, 13 | no_embed_abi: true, 14 | ..Default::default() 15 | }) 16 | .expect("Could not compile Fungible Token contract for tests"); 17 | 18 | let contract_wasm = std::fs::read(&artifact.path).expect( 19 | format!( 20 | "Could not read Fungible Token WASM file from {}", 21 | artifact.path 22 | ) 23 | .as_str(), 24 | ); 25 | 26 | contract_wasm 27 | }); 28 | 29 | static DEFI_CONTRACT_WASM: LazyLock> = LazyLock::new(|| { 30 | let artifact_path = "tests/contracts/defi/res/defi.wasm"; 31 | 32 | let contract_wasm = std::fs::read(artifact_path) 33 | .expect(format!("Could not read DeFi WASM file from {}", artifact_path).as_str()); 34 | 35 | contract_wasm 36 | }); 37 | 38 | pub async fn init_accounts(root: &Account) -> anyhow::Result<(Account, Account, Account, Account)> { 39 | // create accounts 40 | let alice = root 41 | .create_subaccount("alice") 42 | .initial_balance(INITIAL_BALANCE) 43 | .transact() 44 | .await? 45 | .into_result()?; 46 | let bob = root 47 | .create_subaccount("bob") 48 | .initial_balance(INITIAL_BALANCE) 49 | .transact() 50 | .await? 51 | .into_result()?; 52 | let charlie = root 53 | .create_subaccount("charlie") 54 | .initial_balance(INITIAL_BALANCE) 55 | .transact() 56 | .await? 57 | .into_result()?; 58 | let dave = root 59 | .create_subaccount("dave") 60 | .initial_balance(INITIAL_BALANCE) 61 | .transact() 62 | .await? 63 | .into_result()?; 64 | 65 | return Ok((alice, bob, charlie, dave)); 66 | } 67 | 68 | pub async fn init_contracts( 69 | worker: &Worker, 70 | initial_balance: U128, 71 | account: &Account, 72 | ) -> anyhow::Result<(Contract, Contract)> { 73 | let ft_contract = worker.dev_deploy(&FUNGIBLE_TOKEN_CONTRACT_WASM).await?; 74 | 75 | let res = ft_contract 76 | .call("new_default_meta") 77 | .args_json((ft_contract.id(), initial_balance)) 78 | .max_gas() 79 | .transact() 80 | .await?; 81 | assert!(res.is_success()); 82 | 83 | let defi_contract = worker.dev_deploy(&DEFI_CONTRACT_WASM).await?; 84 | 85 | let res = defi_contract 86 | .call("new") 87 | .args_json((ft_contract.id(),)) 88 | .max_gas() 89 | .transact() 90 | .await?; 91 | assert!(res.is_success()); 92 | 93 | let res = ft_contract 94 | .call("storage_deposit") 95 | .args_json((account.id(), Option::::None)) 96 | .deposit(near_sdk::env::storage_byte_cost().saturating_mul(125)) 97 | .max_gas() 98 | .transact() 99 | .await?; 100 | assert!(res.is_success()); 101 | 102 | return Ok((ft_contract, defi_contract)); 103 | } 104 | 105 | pub async fn register_user(contract: &Contract, account_id: &AccountId) -> anyhow::Result<()> { 106 | let res = contract 107 | .call("storage_deposit") 108 | .args_json((account_id, Option::::None)) 109 | .max_gas() 110 | .deposit(near_sdk::env::storage_byte_cost().saturating_mul(125)) 111 | .transact() 112 | .await?; 113 | assert!(res.is_success()); 114 | 115 | Ok(()) 116 | } 117 | -------------------------------------------------------------------------------- /tests/contracts/defi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "defi" 3 | version = "0.0.1" 4 | authors = ["Near Inc "] 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | near-sdk = "5.5.0" 12 | near-contract-standards = "5.5.0" 13 | 14 | [dev-dependencies] 15 | near-sdk = { version = "5.5.0", features = ["unit-testing"] } -------------------------------------------------------------------------------- /tests/contracts/defi/res/defi.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/near-examples/FT/13c1a3ce01a56bb230770d402598bb9fe927aba8/tests/contracts/defi/res/defi.wasm -------------------------------------------------------------------------------- /tests/contracts/defi/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Some hypothetical DeFi contract that will do smart things with the transferred tokens 3 | */ 4 | use near_contract_standards::fungible_token::{receiver::FungibleTokenReceiver, Balance}; 5 | use near_sdk::json_types::U128; 6 | use near_sdk::{env, log, near, require, AccountId, Gas, PanicOnDefault, PromiseOrValue}; 7 | 8 | const BASE_GAS: u64 = 5_000_000_000_000; 9 | const PROMISE_CALL: u64 = 5_000_000_000_000; 10 | const GAS_FOR_FT_ON_TRANSFER: Gas = Gas::from_gas(BASE_GAS + PROMISE_CALL); 11 | 12 | #[derive(PanicOnDefault)] 13 | #[near(contract_state)] 14 | pub struct DeFi { 15 | fungible_token_account_id: AccountId, 16 | } 17 | 18 | // Have to repeat the same trait for our own implementation. 19 | #[allow(dead_code)] 20 | trait ValueReturnTrait { 21 | fn value_please(&self, amount_to_return: String) -> PromiseOrValue; 22 | } 23 | 24 | #[near] 25 | impl DeFi { 26 | #[init] 27 | pub fn new(fungible_token_account_id: AccountId) -> Self { 28 | require!(!env::state_exists(), "Already initialized"); 29 | Self { fungible_token_account_id: fungible_token_account_id.into() } 30 | } 31 | } 32 | 33 | #[near] 34 | impl FungibleTokenReceiver for DeFi { 35 | /// If given `msg: "take-my-money", immediately returns U128::From(0) 36 | /// Otherwise, makes a cross-contract call to own `value_please` function, passing `msg` 37 | /// value_please will attempt to parse `msg` as an integer and return a U128 version of it 38 | fn ft_on_transfer( 39 | &mut self, 40 | sender_id: AccountId, 41 | amount: U128, 42 | msg: String, 43 | ) -> PromiseOrValue { 44 | // Verifying that we were called by fungible token contract that we expect. 45 | require!( 46 | env::predecessor_account_id() == self.fungible_token_account_id, 47 | "Only supports the one fungible token contract" 48 | ); 49 | log!("in {} tokens from @{} ft_on_transfer, msg = {}", amount.0, sender_id, msg); 50 | match msg.as_str() { 51 | "take-my-money" => PromiseOrValue::Value(U128::from(0)), 52 | _ => { 53 | let prepaid_gas = env::prepaid_gas(); 54 | let account_id = env::current_account_id(); 55 | Self::ext(account_id) 56 | .with_static_gas(prepaid_gas.saturating_sub(GAS_FOR_FT_ON_TRANSFER)) 57 | .value_please(msg) 58 | .into() 59 | } 60 | } 61 | } 62 | } 63 | 64 | #[near] 65 | impl ValueReturnTrait for DeFi { 66 | fn value_please(&self, amount_to_return: String) -> PromiseOrValue { 67 | log!("in value_please, amount_to_return = {}", amount_to_return); 68 | let amount: Balance = amount_to_return.parse().expect("Not an integer"); 69 | PromiseOrValue::Value(amount.into()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/storage.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use near_sdk::{json_types::U128, NearToken}; 4 | 5 | use common::{init_accounts, init_contracts, ONE_YOCTO}; 6 | 7 | #[tokio::test] 8 | async fn storage_deposit_not_enough_deposit() -> anyhow::Result<()> { 9 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 10 | 11 | let worker = near_workspaces::sandbox().await?; 12 | let root = worker.root_account()?; 13 | let (alice, _, _, _) = init_accounts(&root).await?; 14 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 15 | 16 | let new_account = ft_contract 17 | .as_account() 18 | .create_subaccount("new-account") 19 | .initial_balance(NearToken::from_near(10)) 20 | .transact() 21 | .await? 22 | .into_result()?; 23 | 24 | let new_account_balance_before_deposit = new_account.view_account().await?.balance; 25 | let contract_balance_before_deposit = ft_contract.view_account().await?.balance; 26 | 27 | let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); 28 | let res = new_account 29 | .call(ft_contract.id(), "storage_deposit") 30 | .args(b"{}".to_vec()) 31 | .max_gas() 32 | .deposit(minimal_deposit.saturating_sub(NearToken::from_yoctonear(1))) 33 | .transact() 34 | .await?; 35 | assert!(res.is_failure()); 36 | 37 | let new_account_balance_diff = new_account_balance_before_deposit 38 | .saturating_sub(new_account.view_account().await?.balance); 39 | // new_account is charged the transaction fee, so it should loose some NEAR 40 | assert!(new_account_balance_diff > NearToken::from_near(0)); 41 | assert!(new_account_balance_diff < NearToken::from_millinear(1)); 42 | 43 | let contract_balance_diff = ft_contract 44 | .view_account() 45 | .await? 46 | .balance 47 | .saturating_sub(contract_balance_before_deposit); 48 | // contract receives a gas rewards for the function call, so it should gain some NEAR 49 | assert!(contract_balance_diff > NearToken::from_near(0)); 50 | assert!(contract_balance_diff < NearToken::from_yoctonear(30_000_000_000_000_000_000)); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[tokio::test] 56 | async fn storage_deposit_minimal_deposit() -> anyhow::Result<()> { 57 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 58 | 59 | let worker = near_workspaces::sandbox().await?; 60 | let root = worker.root_account()?; 61 | let (alice, _, _, _) = init_accounts(&root).await?; 62 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 63 | 64 | let new_account = ft_contract 65 | .as_account() 66 | .create_subaccount("new-account") 67 | .initial_balance(NearToken::from_near(10)) 68 | .transact() 69 | .await? 70 | .into_result()?; 71 | 72 | let new_account_balance_before_deposit = new_account.view_account().await?.balance; 73 | let contract_balance_before_deposit = ft_contract.view_account().await?.balance; 74 | 75 | let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); 76 | new_account 77 | .call(ft_contract.id(), "storage_deposit") 78 | .args(b"{}".to_vec()) 79 | .max_gas() 80 | .deposit(minimal_deposit) 81 | .transact() 82 | .await? 83 | .into_result()?; 84 | 85 | let new_account_balance_diff = new_account_balance_before_deposit 86 | .saturating_sub(new_account.view_account().await?.balance); 87 | // new_account is charged the transaction fee, so it should loose a bit more than minimal_deposit 88 | assert!(new_account_balance_diff > minimal_deposit); 89 | assert!( 90 | new_account_balance_diff < minimal_deposit.saturating_add(NearToken::from_millinear(1)) 91 | ); 92 | 93 | let contract_balance_diff = ft_contract 94 | .view_account() 95 | .await? 96 | .balance 97 | .saturating_sub(contract_balance_before_deposit); 98 | // contract receives a gas rewards for the function call, so the difference should be slightly more than minimal_deposit 99 | assert!(contract_balance_diff > minimal_deposit); 100 | // adjust the upper limit of the assertion to be more flexible for small variations in the gas reward received 101 | assert!( 102 | contract_balance_diff 103 | < minimal_deposit.saturating_add(NearToken::from_yoctonear(50_000_000_000_000_000_000)) 104 | ); 105 | 106 | Ok(()) 107 | } 108 | 109 | #[tokio::test] 110 | async fn storage_deposit_refunds_excessive_deposit() -> anyhow::Result<()> { 111 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 112 | 113 | let worker = near_workspaces::sandbox().await?; 114 | let root = worker.root_account()?; 115 | let (alice, _, _, _) = init_accounts(&root).await?; 116 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 117 | 118 | let minimal_deposit = near_sdk::env::storage_byte_cost().saturating_mul(125); 119 | 120 | // Check the storage balance bounds to make sure we have the right minimal deposit 121 | // 122 | #[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)] 123 | #[serde(crate = "near_sdk::serde")] 124 | struct StorageBalanceBounds { 125 | min: U128, 126 | max: U128, 127 | } 128 | let storage_balance_bounds: StorageBalanceBounds = ft_contract 129 | .call("storage_balance_bounds") 130 | .view() 131 | .await? 132 | .json()?; 133 | assert_eq!( 134 | storage_balance_bounds.min, 135 | minimal_deposit.as_yoctonear().into() 136 | ); 137 | assert_eq!( 138 | storage_balance_bounds.max, 139 | minimal_deposit.as_yoctonear().into() 140 | ); 141 | 142 | // Check that a non-registered account does not have storage balance 143 | // 144 | #[derive(near_sdk::serde::Serialize, near_sdk::serde::Deserialize)] 145 | #[serde(crate = "near_sdk::serde")] 146 | struct StorageBalanceOf { 147 | total: U128, 148 | available: U128, 149 | } 150 | let storage_balance_bounds: Option = ft_contract 151 | .call("storage_balance_of") 152 | .args_json(near_sdk::serde_json::json!({"account_id": "non-registered-account"})) 153 | .view() 154 | .await? 155 | .json()?; 156 | assert!(storage_balance_bounds.is_none()); 157 | 158 | // Create a new account and deposit some NEAR to cover the storage 159 | // 160 | let new_account = ft_contract 161 | .as_account() 162 | .create_subaccount("new-account") 163 | .initial_balance(NearToken::from_near(10)) 164 | .transact() 165 | .await? 166 | .into_result()?; 167 | 168 | let new_account_balance_before_deposit = new_account.view_account().await?.balance; 169 | let contract_balance_before_deposit = ft_contract.view_account().await?.balance; 170 | 171 | new_account 172 | .call(ft_contract.id(), "storage_deposit") 173 | .args(b"{}".to_vec()) 174 | .max_gas() 175 | .deposit(NearToken::from_near(5)) 176 | .transact() 177 | .await? 178 | .into_result()?; 179 | 180 | // The expected storage balance should be the minimal deposit, 181 | // the balance of the account should be reduced by the deposit, 182 | // and the contract should gain the deposit. 183 | // 184 | let storage_balance_bounds: StorageBalanceOf = ft_contract 185 | .call("storage_balance_of") 186 | .args_json(near_sdk::serde_json::json!({"account_id": new_account.id()})) 187 | .view() 188 | .await? 189 | .json()?; 190 | assert_eq!( 191 | storage_balance_bounds.total, 192 | minimal_deposit.as_yoctonear().into() 193 | ); 194 | assert_eq!(storage_balance_bounds.available, 0.into()); 195 | 196 | let new_account_balance_diff = new_account_balance_before_deposit 197 | .saturating_sub(new_account.view_account().await?.balance); 198 | // new_account is charged the transaction fee, so it should loose a bit more than minimal_deposit 199 | assert!(new_account_balance_diff > minimal_deposit); 200 | assert!( 201 | new_account_balance_diff < minimal_deposit.saturating_add(NearToken::from_millinear(1)) 202 | ); 203 | 204 | let contract_balance_diff = ft_contract 205 | .view_account() 206 | .await? 207 | .balance 208 | .saturating_sub(contract_balance_before_deposit); 209 | // contract receives a gas rewards for the function call, so the difference should be slightly more than minimal_deposit 210 | assert!(contract_balance_diff > minimal_deposit); 211 | assert!( 212 | contract_balance_diff 213 | < minimal_deposit.saturating_add(NearToken::from_yoctonear(50_000_000_000_000_000_000)) 214 | ); 215 | 216 | Ok(()) 217 | } 218 | 219 | #[tokio::test] 220 | async fn close_account_empty_balance() -> anyhow::Result<()> { 221 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 222 | 223 | let worker = near_workspaces::sandbox().await?; 224 | let root = worker.root_account()?; 225 | let (alice, _, _, _) = init_accounts(&root).await?; 226 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 227 | 228 | let res = alice 229 | .call(ft_contract.id(), "storage_unregister") 230 | .args_json((Option::::None,)) 231 | .max_gas() 232 | .deposit(ONE_YOCTO) 233 | .transact() 234 | .await?; 235 | assert!(res.json::()?); 236 | 237 | Ok(()) 238 | } 239 | 240 | #[tokio::test] 241 | async fn close_account_non_empty_balance() -> anyhow::Result<()> { 242 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 243 | 244 | let worker = near_workspaces::sandbox().await?; 245 | let root = worker.root_account()?; 246 | let (alice, _, _, _) = init_accounts(&root).await?; 247 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 248 | 249 | let res = ft_contract 250 | .call("storage_unregister") 251 | .args_json((Option::::None,)) 252 | .max_gas() 253 | .deposit(ONE_YOCTO) 254 | .transact() 255 | .await?; 256 | assert!(format!("{:?}", res) 257 | .contains("Can't unregister the account with the positive balance without force")); 258 | 259 | let res = ft_contract 260 | .call("storage_unregister") 261 | .args_json((Some(false),)) 262 | .max_gas() 263 | .deposit(ONE_YOCTO) 264 | .transact() 265 | .await?; 266 | assert!(format!("{:?}", res) 267 | .contains("Can't unregister the account with the positive balance without force")); 268 | 269 | Ok(()) 270 | } 271 | 272 | #[tokio::test] 273 | async fn close_account_force_non_empty_balance() -> anyhow::Result<()> { 274 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 275 | 276 | let worker = near_workspaces::sandbox().await?; 277 | let root = worker.root_account()?; 278 | let (alice, _, _, _) = init_accounts(&root).await?; 279 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 280 | 281 | let res = ft_contract 282 | .call("storage_unregister") 283 | .args_json((Some(true),)) 284 | .max_gas() 285 | .deposit(ONE_YOCTO) 286 | .transact() 287 | .await?; 288 | assert!(res.is_success()); 289 | 290 | let res = ft_contract.call("ft_total_supply").view().await?; 291 | assert_eq!(res.json::()?.0, 0); 292 | 293 | Ok(()) 294 | } 295 | -------------------------------------------------------------------------------- /tests/supply.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use near_sdk::{json_types::U128, NearToken}; 4 | 5 | use common::{init_accounts, init_contracts}; 6 | 7 | #[tokio::test] 8 | async fn test_total_supply() -> anyhow::Result<()> { 9 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 10 | 11 | let worker = near_workspaces::sandbox().await?; 12 | let root = worker.root_account()?; 13 | let (alice, _, _, _) = init_accounts(&root).await?; 14 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 15 | 16 | let res = ft_contract.call("ft_total_supply").view().await?; 17 | assert_eq!(res.json::()?, initial_balance); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /tests/transfer.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | use near_sdk::{json_types::U128, NearToken}; 4 | use near_workspaces::{operations::Function, result::ValueOrReceiptId}; 5 | 6 | use common::{init_accounts, init_contracts, register_user, ONE_YOCTO}; 7 | 8 | #[tokio::test] 9 | async fn simple_transfer() -> anyhow::Result<()> { 10 | // Create balance variables 11 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 12 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 13 | 14 | let worker = near_workspaces::sandbox().await?; 15 | let root = worker.root_account()?; 16 | let (alice, _, _, _) = init_accounts(&root).await?; 17 | let (ft_contract, _) = init_contracts(&worker, initial_balance, &alice).await?; 18 | 19 | let res = ft_contract 20 | .call("ft_transfer") 21 | .args_json((alice.id(), transfer_amount, Option::::None)) 22 | .max_gas() 23 | .deposit(ONE_YOCTO) 24 | .transact() 25 | .await?; 26 | assert!(res.is_success()); 27 | 28 | let ft_contract_balance = ft_contract 29 | .call("ft_balance_of") 30 | .args_json((ft_contract.id(),)) 31 | .view() 32 | .await? 33 | .json::()?; 34 | let alice_balance = ft_contract 35 | .call("ft_balance_of") 36 | .args_json((alice.id(),)) 37 | .view() 38 | .await? 39 | .json::()?; 40 | assert_eq!(initial_balance.0 - transfer_amount.0, ft_contract_balance.0); 41 | assert_eq!(transfer_amount.0, alice_balance.0); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[tokio::test] 47 | async fn transfer_call_with_burned_amount() -> anyhow::Result<()> { 48 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 49 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 50 | 51 | let worker = near_workspaces::sandbox().await?; 52 | let root = worker.root_account()?; 53 | let (alice, _, _, _) = init_accounts(&root).await?; 54 | let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; 55 | 56 | // defi contract must be registered as a FT account 57 | register_user(&ft_contract, defi_contract.id()).await?; 58 | 59 | // root invests in defi by calling `ft_transfer_call` 60 | let res = ft_contract 61 | .batch() 62 | .call( 63 | Function::new("ft_transfer_call") 64 | .args_json(( 65 | defi_contract.id(), 66 | transfer_amount, 67 | Option::::None, 68 | "10", 69 | )) 70 | .deposit(ONE_YOCTO) 71 | .gas(near_sdk::Gas::from_tgas(150)), 72 | ) 73 | .call( 74 | Function::new("storage_unregister") 75 | .args_json((Some(true),)) 76 | .deposit(ONE_YOCTO) 77 | .gas(near_sdk::Gas::from_tgas(150)), 78 | ) 79 | .transact() 80 | .await?; 81 | assert!(res.is_success()); 82 | 83 | let logs = res.logs(); 84 | let expected = format!("Account @{} burned {}", ft_contract.id(), 10); 85 | assert!(logs.len() >= 2); 86 | assert!(logs.contains(&"The account of the sender was deleted")); 87 | assert!(logs.contains(&(expected.as_str()))); 88 | 89 | match res.receipt_outcomes()[5].clone().into_result()? { 90 | ValueOrReceiptId::Value(val) => { 91 | let used_amount = val.json::()?; 92 | assert_eq!(used_amount, transfer_amount); 93 | } 94 | _ => panic!("Unexpected receipt id"), 95 | } 96 | assert!(res.json::()?); 97 | 98 | let res = ft_contract.call("ft_total_supply").view().await?; 99 | assert_eq!(res.json::()?.0, transfer_amount.0 - 10); 100 | let defi_balance = ft_contract 101 | .call("ft_balance_of") 102 | .args_json((defi_contract.id(),)) 103 | .view() 104 | .await? 105 | .json::()?; 106 | assert_eq!(defi_balance.0, transfer_amount.0 - 10); 107 | 108 | Ok(()) 109 | } 110 | 111 | #[tokio::test] 112 | async fn transfer_call_with_immediate_return_and_no_refund() -> anyhow::Result<()> { 113 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 114 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 115 | 116 | let worker = near_workspaces::sandbox().await?; 117 | let root = worker.root_account()?; 118 | let (alice, _, _, _) = init_accounts(&root).await?; 119 | let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; 120 | 121 | // defi contract must be registered as a FT account 122 | register_user(&ft_contract, defi_contract.id()).await?; 123 | 124 | // root invests in defi by calling `ft_transfer_call` 125 | let res = ft_contract 126 | .call("ft_transfer_call") 127 | .args_json(( 128 | defi_contract.id(), 129 | transfer_amount, 130 | Option::::None, 131 | "take-my-money", 132 | )) 133 | .max_gas() 134 | .deposit(ONE_YOCTO) 135 | .transact() 136 | .await?; 137 | assert!(res.is_success()); 138 | 139 | let root_balance = ft_contract 140 | .call("ft_balance_of") 141 | .args_json((ft_contract.id(),)) 142 | .view() 143 | .await? 144 | .json::()?; 145 | let defi_balance = ft_contract 146 | .call("ft_balance_of") 147 | .args_json((defi_contract.id(),)) 148 | .view() 149 | .await? 150 | .json::()?; 151 | assert_eq!(initial_balance.0 - transfer_amount.0, root_balance.0); 152 | assert_eq!(transfer_amount.0, defi_balance.0); 153 | 154 | Ok(()) 155 | } 156 | 157 | #[tokio::test] 158 | async fn transfer_call_when_called_contract_not_registered_with_ft() -> anyhow::Result<()> { 159 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 160 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 161 | 162 | let worker = near_workspaces::sandbox().await?; 163 | let root = worker.root_account()?; 164 | let (alice, _, _, _) = init_accounts(&root).await?; 165 | let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; 166 | 167 | // call fails because DEFI contract is not registered as FT user 168 | let res = ft_contract 169 | .call("ft_transfer_call") 170 | .args_json(( 171 | defi_contract.id(), 172 | transfer_amount, 173 | Option::::None, 174 | "take-my-money", 175 | )) 176 | .max_gas() 177 | .deposit(ONE_YOCTO) 178 | .transact() 179 | .await?; 180 | assert!(res.is_failure()); 181 | 182 | // balances remain unchanged 183 | let root_balance = ft_contract 184 | .call("ft_balance_of") 185 | .args_json((ft_contract.id(),)) 186 | .view() 187 | .await? 188 | .json::()?; 189 | let defi_balance = ft_contract 190 | .call("ft_balance_of") 191 | .args_json((defi_contract.id(),)) 192 | .view() 193 | .await? 194 | .json::()?; 195 | assert_eq!(initial_balance.0, root_balance.0); 196 | assert_eq!(0, defi_balance.0); 197 | 198 | Ok(()) 199 | } 200 | 201 | #[tokio::test] 202 | async fn transfer_call_with_promise_and_refund() -> anyhow::Result<()> { 203 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 204 | let refund_amount = U128::from(NearToken::from_near(50).as_yoctonear()); 205 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 206 | 207 | let worker = near_workspaces::sandbox().await?; 208 | let root = worker.root_account()?; 209 | let (alice, _, _, _) = init_accounts(&root).await?; 210 | let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; 211 | 212 | // defi contract must be registered as a FT account 213 | register_user(&ft_contract, defi_contract.id()).await?; 214 | 215 | let res = ft_contract 216 | .call("ft_transfer_call") 217 | .args_json(( 218 | defi_contract.id(), 219 | transfer_amount, 220 | Option::::None, 221 | refund_amount.0.to_string(), 222 | )) 223 | .max_gas() 224 | .deposit(ONE_YOCTO) 225 | .transact() 226 | .await?; 227 | assert!(res.is_success()); 228 | 229 | let root_balance = ft_contract 230 | .call("ft_balance_of") 231 | .args_json((ft_contract.id(),)) 232 | .view() 233 | .await? 234 | .json::()?; 235 | let defi_balance = ft_contract 236 | .call("ft_balance_of") 237 | .args_json((defi_contract.id(),)) 238 | .view() 239 | .await? 240 | .json::()?; 241 | assert_eq!( 242 | initial_balance.0 - transfer_amount.0 + refund_amount.0, 243 | root_balance.0 244 | ); 245 | assert_eq!(transfer_amount.0 - refund_amount.0, defi_balance.0); 246 | 247 | Ok(()) 248 | } 249 | 250 | #[tokio::test] 251 | async fn transfer_call_promise_panics_for_a_full_refund() -> anyhow::Result<()> { 252 | let initial_balance = U128::from(NearToken::from_near(10000).as_yoctonear()); 253 | let transfer_amount = U128::from(NearToken::from_near(100).as_yoctonear()); 254 | let worker = near_workspaces::sandbox().await?; 255 | let root = worker.root_account()?; 256 | let (alice, _, _, _) = init_accounts(&root).await?; 257 | let (ft_contract, defi_contract) = init_contracts(&worker, initial_balance, &alice).await?; 258 | 259 | // defi contract must be registered as a FT account 260 | register_user(&ft_contract, defi_contract.id()).await?; 261 | 262 | // root invests in defi by calling `ft_transfer_call` 263 | let res = ft_contract 264 | .call("ft_transfer_call") 265 | .args_json(( 266 | defi_contract.id(), 267 | transfer_amount, 268 | Option::::None, 269 | "no parsey as integer big panic oh no".to_string(), 270 | )) 271 | .max_gas() 272 | .deposit(ONE_YOCTO) 273 | .transact() 274 | .await?; 275 | assert!(res.is_success()); 276 | 277 | let promise_failures = res.receipt_failures(); 278 | assert_eq!(promise_failures.len(), 1); 279 | let failure = promise_failures[0].clone().into_result(); 280 | if let Err(err) = failure { 281 | assert!(format!("{:?}", err).contains("ParseIntError")); 282 | } else { 283 | unreachable!(); 284 | } 285 | 286 | // balances remain unchanged 287 | let root_balance = ft_contract 288 | .call("ft_balance_of") 289 | .args_json((ft_contract.id(),)) 290 | .view() 291 | .await? 292 | .json::()?; 293 | let defi_balance = ft_contract 294 | .call("ft_balance_of") 295 | .args_json((defi_contract.id(),)) 296 | .view() 297 | .await? 298 | .json::()?; 299 | assert_eq!(initial_balance, root_balance); 300 | assert_eq!(0, defi_balance.0); 301 | 302 | Ok(()) 303 | } 304 | --------------------------------------------------------------------------------