Skip to content

Commit

Permalink
cosmwasm: accountant: Allow transfers on the same chain
Browse files Browse the repository at this point in the history
While sending tokens to another address on the same chain via wormhole
is quite inefficient, it's not strictly disallowed and we do have some
VAAs on solana that do this.  Explicitly check for this case and allow
it.

There are no restrictions on native tokens sent in this way but wrapped
transfers are still subject to some checks: the wrapped account for
the token must exist and it must have a balance larger than the amount
being transferred.  If either of those checks fails then that means
that the sender acquired some wrapped tokens that did not go through
the accountant and so that transfer should be blocked and require manual
intervention.
  • Loading branch information
jynnantonix authored and evan-gray committed Jan 26, 2023
1 parent 4c7df41 commit 9f01093
Showing 1 changed file with 127 additions and 3 deletions.
130 changes: 127 additions & 3 deletions cosmwasm/packages/accountant/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,10 @@ fn transfer<C: CustomQuery>(deps: Deps<C>, t: &Transfer) -> anyhow::Result<(Acco
Account { key, balance }
};

let mut dst = {
let mut dst = if t.key.emitter_chain() == t.data.recipient_chain {
// This is a self-transfer so the source and destination accounts are the same.
src.clone()
} else {
let key = account::Key::new(
t.data.recipient_chain,
t.data.token_chain,
Expand All @@ -180,8 +183,15 @@ fn transfer<C: CustomQuery>(deps: Deps<C>, t: &Transfer) -> anyhow::Result<(Acco

src.lock_or_burn(t.data.amount)
.context(TransferError::InsufficientSourceBalance)?;
dst.unlock_or_mint(t.data.amount)
.context(TransferError::InsufficientDestinationBalance)?;

// If we're doing a transfer on the same chain then apply both changes to the same account.
if src.key == dst.key {
src.unlock_or_mint(t.data.amount)
.context(TransferError::InsufficientDestinationBalance)?;
} else {
dst.unlock_or_mint(t.data.amount)
.context(TransferError::InsufficientDestinationBalance)?;
}

Ok((src, dst))
}
Expand Down Expand Up @@ -467,6 +477,120 @@ mod tests {
assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
}

#[test]
fn local_native_transfer() {
let mut deps = mock_dependencies();
let tx = Transfer {
key: transfer::Key::new(3, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 3,
},
};

validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();

// Since we transfered within the same chain, the balance should be 0.
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(Balance::zero(), query_balance(deps.as_ref(), src).unwrap());

let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(Balance::zero(), query_balance(deps.as_ref(), dst).unwrap());

assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
}

#[test]
fn local_wrapped_transfer() {
let mut deps = mock_dependencies();
let mut tx = Transfer {
key: transfer::Key::new(9, [1u8; 32].into(), 5),
data: transfer::Data {
amount: Uint256::from(400u128),
token_chain: 3,
token_address: [3u8; 32].into(),
recipient_chain: 9,
},
};

// A local wrapped transfer is only allowed if we've already issued wrapped tokens.
let e = validate_transfer(deps.as_ref(), &tx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingWrappedAccount
));

let e = commit_transfer(deps.as_mut(), tx.clone())
.expect_err("successfully committed duplicate transfer");
assert!(matches!(
e.downcast().unwrap(),
TransferError::MissingWrappedAccount
));

// Issue some wrapped tokens.
let wrapped = tx.data.amount.checked_div(Uint256::from(2u128)).unwrap();
let m = Modification {
sequence: 1,
chain_id: tx.key.emitter_chain(),
token_chain: tx.data.token_chain,
token_address: tx.data.token_address,
kind: Kind::Add,
amount: wrapped,
reason: "test".into(),
};
modify_balance(deps.as_mut(), m).unwrap();

// The transfer should still fail because we're trying to move more wrapped tokens than
// were issued.
let e = validate_transfer(deps.as_ref(), &tx).unwrap_err();
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientSourceBalance
));

let e = commit_transfer(deps.as_mut(), tx.clone())
.expect_err("successfully committed duplicate transfer");
assert!(matches!(
e.downcast().unwrap(),
TransferError::InsufficientSourceBalance
));

// Lower the amount in the transfer.
tx.data.amount = wrapped;

// Now the transfer should be fine.
validate_transfer(deps.as_ref(), &tx).unwrap();
commit_transfer(deps.as_mut(), tx.clone()).unwrap();

// The balance should not have changed.
let src = account::Key::new(
tx.key.emitter_chain(),
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(wrapped, *query_balance(deps.as_ref(), src).unwrap());

let dst = account::Key::new(
tx.data.recipient_chain,
tx.data.token_chain,
tx.data.token_address,
);
assert_eq!(wrapped, *query_balance(deps.as_ref(), dst).unwrap());

assert_eq!(tx.data, query_transfer(deps.as_ref(), tx.key).unwrap());
}

#[test]
fn duplicate_transfer() {
let mut deps = mock_dependencies();
Expand Down

0 comments on commit 9f01093

Please sign in to comment.