├── .gitignore ├── Makefile ├── Move.toml ├── README.md ├── resource.drawio └── sources └── token_mint.move /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | **/build/* 12 | .vscode/* 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | Mod = 0x833781e93f9b2abf507a113d517290aed99befe1d450cbb15b73c65337292222 4 | aptos = ~/bin/aptos 5 | 6 | build: 7 | ${aptos} move compile --package-dir ./ --named-addresses aptosx=${Mod} 8 | 9 | .PHONY: test 10 | test: 11 | ${aptos} move test --package-dir ./ --named-addresses aptosx=0xCAFE 12 | 13 | .PHONY: publish 14 | publish: 15 | ${aptos} move publish --named-addresses aptosx=${Mod} -------------------------------------------------------------------------------- /Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aptosx-liquid-token" 3 | version = "0.1.0" 4 | 5 | [dependencies.AptosFramework] 6 | git = 'https://github.com/aptos-labs/aptos-core.git' 7 | rev = 'devnet' 8 | subdir = 'aptos-move/framework/aptos-framework' 9 | 10 | [addresses] 11 | std = "0x1" 12 | aptos_std = "0x1" 13 | aptos_framework = "0x1" 14 | core_resources = "0xA550C18" 15 | vm_reserved = "0x0" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aptos-liquid-token 2 | 3 | # How to build: 4 | 5 | * make build 6 | 7 | # How to test: 8 | 9 | * make test 10 | 11 | # Current flow: 12 | 13 | 1. Stader initialize aptosXCoin 14 | 2. User stake Aptoscoin => stader mint AptosX 15 | 3. User unstake => user burn AptosX, stader transfer back AptosCoins 16 | 17 | # Devnet deployment: 18 | 19 | 1. Generate an account 20 | 2. Setup aptos-cli to newly generate account 21 | 3. Change `Mod` variable in Makefile to public address of new account 22 | 4. `make publish` 23 | 24 | ## Devnet module: 25 | 26 | commit: `6e42279522b0407e68685b5d2c5ba81e58883920` 27 | https://explorer.devnet.aptos.dev/account/0x833781e93f9b2abf507a113d517290aed99befe1d450cbb15b73c65337292222 28 | -------------------------------------------------------------------------------- /resource.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /sources/token_mint.move: -------------------------------------------------------------------------------- 1 | module aptosx::token_mint { 2 | use std::string; 3 | use std::error; 4 | use std::signer; 5 | use std::simple_map; 6 | use std::option; 7 | 8 | use aptos_framework::aptos_coin::{Self}; 9 | use aptos_framework::coin::{Self, BurnCapability, FreezeCapability, MintCapability}; 10 | 11 | const EINVALID_BALANCE: u64 = 0; 12 | const EACCOUNT_DOESNT_EXIST: u64 = 1; 13 | const ENO_CAPABILITIES: u64 = 2; 14 | const ENOT_APTOSX_ADDRESS: u64 = 3; 15 | 16 | 17 | const STAKE_VAULT_SEED: vector = b"aptosx::token_mint::stake_vault"; 18 | use aptos_framework::account; 19 | 20 | // every user stake have this resource 21 | struct UserStakeInfo has key { 22 | amount: u64, 23 | } 24 | 25 | // One stake vault for all user, used for recieve Aptoscoin 26 | struct StakeVault has key { 27 | resource_addr: address, 28 | signer_cap: account::SignerCapability 29 | } 30 | 31 | struct ValidatorSet has key { 32 | validators: simple_map::SimpleMap, 33 | } 34 | 35 | // 36 | // Data structures 37 | // 38 | 39 | /// Capabilities resource storing mint and burn capabilities. 40 | /// The resource is stored on the account that initialized coin `CoinType`. 41 | struct Capabilities has key { 42 | burn_cap: BurnCapability, 43 | freeze_cap: FreezeCapability, 44 | mint_cap: MintCapability, 45 | } 46 | 47 | struct AptosXCoin {} 48 | 49 | public entry fun initialize( 50 | account: &signer, 51 | decimals: u8, 52 | ) { 53 | let (burn_cap, freeze_cap, mint_cap) = coin::initialize( 54 | account, 55 | string::utf8(b"AptosX Liquid Token"), 56 | string::utf8(b"APTX"), 57 | decimals, 58 | true, 59 | ); 60 | 61 | move_to(account, ValidatorSet { 62 | validators: simple_map::create(), 63 | }); 64 | 65 | 66 | move_to(account, Capabilities { 67 | burn_cap, 68 | freeze_cap, 69 | mint_cap, 70 | }); 71 | 72 | // Create stake_vault resource 73 | let (stake_vault, signer_cap) = account::create_resource_account(account, STAKE_VAULT_SEED); 74 | let resource_addr = signer::address_of(&stake_vault); 75 | coin::register(&stake_vault); 76 | let stake_info = StakeVault { 77 | resource_addr, 78 | signer_cap 79 | }; 80 | move_to(account, stake_info); 81 | } 82 | 83 | public fun is_aptosx_address(addr: address): bool { 84 | addr == @aptosx 85 | } 86 | 87 | public entry fun add_validator(account: &signer, validator_address: address) acquires ValidatorSet { 88 | assert!( 89 | is_aptosx_address(signer::address_of(account)), 90 | error::permission_denied(ENOT_APTOSX_ADDRESS), 91 | ); 92 | 93 | let validator_set = borrow_global_mut(@aptosx); 94 | simple_map::add(&mut validator_set.validators, validator_address, true); 95 | } 96 | 97 | public entry fun remove_validator(account: &signer, validator_address: address) acquires ValidatorSet { 98 | assert!( 99 | is_aptosx_address(signer::address_of(account)), 100 | error::permission_denied(ENOT_APTOSX_ADDRESS), 101 | ); 102 | let validator_set = borrow_global_mut(@aptosx); 103 | 104 | simple_map::remove(&mut validator_set.validators, &validator_address ); 105 | } 106 | 107 | public entry fun deposit(staker: &signer, amount: u64) acquires UserStakeInfo, Capabilities, StakeVault { 108 | let staker_addr = signer::address_of(staker); 109 | 110 | 111 | if (!exists(staker_addr)) { 112 | let stake_info = UserStakeInfo { 113 | amount: 0, 114 | }; 115 | move_to(staker, stake_info); 116 | }; 117 | 118 | let resource_addr = borrow_global(@aptosx).resource_addr; 119 | 120 | if (!coin::is_account_registered(staker_addr)) { 121 | coin::register(staker); 122 | }; 123 | 124 | // Transfer AptosCoin to vault 125 | let stake_info = borrow_global_mut(staker_addr); 126 | coin::transfer(staker, resource_addr, amount); 127 | stake_info.amount = stake_info.amount + amount; 128 | 129 | 130 | // Mint Aptosx 131 | let mod_account = @aptosx; 132 | assert!( 133 | exists(mod_account), 134 | error::not_found(ENO_CAPABILITIES), 135 | ); 136 | let capabilities = borrow_global(mod_account); 137 | let coins_minted = coin::mint(amount, &capabilities.mint_cap); 138 | coin::deposit(staker_addr, coins_minted); 139 | } 140 | 141 | public entry fun withdraw(staker: &signer, amount: u64) acquires UserStakeInfo, Capabilities, StakeVault { 142 | let staker_addr = signer::address_of(staker); 143 | assert!(exists(staker_addr), EACCOUNT_DOESNT_EXIST); 144 | 145 | let stake_info = borrow_global_mut(staker_addr); 146 | assert!(stake_info.amount >= amount, EINVALID_BALANCE); 147 | 148 | stake_info.amount = stake_info.amount - amount; 149 | 150 | // Transfer AptosCoin to user from vault 151 | let vault = borrow_global(@aptosx); 152 | let resource_account = account::create_signer_with_capability(&vault.signer_cap); 153 | coin::transfer(&resource_account, staker_addr, amount); 154 | 155 | // Burn aptosx 156 | let coin = coin::withdraw(staker, amount); 157 | let mod_account = @aptosx; 158 | assert!( 159 | exists(mod_account), 160 | error::not_found(ENO_CAPABILITIES), 161 | ); 162 | let capabilities = borrow_global(mod_account); 163 | coin::burn(coin, &capabilities.burn_cap); 164 | } 165 | 166 | // 167 | // Tests 168 | // 169 | #[test(staker = @0xa11ce, mod_account = @0xCAFE, core = @std)] 170 | public entry fun end_to_end_deposit( 171 | staker: signer, 172 | mod_account: signer, 173 | core: signer, 174 | ) acquires Capabilities, UserStakeInfo, StakeVault { 175 | let staker_addr = signer::address_of(&staker); 176 | account::create_account_for_test(staker_addr); 177 | 178 | initialize( 179 | &mod_account, 180 | 10 181 | ); 182 | assert!(coin::is_coin_initialized(), 0); 183 | 184 | 185 | coin::register(&staker); 186 | 187 | let amount = 100; 188 | let (burn_cap, mint_cap) = aptos_coin::initialize_for_test(&core); 189 | coin::deposit(staker_addr, coin::mint(amount, &mint_cap)); 190 | 191 | // Before deposit 192 | assert!(coin::balance(staker_addr) == amount, 1); 193 | assert!(coin::is_account_registered(staker_addr) == false, 3); 194 | 195 | deposit(&staker, amount); 196 | 197 | // After deposit 198 | assert!(coin::balance(staker_addr) == 0, 5); 199 | assert!(coin::balance(staker_addr) == amount, 6); 200 | assert!(coin::supply() == option::some((amount as u128)), 7); 201 | 202 | 203 | withdraw(&staker, amount); 204 | 205 | // // After withdraw 206 | assert!(coin::balance(staker_addr) == amount, 8); 207 | assert!(coin::balance(staker_addr) == 0, 9); 208 | assert!(coin::supply() == option::some(0), 10); 209 | 210 | coin::destroy_burn_cap(burn_cap); 211 | coin::destroy_mint_cap(mint_cap); 212 | } 213 | 214 | #[test(staker = @0xa11ce, mod_account = @0xCAFE, validator_1 = @0x1001, validator_2 = @0x1002, validator_3 = @0x1003)] 215 | public entry fun validators( 216 | mod_account: signer, 217 | validator_1: address, 218 | validator_2: address, 219 | ) acquires ValidatorSet { 220 | initialize( 221 | &mod_account, 222 | 10 223 | ); 224 | 225 | add_validator(&mod_account, validator_1); 226 | add_validator(&mod_account, validator_2); 227 | remove_validator(&mod_account, validator_2); 228 | } 229 | 230 | 231 | #[test(mod_account = @0xCAFE, validator_1 = @0x1001)] 232 | #[expected_failure] 233 | public entry fun remove_validator_not_exist( 234 | mod_account: signer, 235 | validator_1: address, 236 | ) acquires ValidatorSet { 237 | initialize( 238 | &mod_account, 239 | 10 240 | ); 241 | 242 | remove_validator(&mod_account, validator_1); 243 | } 244 | 245 | 246 | #[test(mod_account = @0xCAFE, validator_1 = @0x1001)] 247 | #[expected_failure] 248 | public entry fun remove_validator_twice( 249 | mod_account: signer, 250 | validator_1: address, 251 | ) acquires ValidatorSet { 252 | initialize( 253 | &mod_account, 254 | 10 255 | ); 256 | add_validator(&mod_account, validator_1); 257 | remove_validator(&mod_account, validator_1); 258 | remove_validator(&mod_account, validator_1); 259 | } 260 | 261 | } --------------------------------------------------------------------------------