Platform: Code4rena
Start Date: 03/05/2022
Pot Size: $75,000 USDC
Total HM: 6
Participants: 55
Period: 7 days
Judge: Albert Chon
Total Solo HM: 2
Id: 116
League: COSMOS
Rank: 18/55
Findings: 2
Award: $690.76
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: p_crypt0
Also found by: 0x1337, GermanKuber, IllIllI, WatchPug, csanuragjain, danb, dirk_y, kirk-baird
The function withdrawERC20()
allows an admin to withdraw any ERC20 tokens help in the bridge.
The impact of this is significant as the function deployERC20()
will create a new CosmosERC20
token with MAX_UINT256
supply minted to the Gravity
smart contract.
The admin can therefore transfer out any amount (up to 2 ^ 256) CosmosERC20 tokens to themselves. This poses a strong centralisation risk as the admin it able to rug pull the protocol at any time. Furthermore, it makes the admin private key a target for attackers due to the large benefit they gain from stealing this access key.
There are no restrictions on what _tokenAddress
is and thus the attacker may withdraw the full balance of any token ERC20 token.
function withdrawERC20( address _tokenAddress) external { require(cudosAccessControls.hasAdminRole(msg.sender), "Recipient is not an admin"); uint256 totalBalance = IERC20(_tokenAddress).balanceOf(address(this)); IERC20(_tokenAddress).safeTransfer(msg.sender , totalBalance); }
We recommend removing this function to remove motivation for attackers and admins to rug pull the bridge.
#0 - mlukanova
2022-05-10T14:31:15Z
Duplicate of #14
🌟 Selected for report: IllIllI
Also found by: 0x1337, 0x1f8b, 0xDjango, 0xkatana, AmitN, CertoraInc, Dravee, Funen, GermanKuber, GimelSec, Hawkeye, JC, MaratCerby, WatchPug, Waze, broccolirob, cccz, ch13fd357r0y3r, cryptphi, danb, defsec, delfin454000, dipp, dirk_y, ellahi, gzeon, hake, hubble, ilan, jah, jayjonah8, kebabsec, kirk-baird, m9800, orion, oyc_109, robee, shenwilly, simon135, sorrynotsorry
188.2942 USDC - $188.29
Smart contracts addresses made using the create
opcode are deterministic based off the deployer account and the nonce of this account.
An attacker is therefore able to predetermine the address of any smart contracts deployed using deployERC20()
. One of the limitations of OpenZeppelin's token.safeTransferFrom(from, to, amount)
is that it will succeed if there is no bytecode at the token
address.
The impact of both of these features is that an attacker may call sendToCosmos(_tokenContract, _destination, _amount)
for an ERC20 token before it is deployed using deployERC20()
since the address can be calculated. The attacker may set any arbitrary amount and the IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount);
will succeed. The SendToCosmosEvent
will be emitted and the relevant transfer will occur on the other side of the bridge.
IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount);
succeeds if _tokenContract
does not have any bytecode. So the attacker may call sendToCosmos()
.
Following this the attacker calls deployERC20()
to deploy the token contract. Note the token contract was precalculated and used as the _tokenContract
parameter in sendToCosmos()
.
The bridge will then process SendToCosmosEvent
on the Cosmos chain.
function sendToCosmos( address _tokenContract, bytes32 _destination, uint256 _amount ) public nonReentrant { IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); state_lastEventNonce = state_lastEventNonce.add(1); emit SendToCosmosEvent( _tokenContract, msg.sender, _destination, _amount, state_lastEventNonce ); } function deployERC20( string memory _cosmosDenom, string memory _name, string memory _symbol, uint8 _decimals ) public { // Deploy an ERC20 with entire supply granted to Gravity.sol CosmosERC20 erc20 = new CosmosERC20(address(this), _name, _symbol, _decimals); // Fire an event to let the Cosmos module know state_lastEventNonce = state_lastEventNonce.add(1); emit ERC20DeployedEvent( _cosmosDenom, address(erc20), _name, _symbol, _decimals, state_lastEventNonce ); }
This issue may be resolved by enforcing sendToCosmos()
check that _tokenContract
contains bytecode.
function isContract(address _addr) private returns (bool isContract){ uint32 size; assembly { size := extcodesize(_addr) } return (size > 0); } function sendToCosmos( address _tokenContract, bytes32 _destination, uint256 _amount ) public nonReentrant { require(isContract(_tokenContract, "Invalid contract address")); IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); ... }
#0 - V-Staykov
2022-05-11T12:35:31Z
This will be solved by the solution of #58, because ultimately they both point to the same thing that the sendToCosmos function should work only with approved tokens.
#1 - V-Staykov
2022-05-17T12:54:07Z
We were actually not able to reproduce this PoC. When trying to call any function on an empty bytecode address, it reverts with an error that there is no such function. In our tests the same applied to OpenZeppelin's token.safeTransferFrom(from, to, amount). Are we missing something?
#2 - albertchon
2022-05-18T23:34:10Z
Agreed with @V-Staykov's finding - if the function does not exist on the contract (due to the contract not existing) the call reverts.
#3 - JeeberC4
2022-05-19T18:35:00Z
Creating as warden's QA Report as judge downgraded issue. Preserving original title: Knowledge of Cosmos tokens ERC20 address allows infinite transfers