function bps() internal pure returns (IERC20 rt) {
// These fields are not accessible from assembly
bytes memory array = msg.data;
uint256 index = msg.data.length;
// solhint-disable-next-line no-inline-assembly
assembly {
// Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
rt := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
The above function is designed to expect the token at the end of
calldata, but a malicious user can inject extra values at the end of
calldata and fake return values.
The following contract demonstrates an example:
pragma solidity 0.8.6;
interface IERC20 {}
error StaticCallFailed();
contract BadEncoding {
/// Will return address(1). But address(0) is expected!
function f() external view returns (address) {
address actual = address(0);
address injected = address(1);
(bool success, bytes memory ret) = address(this).staticcall(abi.encodeWithSelector(this.g.selector, actual, injected));
if (!success) revert StaticCallFailed();
return abi.decode(ret, (address));
}
function g(IERC20 _token) external pure returns (IERC20) {
// to get rid of the unused warning
_token;
// Does it always match _token?
return bps();
}
// From Sherlock Protocol: PoolBase.sol
function bps() internal pure returns (IERC20 rt) {
// These fields are not accessible from assembly
bytes memory array = msg.data;
uint256 index = msg.data.length;
// solhint-disable-next-line no-inline-assembly
assembly {
// Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those.
rt := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff)
}
}
}
All the calculations on ps would be done on Token2, but at the end,
because of, _token.safeTransfer(_receiver, amount);, Token2 would be
transferred. Assuming that Token2 is more expensive than Token1, the
attacker makes a profit.
Similarly, the same technique can be used at a lot of other places. Even
if this exploit is not profitable, the fact that the computations can be
done on two different tokens is buggy.
There are several other places where the same pattern is used. All of
them needs to be fixed. I've not written an exhaustive list.
Handle
hrkrshnn
Vulnerability details
A critical bug in bps function: PoolBase.sol
The above function is designed to expect the token at the end of
calldata
, but a malicious user can inject extra values at the end ofcalldata
and fake return values.The following contract demonstrates an example:
An example exploit
This can be used to exploit the protocol:
State token
Token1
. Let's say there is a more expensive tokenToken2
.Here's an example exploit:
All the calculations on
ps
would be done onToken2
, but at the end, because of,_token.safeTransfer(_receiver, amount);
,Token2
would be transferred. Assuming thatToken2
is more expensive thanToken1
, the attacker makes a profit.Similarly, the same technique can be used at a lot of other places. Even if this exploit is not profitable, the fact that the computations can be done on two different tokens is buggy.
There are several other places where the same pattern is used. All of them needs to be fixed. I've not written an exhaustive list.