Skip to content

Commit c91007b

Browse files
authored
Merge pull request #13 from movementlabsxyz/add-pauser
Add pauser role to MOVE OFT Adapter contract
2 parents 347b126 + 529cc9a commit c91007b

2 files changed

Lines changed: 230 additions & 3 deletions

File tree

examples/oft-evm-move-adapters/sources/oft_implementation/move_oft_adapter.move

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
/// because there is not a rebalancing mechanism that can ensure pools maintain sufficient balance.
99
module oft::move_oft_adapter {
1010
use std::coin::Coin;
11+
use std::event::emit;
1112
use std::fungible_asset::{Self, FungibleAsset, Metadata};
1213
use std::object::{Self, address_to_object, ExtendRef, Object, object_exists};
1314
use std::option::{Self, Option};
15+
use std::vector;
1416
use std::primary_fungible_store;
1517
use std::signer::address_of;
18+
use aptos_framework::account;
1619

1720
use endpoint_v2_common::bytes32::Bytes32;
18-
use oft::oapp_core::{assert_admin, combine_options};
21+
use oft::oapp_core::{assert_admin, combine_options, get_admin};
1922
use oft::oapp_store::OAPP_ADDRESS;
2023
use oft::oft_core;
2124
use oft::oft_impl_config::{
@@ -39,6 +42,11 @@ module oft::move_oft_adapter {
3942
escrow_extend_ref: ExtendRef,
4043
}
4144

45+
// =================================================== Pauser Store ==================================================
46+
47+
/// Separate resource to keep upgrades safe (no change to OftImpl layout)
48+
struct PauserStore has key { pausers: vector<address>, paused: bool }
49+
4250
// ================================================= OFT Handlers =================================================
4351

4452
/// The default *credit* behavior for a Adapter OFT is to unlock the amount from escrow and credit the recipient
@@ -47,7 +55,9 @@ module oft::move_oft_adapter {
4755
amount_ld: u64,
4856
src_eid: u32,
4957
lz_receive_value: Option<FungibleAsset>,
50-
): u64 acquires OftImpl {
58+
): u64 acquires OftImpl, PauserStore {
59+
// Global inflow pause gate
60+
assert!(!is_paused(), EPAUSED);
5161
// Default implementation does not make special use of LZ Receive Value sent; just deposit to the OFT address
5262
option::for_each(lz_receive_value, |fa| primary_fungible_store::deposit(@oft_admin, fa));
5363

@@ -57,6 +67,9 @@ module oft::move_oft_adapter {
5767
// unlock the amount from escrow
5868
let escrow_signer = &object::generate_signer_for_extending(&store().escrow_extend_ref);
5969

70+
// Create recipient account if it doesn't exist
71+
account::create_account_if_does_not_exist(to);
72+
6073
// Deposit the extracted amount to the recipient, or redirect to the admin if the recipient is blocklisted
6174
primary_fungible_store::transfer(
6275
escrow_signer,
@@ -76,9 +89,11 @@ module oft::move_oft_adapter {
7689
fa: &mut FungibleAsset,
7790
min_amount_ld: u64,
7891
dst_eid: u32,
79-
): (u64, u64) acquires OftImpl {
92+
): (u64, u64) acquires OftImpl, PauserStore {
8093
assert_not_blocklisted(sender);
8194
assert_metadata(fa);
95+
// Global outflow pause gate
96+
assert!(!is_paused(), EPAUSED);
8297

8398
// Calculate the exact send amount
8499
let amount_ld = fungible_asset::amount(fa);
@@ -266,6 +281,65 @@ module oft::move_oft_adapter {
266281
oft_limit::new_oft_limit(0, oft_impl_config::rate_limit_capacity(eid))
267282
}
268283

284+
// ==================================================== Pauser Logic ===================================================
285+
286+
#[view]
287+
public fun is_paused(): bool acquires PauserStore {
288+
let admin_addr = get_admin();
289+
if (!exists<PauserStore>(admin_addr)) { return false };
290+
borrow_global<PauserStore>(admin_addr).paused
291+
}
292+
293+
/// Add a pauser address (admin-only). Creates the store at current admin address if missing.
294+
public entry fun set_pauser(admin: &signer, pauser: address) acquires PauserStore {
295+
let admin_addr = address_of(admin);
296+
assert_admin(admin_addr);
297+
if (exists<PauserStore>(admin_addr)) {
298+
let store_mut = borrow_global_mut<PauserStore>(admin_addr);
299+
if (!vector::contains(&store_mut.pausers, &pauser)) {
300+
vector::push_back(&mut store_mut.pausers, pauser);
301+
};
302+
} else {
303+
move_to<PauserStore>(admin, PauserStore { pausers: vector[pauser], paused: false });
304+
}
305+
}
306+
307+
/// Remove a pauser address (admin-only). No-op if the address is not present.
308+
public entry fun remove_pauser(admin: &signer, pauser: address) acquires PauserStore {
309+
let admin_addr = address_of(admin);
310+
assert_admin(admin_addr);
311+
assert!(exists<PauserStore>(admin_addr), ENOT_INITIALIZED);
312+
let store_mut = borrow_global_mut<PauserStore>(admin_addr);
313+
let i = 0u64;
314+
let n = vector::length(&store_mut.pausers);
315+
while (i < n) {
316+
if (*vector::borrow(&store_mut.pausers, i) == pauser) {
317+
vector::remove(&mut store_mut.pausers, i);
318+
return
319+
};
320+
i = i + 1;
321+
};
322+
}
323+
324+
/// Toggle pause (pauser or admin can call)
325+
public entry fun set_paused(caller: &signer, paused: bool) acquires PauserStore {
326+
let admin_addr = get_admin();
327+
assert!(exists<PauserStore>(admin_addr), ENOT_INITIALIZED);
328+
let caller_addr = address_of(caller);
329+
let store_mut = borrow_global_mut<PauserStore>(admin_addr);
330+
assert!(caller_addr == admin_addr || vector::contains(&store_mut.pausers, &caller_addr), EUNAUTHORIZED);
331+
assert!(store_mut.paused != paused, ENO_CHANGE);
332+
store_mut.paused = paused;
333+
emit(PauseSet { paused });
334+
}
335+
336+
#[view]
337+
public fun is_pauser(addr: address): bool acquires PauserStore {
338+
let admin_addr = get_admin();
339+
if (!exists<PauserStore>(admin_addr)) { return false };
340+
vector::contains(&borrow_global<PauserStore>(admin_addr).pausers, &addr)
341+
}
342+
269343
#[view]
270344
/// Total value locked in the contract
271345
public fun tvl(): u64 acquires OftImpl {
@@ -313,6 +387,10 @@ module oft::move_oft_adapter {
313387
init_module(&std::account::create_signer_for_test(OAPP_ADDRESS()));
314388
}
315389

390+
// ================================================ Tests Helpers =================================================
391+
#[test_only]
392+
public fun pause_set_event(paused: bool): PauseSet { PauseSet { paused } }
393+
316394
// ================================================ Storage Helpers ===============================================
317395

318396
#[view]
@@ -333,4 +411,13 @@ module oft::move_oft_adapter {
333411
const EINVALID_METADATA_ADDRESS: u64 = 1;
334412
const ENOT_IMPLEMENTED: u64 = 2;
335413
const EWRONG_FA_METADATA: u64 = 3;
414+
const EPAUSED: u64 = 4;
415+
const EUNAUTHORIZED: u64 = 5;
416+
const ENO_CHANGE: u64 = 6;
417+
const ENOT_INITIALIZED: u64 = 7;
418+
419+
// ==================================================== Events ====================================================
420+
421+
#[event]
422+
struct PauseSet has store, drop { paused: bool }
336423
}

examples/oft-evm-move-adapters/tests/implementations/move_oft_adapter_tests.move

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ module oft::move_oft_adapter_tests {
2222
use oft::move_oft_adapter::{
2323
Self,
2424
escrow_address,
25+
credit,
26+
debit_fungible_asset,
2527
fee_bps,
2628
fee_deposit_address,
29+
is_paused,
2730
is_blocklisted,
31+
set_paused,
32+
set_pauser,
33+
remove_pauser,
2834
set_fee_bps,
2935
set_fee_deposit_address,
3036
};
@@ -59,6 +65,140 @@ module oft::move_oft_adapter_tests {
5965
mint_ref
6066
}
6167

68+
// ============================= Pauser Tests (moved from source) =============================
69+
70+
#[test]
71+
fun test_pauser_toggle() {
72+
setup();
73+
74+
let admin = &create_signer_for_test(@oft_admin);
75+
let p = @0x1234;
76+
set_pauser(admin, p);
77+
assert!(!is_paused(), 0);
78+
79+
let ps = &create_signer_for_test(p);
80+
set_paused(ps, true);
81+
assert!(is_paused(), 1);
82+
83+
set_paused(admin, false);
84+
assert!(!is_paused(), 2);
85+
}
86+
87+
#[test]
88+
#[expected_failure(abort_code = 5)] // EUNAUTHORIZED
89+
fun test_only_pauser_or_admin_can_toggle() {
90+
setup();
91+
92+
let admin = &create_signer_for_test(@oft_admin);
93+
let p = @0x1111;
94+
let rando = &create_signer_for_test(@0x2222);
95+
set_pauser(admin, p);
96+
set_paused(rando, true);
97+
}
98+
99+
#[test]
100+
#[expected_failure(abort_code = 4)] // EPAUSED
101+
fun test_paused_blocks_debit() {
102+
let mint_ref = setup();
103+
let admin = &create_signer_for_test(@oft_admin);
104+
let p = @0xABCD;
105+
set_pauser(admin, p);
106+
let ps = &create_signer_for_test(p);
107+
set_paused(ps, true);
108+
109+
let fa = mint(&mint_ref, 1000);
110+
let (_s, _r) = debit_fungible_asset(@0x4444, &mut fa, 0, 101);
111+
// Consume FA to satisfy linear types even though test aborts
112+
burn_token_for_test(fa);
113+
}
114+
115+
#[test]
116+
fun test_unpause_restores_debit() {
117+
let mint_ref = setup();
118+
let admin = &create_signer_for_test(@oft_admin);
119+
let p = @0xBEEF;
120+
set_pauser(admin, p);
121+
let ps = &create_signer_for_test(p);
122+
set_paused(ps, true);
123+
set_paused(admin, false);
124+
125+
let fa = mint(&mint_ref, 2000);
126+
let (sent, received) = debit_fungible_asset(@0x5555, &mut fa, 0, 101);
127+
assert!(sent == 2000, 0);
128+
assert!(received == 2000, 1);
129+
assert!(fungible_asset::amount(&fa) == 0, 2);
130+
burn_token_for_test(fa);
131+
}
132+
133+
#[test]
134+
fun test_additional_pauser_can_toggle() {
135+
setup();
136+
137+
let admin = &create_signer_for_test(@oft_admin);
138+
let p1 = @0xAAA1;
139+
let p2 = @0xAAA2;
140+
set_pauser(admin, p1);
141+
set_pauser(admin, p2);
142+
143+
let ps1 = &create_signer_for_test(p1);
144+
set_paused(ps1, true);
145+
assert!(is_paused(), 0);
146+
let ps2 = &create_signer_for_test(p2);
147+
set_paused(ps2, false);
148+
assert!(!is_paused(), 1);
149+
}
150+
151+
#[test]
152+
#[expected_failure(abort_code = 5)] // EUNAUTHORIZED
153+
fun test_non_pauser_still_unauthorized() {
154+
setup();
155+
let admin = &create_signer_for_test(@oft_admin);
156+
let p = @0xAAA2;
157+
set_pauser(admin, p);
158+
let r = &create_signer_for_test(@0xCAFE);
159+
set_paused(r, true);
160+
}
161+
162+
#[test]
163+
#[expected_failure(abort_code = 4)] // EPAUSED
164+
fun test_credit_blocked_when_paused() {
165+
let mint_ref = setup();
166+
let amount_ld = 500u64;
167+
168+
// Prepare escrow balance
169+
let deposit = mint(&mint_ref, amount_ld);
170+
let src_eid = 101u32;
171+
let (_s2, _r2) = debit_fungible_asset(@0x444, &mut deposit, 0, src_eid);
172+
burn_token_for_test(deposit);
173+
174+
let admin = &create_signer_for_test(@oft_admin);
175+
let p = @0xD00D;
176+
set_pauser(admin, p);
177+
let ps = &create_signer_for_test(p);
178+
set_paused(ps, true);
179+
180+
let _ = credit(@0x7777, amount_ld, src_eid, option::none());
181+
}
182+
183+
#[test]
184+
#[expected_failure(abort_code = 5)] // EUNAUTHORIZED
185+
fun test_removed_pauser_loses_ability() {
186+
setup();
187+
188+
let admin = &create_signer_for_test(@oft_admin);
189+
let p1 = @0x111;
190+
let p2 = @0x222;
191+
set_pauser(admin, p1);
192+
set_pauser(admin, p2);
193+
194+
// Remove p1
195+
remove_pauser(admin, p1);
196+
197+
// p1 should no longer be authorized
198+
let ps1 = &create_signer_for_test(p1);
199+
set_paused(ps1, true);
200+
}
201+
62202
#[test]
63203
fun test_debit() {
64204
let mint_ref = setup();

0 commit comments

Comments
 (0)