|
| 1 | +module VaultTest::Vault { |
| 2 | + |
| 3 | + use std::signer; |
| 4 | + use std::error; |
| 5 | + use std::bcs; |
| 6 | + use std::vector; |
| 7 | + use aptos_std::type_info; |
| 8 | + use aptos_framework::coin; |
| 9 | + use aptos_framework::account; |
| 10 | + |
| 11 | + /* Errors. */ |
| 12 | + const EAPP_NOT_INITIALIZED: u64 = 0; |
| 13 | + const EVAULT_NOT_EXISTS: u64 = 1; |
| 14 | + const EINVALID_BALANCE: u64 = 2; |
| 15 | + const EINVALID_VALUE: u64 = 3; |
| 16 | + const EINVALID_DEDICATED_INITIALIZER: u64 = 4; |
| 17 | + const EINVALID_ADMIN: u64 = 5; |
| 18 | + const EINVALID_COIN: u64 = 6; |
| 19 | + const EAPP_IS_PAUSED: u64 = 7; |
| 20 | + |
| 21 | + /* Constants. */ |
| 22 | + const APP_INFO_SEED: vector<u8> = b"APP_INFO_SEED"; |
| 23 | + const VAULT_SEED: vector<u8> = b"VAULT_SEED"; |
| 24 | + |
| 25 | + /* data structures. */ |
| 26 | + struct AppInfo has key, store { |
| 27 | + admin_addr: address, |
| 28 | + is_paused: u8, |
| 29 | + } |
| 30 | + |
| 31 | + struct VaultInfo has key, store { |
| 32 | + amount: u64, |
| 33 | + deposit_coin_addr: address, |
| 34 | + signer_cap: account::SignerCapability, |
| 35 | + } |
| 36 | + |
| 37 | + /* entry functions */ |
| 38 | + public entry fun initialize_app(initializer: &signer, admin_addr: address) { |
| 39 | + let initializer_addr = signer::address_of(initializer); |
| 40 | + assert!(initializer_addr == @VaultTest, error::permission_denied(EINVALID_DEDICATED_INITIALIZER)); |
| 41 | + // pool is derived from contract address |
| 42 | + let (app, _) = account::create_resource_account(initializer, APP_INFO_SEED); |
| 43 | + move_to<AppInfo>(&app, AppInfo { |
| 44 | + admin_addr, |
| 45 | + is_paused: 0, |
| 46 | + }); |
| 47 | + } |
| 48 | + |
| 49 | + public entry fun deposit<CoinType>(account: &signer, amount: u64) acquires VaultInfo, AppInfo { |
| 50 | + |
| 51 | + let app_addr = account::create_resource_address(&@VaultTest, APP_INFO_SEED); |
| 52 | + // check if app exists |
| 53 | + assert!(exists<AppInfo>(app_addr), error::not_found(EAPP_NOT_INITIALIZED)); |
| 54 | + |
| 55 | + let account_addr = signer::address_of(account); |
| 56 | + let app_info = borrow_global_mut<AppInfo>(app_addr); |
| 57 | + assert!(app_info.is_paused == 0, error::permission_denied(EAPP_IS_PAUSED)); |
| 58 | + |
| 59 | + let coin_addr = coin_address<CoinType>(); |
| 60 | + |
| 61 | + let vault_addr = account::create_resource_address(&account_addr, seed_with_address(coin_addr, VAULT_SEED)); |
| 62 | + if (!exists<VaultInfo>(vault_addr)) { |
| 63 | + // if it is first deposit, move VaultInfo resource to account |
| 64 | + let (vault, vault_signer_cap) = account::create_resource_account(account, seed_with_address(coin_addr, VAULT_SEED)); |
| 65 | + move_to<VaultInfo>(&vault, VaultInfo { |
| 66 | + amount, |
| 67 | + deposit_coin_addr: coin_addr, |
| 68 | + signer_cap: vault_signer_cap |
| 69 | + }); |
| 70 | + coin::register<CoinType>(&vault); |
| 71 | + } else { |
| 72 | + // if already deposited, then update vault_info |
| 73 | + let vault_info = borrow_global_mut<VaultInfo>(vault_addr); |
| 74 | + vault_info.amount = vault_info.amount + amount; |
| 75 | + |
| 76 | + check_coin_type<CoinType>(vault_info.deposit_coin_addr); |
| 77 | + }; |
| 78 | + |
| 79 | + // deposit coin to vault |
| 80 | + coin::transfer<CoinType>(account, vault_addr, amount); |
| 81 | + } |
| 82 | + |
| 83 | + public entry fun withdraw<CoinType>(account: &signer, amount: u64) acquires VaultInfo, AppInfo { |
| 84 | + |
| 85 | + let account_addr = signer::address_of(account); |
| 86 | + let app_addr = account::create_resource_address(&@VaultTest, APP_INFO_SEED); |
| 87 | + // check if app exists |
| 88 | + assert!(exists<AppInfo>(app_addr), error::not_found(EAPP_NOT_INITIALIZED)); |
| 89 | + |
| 90 | + let coin_addr = coin_address<CoinType>(); |
| 91 | + let vault_addr = account::create_resource_address(&account_addr, seed_with_address(coin_addr, VAULT_SEED)); |
| 92 | + assert!(exists<VaultInfo>(vault_addr), error::not_found(EVAULT_NOT_EXISTS)); |
| 93 | + |
| 94 | + // update user's stake amount in stakeInfo |
| 95 | + let vault_info = borrow_global_mut<VaultInfo>(vault_addr); |
| 96 | + assert!(amount <= vault_info.amount, error::invalid_argument(EINVALID_VALUE)); |
| 97 | + vault_info.amount = vault_info.amount - amount; |
| 98 | + |
| 99 | + // check if app is paused |
| 100 | + let app_info = borrow_global_mut<AppInfo>(app_addr); |
| 101 | + assert!(app_info.is_paused == 0, error::permission_denied(EAPP_IS_PAUSED)); |
| 102 | + |
| 103 | + // check coin type |
| 104 | + check_coin_type<CoinType>(vault_info.deposit_coin_addr); |
| 105 | + |
| 106 | + // transfer to user |
| 107 | + let vault_account_from_cap = account::create_signer_with_capability(&vault_info.signer_cap); |
| 108 | + coin::transfer<CoinType>(&vault_account_from_cap, account_addr, amount); |
| 109 | + } |
| 110 | + |
| 111 | + public entry fun pause(account: &signer) acquires AppInfo { |
| 112 | + let app_addr = account::create_resource_address(&@VaultTest, APP_INFO_SEED); |
| 113 | + // check if app exists |
| 114 | + assert!(exists<AppInfo>(app_addr), error::not_found(EAPP_NOT_INITIALIZED)); |
| 115 | + |
| 116 | + let app_info = borrow_global_mut<AppInfo>(app_addr); |
| 117 | + |
| 118 | + // check if account is admin |
| 119 | + let account_addr = signer::address_of(account); |
| 120 | + assert!(app_info.admin_addr == account_addr, error::permission_denied(EINVALID_ADMIN)); |
| 121 | + |
| 122 | + // resume the app |
| 123 | + app_info.is_paused = 1; |
| 124 | + } |
| 125 | + |
| 126 | + public entry fun unpause(account: &signer) acquires AppInfo { |
| 127 | + let app_addr = account::create_resource_address(&@VaultTest, APP_INFO_SEED); |
| 128 | + // check if app exists |
| 129 | + assert!(exists<AppInfo>(app_addr), error::not_found(EAPP_NOT_INITIALIZED)); |
| 130 | + |
| 131 | + let app_info = borrow_global_mut<AppInfo>(app_addr); |
| 132 | + |
| 133 | + // check if account is admin |
| 134 | + let account_addr = signer::address_of(account); |
| 135 | + assert!(app_info.admin_addr == account_addr, error::permission_denied(EINVALID_ADMIN)); |
| 136 | + |
| 137 | + // resume the app |
| 138 | + app_info.is_paused = 0; |
| 139 | + } |
| 140 | + |
| 141 | + |
| 142 | + /* private functions*/ |
| 143 | + |
| 144 | + /// function to get mixed seeds (addr + seed) |
| 145 | + fun seed_with_address(addr: address, seed: vector<u8>): vector<u8> { |
| 146 | + let bytes = bcs::to_bytes(&addr); |
| 147 | + vector::append(&mut bytes, seed); |
| 148 | + bytes |
| 149 | + } |
| 150 | + |
| 151 | + /// function to check if coin is stakable coin |
| 152 | + fun check_coin_type<CoinType>(coin_addr: address) { |
| 153 | + assert!(coin_addr == type_info::account_address(&type_info::type_of<CoinType>()), error::invalid_argument(EINVALID_COIN)); |
| 154 | + } |
| 155 | + |
| 156 | + /// A helper function that returns the address of CoinType. |
| 157 | + fun coin_address<CoinType>(): address { |
| 158 | + let type_info = type_info::type_of<CoinType>(); |
| 159 | + type_info::account_address(&type_info) |
| 160 | + } |
| 161 | + |
| 162 | + /* Here are tests. */ |
| 163 | + |
| 164 | + #[test_only] |
| 165 | + struct CoinA {} |
| 166 | + |
| 167 | + #[test_only] |
| 168 | + struct CoinB {} |
| 169 | + |
| 170 | + #[test_only] |
| 171 | + use aptos_framework::managed_coin; |
| 172 | + |
| 173 | + #[test_only] |
| 174 | + use aptos_framework::aptos_account; |
| 175 | + |
| 176 | + #[test_only] |
| 177 | + public fun initialize_and_mint<CoinType>(authority: &signer, to: &signer, mint_amount: u64) { |
| 178 | + |
| 179 | + let to_addr = signer::address_of(to); |
| 180 | + managed_coin::initialize<CoinType>(authority, b"FakeCoinX", b"CoinX", 9, false); |
| 181 | + if (!account::exists_at(to_addr)) { |
| 182 | + aptos_account::create_account(to_addr); |
| 183 | + }; |
| 184 | + if (!coin::is_account_registered<CoinType>(to_addr)) { |
| 185 | + coin::register<CoinType>(to); |
| 186 | + }; |
| 187 | + managed_coin::mint<CoinType>(authority, to_addr, mint_amount); |
| 188 | + } |
| 189 | + |
| 190 | + #[test(fake_admin = @0x1234)] |
| 191 | + #[expected_failure(abort_code = 327684)] // EINVALID_DEDICATED_INITIALIZER |
| 192 | + public fun test_others_can_init(fake_admin: signer) { |
| 193 | + initialize_app(&fake_admin, signer::address_of(&fake_admin)); |
| 194 | + } |
| 195 | + |
| 196 | + #[test(alice = @0x1234, admin = @0x3333, initializer = @VaultTest)] |
| 197 | + #[expected_failure(abort_code = 327687)] // EAPP_IS_PAUSED |
| 198 | + public fun test_can_deposit_if_paused(alice: signer, admin: signer, initializer: signer) acquires AppInfo, VaultInfo { |
| 199 | + // init and mint coins |
| 200 | + initialize_and_mint<CoinA>(&initializer, &alice, 10000); |
| 201 | + |
| 202 | + // initialize app |
| 203 | + let admin_addr = signer::address_of(&admin); |
| 204 | + initialize_app(&initializer, admin_addr); |
| 205 | + |
| 206 | + // pause app |
| 207 | + pause(&admin); |
| 208 | + // try deposit but will be failed |
| 209 | + deposit<CoinA>(&alice, 500); |
| 210 | + } |
| 211 | + |
| 212 | + #[test(alice = @0x1234, admin = @0x3333, initializer = @VaultTest)] |
| 213 | + #[expected_failure(abort_code = 327687)] // EAPP_IS_PAUSED |
| 214 | + public fun test_can_withdraw_if_paused(alice: signer, admin: signer, initializer: signer) acquires AppInfo, VaultInfo { |
| 215 | + // init and mint coins |
| 216 | + initialize_and_mint<CoinA>(&initializer, &alice, 10000); |
| 217 | + |
| 218 | + // initialize app |
| 219 | + let admin_addr = signer::address_of(&admin); |
| 220 | + initialize_app(&initializer, admin_addr); |
| 221 | + |
| 222 | + // deposit 500 |
| 223 | + deposit<CoinA>(&alice, 500); |
| 224 | + |
| 225 | + // pause app |
| 226 | + pause(&admin); |
| 227 | + |
| 228 | + // try withdraw but will be failed |
| 229 | + withdraw<CoinA>(&alice, 500); |
| 230 | + } |
| 231 | + |
| 232 | + #[test(alice = @0x1234, admin = @0x3333, initializer = @VaultTest)] |
| 233 | + public fun test_can_resume(alice: signer, admin: signer, initializer: signer) acquires AppInfo, VaultInfo { |
| 234 | + // init and mint coins |
| 235 | + initialize_and_mint<CoinA>(&initializer, &alice, 10000); |
| 236 | + |
| 237 | + // initialize app |
| 238 | + let admin_addr = signer::address_of(&admin); |
| 239 | + initialize_app(&initializer, admin_addr); |
| 240 | + |
| 241 | + // deposit 500 |
| 242 | + deposit<CoinA>(&alice, 500); |
| 243 | + |
| 244 | + // pause app |
| 245 | + pause(&admin); |
| 246 | + |
| 247 | + // resume app |
| 248 | + unpause(&admin); |
| 249 | + |
| 250 | + // withdraw 500 |
| 251 | + withdraw<CoinA>(&alice, 500); |
| 252 | + } |
| 253 | + |
| 254 | + #[test(alice = @0x1234, bob = @0x2345, admin = @0x3333, initializer = @VaultTest)] |
| 255 | + public fun e2e_test(alice: signer, bob: signer, admin: signer, initializer: signer) acquires AppInfo, VaultInfo { |
| 256 | + let alice_addr = signer::address_of(&alice); |
| 257 | + let bob_addr = signer::address_of(&bob); |
| 258 | + |
| 259 | + // init and mint coins |
| 260 | + initialize_and_mint<CoinA>(&initializer, &alice, 10000); |
| 261 | + initialize_and_mint<CoinB>(&initializer, &bob, 10000); |
| 262 | + |
| 263 | + assert!(coin::balance<CoinA>(alice_addr) == 10000, error::invalid_argument(EINVALID_BALANCE)); |
| 264 | + assert!(coin::balance<CoinB>(bob_addr) == 10000, error::invalid_argument(EINVALID_BALANCE)); |
| 265 | + |
| 266 | + // initialize app |
| 267 | + let admin_addr = signer::address_of(&admin); |
| 268 | + initialize_app(&initializer, admin_addr); |
| 269 | + |
| 270 | + // alice deposit 500 CoinA |
| 271 | + deposit<CoinA>(&alice, 500); |
| 272 | + assert!(coin::balance<CoinA>(alice_addr) == 9500, error::invalid_argument(EINVALID_BALANCE)); |
| 273 | + |
| 274 | + let alice_coin_a_vault_addr = account::create_resource_address(&alice_addr, seed_with_address(coin_address<CoinA>(), VAULT_SEED)); |
| 275 | + assert!(coin::balance<CoinA>(alice_coin_a_vault_addr) == 500, error::invalid_argument(EINVALID_BALANCE)); |
| 276 | + |
| 277 | + let vault_info = borrow_global<VaultInfo>(alice_coin_a_vault_addr); |
| 278 | + assert!(vault_info.amount == 500, error::invalid_argument(EINVALID_BALANCE)); |
| 279 | + |
| 280 | + // alice withdraw 300 coinA |
| 281 | + withdraw<CoinA>(&alice, 300); |
| 282 | + assert!(coin::balance<CoinA>(alice_addr) == 9800, error::invalid_argument(EINVALID_BALANCE)); |
| 283 | + assert!(coin::balance<CoinA>(alice_coin_a_vault_addr) == 200, error::invalid_argument(EINVALID_BALANCE)); |
| 284 | + |
| 285 | + let vault_info = borrow_global<VaultInfo>(alice_coin_a_vault_addr); |
| 286 | + assert!(vault_info.amount == 200, error::invalid_argument(EINVALID_BALANCE)); |
| 287 | + |
| 288 | + // bob deposit 500 CoinA |
| 289 | + deposit<CoinB>(&bob, 500); |
| 290 | + assert!(coin::balance<CoinB>(bob_addr) == 9500, error::invalid_argument(EINVALID_BALANCE)); |
| 291 | + |
| 292 | + let bob_coin_b_vault_addr = account::create_resource_address(&bob_addr, seed_with_address(coin_address<CoinB>(), VAULT_SEED)); |
| 293 | + assert!(coin::balance<CoinB>(bob_coin_b_vault_addr) == 500, error::invalid_argument(EINVALID_BALANCE)); |
| 294 | + |
| 295 | + // bob withdraw 300 coinA |
| 296 | + withdraw<CoinB>(&bob, 300); |
| 297 | + assert!(coin::balance<CoinB>(bob_addr) == 9800, error::invalid_argument(EINVALID_BALANCE)); |
| 298 | + assert!(coin::balance<CoinB>(bob_coin_b_vault_addr) == 200, error::invalid_argument(EINVALID_BALANCE)); |
| 299 | + |
| 300 | + } |
| 301 | +} |
0 commit comments