Skip to main content

Unrestricted Transfer From

Description

Using an user-defined argument as a transfer_from's from parameter could lead to transfer funds from a third party account without proper authorization.

Exploit Scenario

Consider the following ink! contract:

// build_call example
#[ink(message)]
pub fn deposit(&mut self, from: AccountId) -> Result<(), Error> {
let call_params = build_call::<DefaultEnvironment>()
.exec_input(
ExecutionInput::new(Selector::new(ink::selector_bytes!(
"PSP22::transfer_from"
)))
.push_arg(from)
.push_arg(self.env().account_id())
.push_arg(self.amount)
.push_arg([0u8]),
)
}

// ContractRef example
#[ink(message)]
pub fn deposit(&mut self, from: AccountId) -> Result<(), Error> {
let res = PSP22Ref::transfer_from(
&self.psp22_address,
from,
self.env().account_id(),
self.amount,
vec![],
);
}

The vulnerability in this deposit function arises from the use of from, an user-defined parameter as an argument in the from field of the transfer_from function. Alice can approve a contract to spend their tokens, then Bob can call that contract, use that allowance to send as themselves Alice's tokens.

The vulnerable code example can be found here.

Remediation

Avoid using user-defined arguments as from parameter in transfer_from. Instead, use self.env().caller().

The remediated code example can be found here.

References