$npx -y skills add aptos-labs/aptos-agent-skills --skill security-auditAudits Move contracts for security vulnerabilities before deployment using 7-category checklist. Triggers on: 'audit contract', 'security check', 'review security', 'check for vulnerabilities', 'security audit', 'is this secure', 'find security issues'.
| 1 | # Security Audit Skill |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This skill performs systematic security audits of Move contracts using a comprehensive checklist. Every item must pass |
| 6 | before deployment. |
| 7 | |
| 8 | **Critical:** Security is non-negotiable. User funds depend on correct implementation. |
| 9 | |
| 10 | ## Core Workflow |
| 11 | |
| 12 | ### Step 1: Run Security Checklist |
| 13 | |
| 14 | Review ALL categories in order: |
| 15 | |
| 16 | 1. **Access Control** - Who can call functions? |
| 17 | 2. **Input Validation** - Are inputs checked? |
| 18 | 3. **Object Safety** - Object model used correctly? |
| 19 | 4. **Reference Safety** - No dangerous references exposed? |
| 20 | 5. **Arithmetic Safety** - Overflow/underflow prevented? |
| 21 | 6. **Generic Type Safety** - Phantom types used correctly? |
| 22 | 7. **Testing** - 100% coverage achieved? |
| 23 | |
| 24 | ### Step 2: Access Control Audit |
| 25 | |
| 26 | **Verify:** |
| 27 | |
| 28 | - [ ] All `entry` functions verify signer authority |
| 29 | - [ ] Object ownership checked with `object::owner()` |
| 30 | - [ ] Admin functions check caller is admin |
| 31 | - [ ] Function visibility uses least-privilege |
| 32 | - [ ] No public functions modify state without checks |
| 33 | |
| 34 | **Check for:** |
| 35 | |
| 36 | ```move |
| 37 | // ✅ CORRECT: Signer verification |
| 38 | public entry fun update_config(admin: &signer, value: u64) acquires Config { |
| 39 | let config = borrow_global<Config>(@my_addr); |
| 40 | assert!(signer::address_of(admin) == config.admin, E_NOT_ADMIN); |
| 41 | // Safe to proceed |
| 42 | } |
| 43 | |
| 44 | // ❌ WRONG: No verification |
| 45 | public entry fun update_config(admin: &signer, value: u64) acquires Config { |
| 46 | let config = borrow_global_mut<Config>(@my_addr); |
| 47 | config.value = value; // Anyone can call! |
| 48 | } |
| 49 | ``` |
| 50 | |
| 51 | **For objects:** |
| 52 | |
| 53 | ```move |
| 54 | // ✅ CORRECT: Ownership verification |
| 55 | public entry fun transfer_item( |
| 56 | owner: &signer, |
| 57 | item: Object<Item>, |
| 58 | to: address |
| 59 | ) acquires Item { |
| 60 | assert!(object::owner(item) == signer::address_of(owner), E_NOT_OWNER); |
| 61 | // Safe to transfer |
| 62 | } |
| 63 | |
| 64 | // ❌ WRONG: No ownership check |
| 65 | public entry fun transfer_item( |
| 66 | owner: &signer, |
| 67 | item: Object<Item>, |
| 68 | to: address |
| 69 | ) acquires Item { |
| 70 | // Anyone can transfer any item! |
| 71 | } |
| 72 | ``` |
| 73 | |
| 74 | ### Step 3: Input Validation Audit |
| 75 | |
| 76 | **Verify:** |
| 77 | |
| 78 | - [ ] Numeric inputs checked for zero: `assert!(amount > 0, E_ZERO_AMOUNT)` |
| 79 | - [ ] Numeric inputs within max limits: `assert!(amount <= MAX, E_AMOUNT_TOO_HIGH)` |
| 80 | - [ ] Vector lengths validated: `assert!(vector::length(&v) > 0, E_EMPTY_VECTOR)` |
| 81 | - [ ] String lengths checked: `assert!(string::length(&s) <= MAX_LENGTH, E_NAME_TOO_LONG)` |
| 82 | - [ ] Addresses validated: `assert!(addr != @0x0, E_ZERO_ADDRESS)` |
| 83 | - [ ] Enum-like values in range: `assert!(type_id < MAX_TYPES, E_INVALID_TYPE)` |
| 84 | |
| 85 | **Check for:** |
| 86 | |
| 87 | ```move |
| 88 | // ✅ CORRECT: Comprehensive validation |
| 89 | public entry fun deposit(user: &signer, amount: u64) acquires Account { |
| 90 | assert!(amount > 0, E_ZERO_AMOUNT); |
| 91 | assert!(amount <= MAX_DEPOSIT_AMOUNT, E_AMOUNT_TOO_HIGH); |
| 92 | |
| 93 | let account = borrow_global_mut<Account>(signer::address_of(user)); |
| 94 | assert!(account.balance <= MAX_U64 - amount, E_OVERFLOW); |
| 95 | |
| 96 | account.balance = account.balance + amount; |
| 97 | } |
| 98 | |
| 99 | // ❌ WRONG: No validation |
| 100 | public entry fun deposit(user: &signer, amount: u64) acquires Account { |
| 101 | let account = borrow_global_mut<Account>(signer::address_of(user)); |
| 102 | account.balance = account.balance + amount; // Can overflow! |
| 103 | } |
| 104 | ``` |
| 105 | |
| 106 | ### Step 4: Object Safety Audit |
| 107 | |
| 108 | **Verify:** |
| 109 | |
| 110 | - [ ] ConstructorRef never returned from public functions |
| 111 | - [ ] All refs (TransferRef, DeleteRef, ExtendRef) generated in constructor |
| 112 | - [ ] Object signer only used during construction or with ExtendRef |
| 113 | - [ ] Ungated transfers disabled unless explicitly needed |
| 114 | - [ ] DeleteRef only generated for truly burnable objects |
| 115 | |
| 116 | **Check for:** |
| 117 | |
| 118 | ```move |
| 119 | // ❌ DANGEROUS: Returning ConstructorRef |
| 120 | public fun create_item(): ConstructorRef { |
| 121 | let constructor_ref = object::create_object(@my_addr); |
| 122 | constructor_ref // Caller can destroy object! |
| 123 | } |
| 124 | |
| 125 | // ✅ CORRECT: Return Object<T> |
| 126 | public fun create_item(creator: &signer): Object<Item> { |
| 127 | let constructor_ref = object::create_object(signer::address_of(creator)); |
| 128 | |
| 129 | let transfer_ref = object::generate_transfer_ref(&constructor_ref); |
| 130 | let delete_ref = object::generate_delete_ref(&constructor_ref); |
| 131 | let object_signer = object::generate_signer(&constructor_ref); |
| 132 | |
| 133 | move_to(&object_signer, Item { transfer_ref, delete_ref }); |
| 134 | |
| 135 | object::object_from_constructor_ref<Item>(&constructor_ref) |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | ### Step 5: Reference Safety Audit |
| 140 | |
| 141 | **Verify:** |
| 142 | |
| 143 | - [ ] No `&mut` references exposed in public function signatures |
| 144 | - [ ] Critical fields protected from `mem::swap` |
| 145 | - [ ] Mutable borrows minimized in scope |
| 146 | |
| 147 | **Check for:** |
| 148 | |
| 149 | ```move |
| 150 | // ❌ DANGEROUS: Exposing mutable reference |