├── .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 | }
--------------------------------------------------------------------------------