diff --git a/client/src/lib.rs b/client/src/lib.rs
index 256bc5c155..074a8b6f77 100644
--- a/client/src/lib.rs
+++ b/client/src/lib.rs
@@ -659,13 +659,7 @@ impl Client {
         }
     }
 
-    pub fn is_dead(&self) -> bool {
-        self.state
-            .ecs()
-            .read_storage::<comp::Health>()
-            .get(self.entity)
-            .map_or(false, |h| h.is_dead)
-    }
+    pub fn is_dead(&self) -> bool { self.current::<comp::Health>().map_or(false, |h| h.is_dead) }
 
     pub fn pick_up(&mut self, entity: EcsEntity) {
         // Get the health component from the entity
@@ -1504,7 +1498,7 @@ impl Client {
                     },
                 }
             },
-            ServerGeneral::GroupInvite {
+            ServerGeneral::Invite {
                 inviter,
                 timeout,
                 kind,
diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs
index b9b9e1e76b..08fea64d8a 100644
--- a/common/net/src/msg/server.rs
+++ b/common/net/src/msg/server.rs
@@ -78,7 +78,7 @@ pub enum ServerGeneral {
     //Ingame related
     GroupUpdate(comp::group::ChangeNotification<sync::Uid>),
     /// Indicate to the client that they are invited to join a group
-    GroupInvite {
+    Invite {
         inviter: sync::Uid,
         timeout: std::time::Duration,
         kind: InviteKind,
@@ -221,7 +221,7 @@ impl ServerMsg {
                         },
                         //Ingame related
                         ServerGeneral::GroupUpdate(_)
-                        | ServerGeneral::GroupInvite { .. }
+                        | ServerGeneral::Invite { .. }
                         | ServerGeneral::InvitePending(_)
                         | ServerGeneral::InviteComplete { .. }
                         | ServerGeneral::ExitInGameSuccess
diff --git a/common/src/trade.rs b/common/src/trade.rs
index 9b1283ad16..68861e8b64 100644
--- a/common/src/trade.rs
+++ b/common/src/trade.rs
@@ -4,7 +4,7 @@ use crate::{
 };
 use hashbrown::HashMap;
 use serde::{Deserialize, Serialize};
-use tracing::{warn, trace};
+use tracing::{trace, warn};
 
 /// Clients submit `TradeActionMsg` to the server, which adds the Uid of the
 /// player out-of-band (i.e. without trusting the client to say who it's
@@ -15,6 +15,7 @@ pub enum TradeActionMsg {
     RemoveItem { item: InvSlotId, quantity: u32 },
     Phase1Accept,
     Phase2Accept,
+
     Decline,
 }
 
@@ -28,6 +29,28 @@ pub enum TradeResult {
 /// Items are not removed from the inventory during a PendingTrade: all the
 /// items are moved atomically (if there's space and both parties agree) upon
 /// completion
+///
+/// Since this stores `InvSlotId`s (i.e. references into inventories) instead of
+/// items themselves, there aren't any duplication/loss risks from things like
+/// dropped connections or declines, since the server doesn't have to move items
+/// from a trade back into a player's inventory.
+///
+/// On the flip side, since they are references to *slots*, if a player could
+/// swaps items in their inventory during a trade, they could mutate the trade,
+/// enabling them to remove an item from the trade even after receiving the
+/// counterparty's phase2 accept. To prevent this, we disallow all
+/// forms of inventory manipulation in `server::events::inventory_manip` if
+/// there's a pending trade that's past phase1 (in phase1, the trade should be
+/// mutable anyway).
+///
+/// Inventory manipulation in phase1 may be beneficial to trade (e.g. splitting
+/// a stack of items, once that's implemented), but should reset both phase1
+/// accept flags to make the changes more visible.
+///
+/// Another edge case prevented by using `InvSlotId`s is that it disallows
+/// trading currently-equipped items (since `EquipSlot`s are disjoint from
+/// `InvSlotId`s), which avoids the issues associated with trading equipped bags
+/// that may still have contents.
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct PendingTrade {
     /// `parties[0]` is the entity that initiated the trade, parties[1] is the
@@ -79,8 +102,7 @@ impl PendingTrade {
     /// - A party is never shown as offering more of an item than they own
     /// - Offers with a quantity of zero get removed from the trade
     /// - Modifications can only happen in phase 1
-    /// - Whenever a trade is modified, both accept flags get reset (TODO: detect or prevent
-    /// inventory swaps)
+    /// - Whenever a trade is modified, both accept flags get reset
     /// - Accept flags only get set for the current phase
     pub fn process_msg(&mut self, who: usize, msg: TradeActionMsg, inventory: &Inventory) {
         use TradeActionMsg::*;
@@ -89,13 +111,11 @@ impl PendingTrade {
                 item,
                 quantity: delta,
             } => {
-                if self.in_phase1() {
-                    if delta > 0 {
-                        let total = self.offers[who].entry(item).or_insert(0);
-                        let owned_quantity = inventory.get(item).map(|i| i.amount()).unwrap_or(0);
-                        *total = total.saturating_add(delta).min(owned_quantity);
-                        self.phase1_accepts = [false, false];
-                    }
+                if self.in_phase1() && delta > 0 {
+                    let total = self.offers[who].entry(item).or_insert(0);
+                    let owned_quantity = inventory.get(item).map(|i| i.amount()).unwrap_or(0);
+                    *total = total.saturating_add(delta).min(owned_quantity);
+                    self.phase1_accepts = [false, false];
                 }
             },
             RemoveItem {
@@ -130,6 +150,7 @@ impl PendingTrade {
 pub struct Trades {
     pub next_id: usize,
     pub trades: HashMap<usize, PendingTrade>,
+    pub entity_trades: HashMap<Uid, usize>,
 }
 
 impl Trades {
@@ -138,6 +159,8 @@ impl Trades {
         self.next_id = id.wrapping_add(1);
         self.trades
             .insert(id, PendingTrade::new(party, counterparty));
+        self.entity_trades.insert(party, id);
+        self.entity_trades.insert(counterparty, id);
         id
     }
 
@@ -168,6 +191,8 @@ impl Trades {
         if let Some(trade) = self.trades.remove(&id) {
             match trade.which_party(who) {
                 Some(i) => {
+                    self.entity_trades.remove(&trade.parties[0]);
+                    self.entity_trades.remove(&trade.parties[1]);
                     // let the other person know the trade was declined
                     to_notify = Some(trade.parties[1 - i])
                 },
@@ -185,6 +210,37 @@ impl Trades {
         }
         to_notify
     }
+
+    /// See the doc comment on `common::trade::PendingTrade` for the
+    /// significance of these checks
+    pub fn in_trade_with_property<F: FnOnce(&PendingTrade) -> bool>(
+        &self,
+        uid: &Uid,
+        f: F,
+    ) -> bool {
+        self.entity_trades
+            .get(uid)
+            .and_then(|trade_id| self.trades.get(trade_id))
+            .map(f)
+            // if any of the option lookups failed, we're not in any trade
+            .unwrap_or(false)
+    }
+
+    pub fn in_immutable_trade(&self, uid: &Uid) -> bool {
+        self.in_trade_with_property(uid, |trade| !trade.in_phase1())
+    }
+
+    pub fn in_mutable_trade(&self, uid: &Uid) -> bool {
+        self.in_trade_with_property(uid, |trade| trade.in_phase1())
+    }
+
+    pub fn implicit_mutation_occurred(&mut self, uid: &Uid) {
+        if let Some(trade_id) = self.entity_trades.get(uid) {
+            self.trades
+                .get_mut(trade_id)
+                .map(|trade| trade.phase1_accepts = [false, false]);
+        }
+    }
 }
 
 impl Default for Trades {
@@ -192,6 +248,7 @@ impl Default for Trades {
         Trades {
             next_id: 0,
             trades: HashMap::new(),
+            entity_trades: HashMap::new(),
         }
     }
 }
diff --git a/server/src/client.rs b/server/src/client.rs
index 4255808cec..c4b1e68057 100644
--- a/server/src/client.rs
+++ b/server/src/client.rs
@@ -79,7 +79,7 @@ impl Client {
                     },
                     //Ingame related
                     ServerGeneral::GroupUpdate(_)
-                    | ServerGeneral::GroupInvite { .. }
+                    | ServerGeneral::Invite { .. }
                     | ServerGeneral::InvitePending(_)
                     | ServerGeneral::InviteComplete { .. }
                     | ServerGeneral::ExitInGameSuccess
@@ -159,7 +159,7 @@ impl Client {
                     },
                     //Ingame related
                     ServerGeneral::GroupUpdate(_)
-                    | ServerGeneral::GroupInvite { .. }
+                    | ServerGeneral::Invite { .. }
                     | ServerGeneral::InvitePending(_)
                     | ServerGeneral::InviteComplete { .. }
                     | ServerGeneral::ExitInGameSuccess
@@ -170,7 +170,9 @@ impl Client {
                     | ServerGeneral::Outcomes(_)
                     | ServerGeneral::Knockback(_)
                     | ServerGeneral::UpdatePendingTrade(_, _)
-                    | ServerGeneral::FinishedTrade(_) => PreparedMsg::new(2, &g, &self.in_game_stream),
+                    | ServerGeneral::FinishedTrade(_) => {
+                        PreparedMsg::new(2, &g, &self.in_game_stream)
+                    },
                     // Always possible
                     ServerGeneral::PlayerListUpdate(_)
                     | ServerGeneral::ChatMsg(_)
diff --git a/server/src/events/group_manip.rs b/server/src/events/group_manip.rs
index d7ff25792c..61275c31b1 100644
--- a/server/src/events/group_manip.rs
+++ b/server/src/events/group_manip.rs
@@ -160,7 +160,7 @@ pub fn handle_invite(
     // If client comp
     if let (Some(client), Some(inviter)) = (clients.get(invitee), uids.get(inviter).copied()) {
         if send_invite() {
-            client.send_fallible(ServerGeneral::GroupInvite {
+            client.send_fallible(ServerGeneral::Invite {
                 inviter,
                 timeout: PRESENTED_INVITE_TIMEOUT_DUR,
                 kind,
diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs
index 38103ad025..f6419a6b85 100644
--- a/server/src/events/inventory_manip.rs
+++ b/server/src/events/inventory_manip.rs
@@ -10,6 +10,7 @@ use common::{
     },
     consts::MAX_PICKUP_RANGE,
     recipe::default_recipe_book,
+    trade::Trades,
     uid::Uid,
     util::find_dist::{self, FindDist},
     vol::ReadVol,
@@ -39,6 +40,15 @@ pub fn snuff_lantern(storage: &mut WriteStorage<comp::LightEmitter>, entity: Ecs
 #[allow(clippy::same_item_push)] // TODO: Pending review in #587
 pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::SlotManip) {
     let state = server.state_mut();
+
+    if let Some(uid) = state.ecs().uid_from_entity(entity) {
+        let trades = state.ecs().read_resource::<Trades>();
+        if trades.in_immutable_trade(&uid) {
+            // manipulating the inventory can mutate the trade
+            return;
+        }
+    }
+
     let mut dropped_items = Vec::new();
     let mut thrown_items = Vec::new();
 
@@ -568,6 +578,14 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Slo
 
         new_entity.build();
     }
+
+    if let Some(uid) = state.ecs().uid_from_entity(entity) {
+        let mut trades = state.ecs().write_resource::<Trades>();
+        if trades.in_mutable_trade(&uid) {
+            // manipulating the inventory mutated the trade, so reset the accept flags
+            trades.implicit_mutation_occurred(&uid);
+        }
+    }
 }
 
 fn within_pickup_range<S: FindDist<find_dist::Cylinder>>(
diff --git a/server/src/events/trade.rs b/server/src/events/trade.rs
index 144e739ed8..422e080f7a 100644
--- a/server/src/events/trade.rs
+++ b/server/src/events/trade.rs
@@ -1,17 +1,15 @@
 use crate::{events::group_manip::handle_invite, Server};
 use common::{
-    comp::{
-        group::InviteKind,
-        inventory::Inventory,
-    },
-    trade::{PendingTrade, TradeActionMsg, Trades, TradeResult},
+    comp::{group::InviteKind, inventory::Inventory},
+    trade::{PendingTrade, TradeActionMsg, TradeResult, Trades},
 };
 use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
 use specs::{world::WorldExt, Entity as EcsEntity};
 use std::cmp::Ordering;
-use tracing::{error, warn, trace};
+use tracing::{error, trace, warn};
 
-/// Invoked when pressing the trade button near an entity, triggering the invite UI flow
+/// Invoked when pressing the trade button near an entity, triggering the invite
+/// UI flow
 pub fn handle_initiate_trade(server: &mut Server, interactor: EcsEntity, counterparty: EcsEntity) {
     if let Some(uid) = server.state_mut().ecs().uid_from_entity(counterparty) {
         handle_invite(server, interactor, uid, InviteKind::Trade);
@@ -33,7 +31,9 @@ pub fn handle_process_trade_action(
             let to_notify = trades.decline_trade(trade_id, uid);
             to_notify
                 .and_then(|u| server.state.ecs().entity_from_uid(u.0))
-                .map(|e| server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined)));
+                .map(|e| {
+                    server.notify_client(e, ServerGeneral::FinishedTrade(TradeResult::Declined))
+                });
         } else {
             if let Some(inv) = server.state.ecs().read_component::<Inventory>().get(entity) {
                 trades.process_trade_action(trade_id, uid, msg, inv);
@@ -46,19 +46,19 @@ pub fn handle_process_trade_action(
                 }
                 // send the updated state to both parties
                 for party in trade.parties.iter() {
-                    server.state.ecs().entity_from_uid(party.0).map(|e| {
-                        server.notify_client(
-                            e,
-                            msg.clone(),
-                        )
-                    });
+                    server
+                        .state
+                        .ecs()
+                        .entity_from_uid(party.0)
+                        .map(|e| server.notify_client(e, msg.clone()));
                 }
             }
         }
     }
 }
 
-/// Commit a trade that both parties have agreed to, modifying their respective inventories
+/// Commit a trade that both parties have agreed to, modifying their respective
+/// inventories
 fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
     let mut entities = vec![];
     for who in [0, 1].iter().cloned() {
@@ -75,8 +75,8 @@ fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
     }
     let invmsg = "inventories.get_mut(entities[who]).is_none() should have returned already";
     trace!("committing trade: {:?}", trade);
-    // Compute the net change in slots of each player during the trade, to detect out-of-space-ness
-    // before transferring any items
+    // Compute the net change in slots of each player during the trade, to detect
+    // out-of-space-ness before transferring any items
     let mut delta_slots: [isize; 2] = [0, 0];
     for who in [0, 1].iter().cloned() {
         for (slot, quantity) in trade.offers[who].iter() {
@@ -84,30 +84,38 @@ fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
             let item = match inventory.get(*slot) {
                 Some(item) => item,
                 None => {
-                    error!("PendingTrade invariant violation in trade {:?}: slots offered in a trade should be non-empty", trade);
+                    error!(
+                        "PendingTrade invariant violation in trade {:?}: slots offered in a trade \
+                         should be non-empty",
+                        trade
+                    );
                     return TradeResult::Declined;
                 },
             };
             match item.amount().cmp(&quantity) {
                 Ordering::Less => {
-                    error!("PendingTrade invariant violation in trade {:?}: party {} offered more of an item than they have", trade, who);
+                    error!(
+                        "PendingTrade invariant violation in trade {:?}: party {} offered more of \
+                         an item than they have",
+                        trade, who
+                    );
                     return TradeResult::Declined;
                 },
                 Ordering::Equal => {
                     delta_slots[who] -= 1; // exact, removes the whole stack
-                    delta_slots[1-who] += 1; // overapproximation, assumes the stack won't merge
+                    delta_slots[1 - who] += 1; // overapproximation, assumes the stack won't merge
                 },
                 Ordering::Greater => {
                     // no change to delta_slots[who], since they have leftovers
-                    delta_slots[1-who] += 1; // overapproximation, assumes the stack won't merge
+                    delta_slots[1 - who] += 1; // overapproximation, assumes the stack won't merge
                 },
             }
         }
     }
     trace!("delta_slots: {:?}", delta_slots);
     for who in [0, 1].iter().cloned() {
-        // Inventories should never exceed 2^{63} slots, so the usize -> isize conversions here
-        // should be safe
+        // Inventories should never exceed 2^{63} slots, so the usize -> isize
+        // conversions here should be safe
         let inv = inventories.get_mut(entities[who]).expect(invmsg);
         if inv.populated_slots() as isize + delta_slots[who] > inv.capacity() as isize {
             return TradeResult::NotEnoughSpace;
@@ -118,15 +126,26 @@ fn commit_trade(ecs: &specs::World, trade: &PendingTrade) -> TradeResult {
         for (slot, quantity) in trade.offers[who].iter() {
             // take the items one by one, to benefit from Inventory's stack handling
             for _ in 0..*quantity {
-                inventories.get_mut(entities[who]).expect(invmsg).take(*slot).map(|item| items[who].push(item));
+                inventories
+                    .get_mut(entities[who])
+                    .expect(invmsg)
+                    .take(*slot)
+                    .map(|item| items[who].push(item));
             }
         }
     }
     for who in [0, 1].iter().cloned() {
-        if let Err(leftovers) = inventories.get_mut(entities[1-who]).expect(invmsg).push_all(items[who].drain(..)) {
-            // this should only happen if the arithmetic above for delta_slots says there's enough
-            // space and there isn't (i.e. underapproximates)
-            error!("Not enough space for all the items, destroying leftovers {:?}", leftovers);
+        if let Err(leftovers) = inventories
+            .get_mut(entities[1 - who])
+            .expect(invmsg)
+            .push_all(items[who].drain(..))
+        {
+            // this should only happen if the arithmetic above for delta_slots says there's
+            // enough space and there isn't (i.e. underapproximates)
+            error!(
+                "Not enough space for all the items, destroying leftovers {:?}",
+                leftovers
+            );
         }
     }
     TradeResult::Completed
diff --git a/voxygen/src/hud/trade.rs b/voxygen/src/hud/trade.rs
index 9794e8b395..fef8ef5ef5 100644
--- a/voxygen/src/hud/trade.rs
+++ b/voxygen/src/hud/trade.rs
@@ -151,8 +151,7 @@ impl<'a> Trade<'a> {
         // Alignment for Grid
         let mut alignment = Rectangle::fill_with([200.0, 340.0], color::TRANSPARENT);
         if who % 2 == 0 {
-            alignment =
-                alignment.top_left_with_margins_on(state.ids.bg, 180.0, 46.5);
+            alignment = alignment.top_left_with_margins_on(state.ids.bg, 180.0, 46.5);
         } else {
             alignment = alignment.right_from(state.ids.inv_alignment[0], 0.0);
         }
@@ -188,14 +187,15 @@ impl<'a> Trade<'a> {
             .down_from(state.ids.inv_alignment[who], 20.0)
             .font_id(self.fonts.cyri.conrod_id)
             .font_size(self.fonts.cyri.scale(20))
-            .color(Color::Rgba(1.0, 1.0, 1.0, if has_accepted { 1.0 } else { 0.0 }))
+            .color(Color::Rgba(
+                1.0,
+                1.0,
+                1.0,
+                if has_accepted { 1.0 } else { 0.0 },
+            ))
             .set(state.ids.accept_indicators[who], ui);
 
-
-        let mut invslots: Vec<_> = trade.offers[who]
-            .iter()
-            .map(|(k, v)| (k.clone(), v.clone()))
-            .collect();
+        let mut invslots: Vec<_> = trade.offers[who].iter().map(|(k, v)| (*k, *v)).collect();
         invslots.sort();
         let tradeslots: Vec<_> = invslots
             .into_iter()
@@ -222,7 +222,7 @@ impl<'a> Trade<'a> {
         ui: &mut UiCell<'_>,
         inventory: &Inventory,
         who: usize,
-        tradeslots: &Vec<TradeSlot>,
+        tradeslots: &[TradeSlot],
     ) {
         let mut slot_maker = SlotMaker {
             empty_slot: self.imgs.inv_slot,
@@ -262,7 +262,7 @@ impl<'a> Trade<'a> {
             });
             // Slot
             let slot_widget = slot_maker
-                .fabricate(slot.clone(), [40.0; 2])
+                .fabricate(slot, [40.0; 2])
                 .top_left_with_margins_on(
                     state.ids.inv_alignment[who],
                     0.0 + y as f64 * (40.0),
@@ -271,13 +271,14 @@ impl<'a> Trade<'a> {
             slot_widget.set(state.ids.inv_slots[i + who * MAX_TRADE_SLOTS], ui);
         }
     }
+
     fn phase2_itemwidget(
         &mut self,
         state: &mut ConrodState<'_, State>,
         ui: &mut UiCell<'_>,
         inventory: &Inventory,
         who: usize,
-        tradeslots: &Vec<TradeSlot>,
+        tradeslots: &[TradeSlot],
     ) {
         if state.ids.inv_textslots.len() < 2 * MAX_TRADE_SLOTS {
             state.update(|s| {
@@ -302,7 +303,12 @@ impl<'a> Trade<'a> {
                 .top_left_with_margins_on(state.ids.inv_alignment[who], 10.0 + i as f64 * 30.0, 0.0)
                 .font_id(self.fonts.cyri.conrod_id)
                 .font_size(self.fonts.cyri.scale(20))
-                .color(Color::Rgba(1.0, 1.0, 1.0, if is_present { 1.0 } else { 0.0 }))
+                .color(Color::Rgba(
+                    1.0,
+                    1.0,
+                    1.0,
+                    if is_present { 1.0 } else { 0.0 },
+                ))
                 .set(state.ids.inv_textslots[i + who * MAX_TRADE_SLOTS], ui);
         }
     }
@@ -406,7 +412,9 @@ impl<'a> Widget for Trade<'a> {
         }
         if state.ids.accept_indicators.len() < 2 {
             state.update(|s| {
-                s.ids.accept_indicators.resize(2, &mut ui.widget_id_generator());
+                s.ids
+                    .accept_indicators
+                    .resize(2, &mut ui.widget_id_generator());
             });
         }