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: 9/55
Findings: 5
Award: $1,806.99
🌟 Selected for report: 0
🚀 Solo Findings: 0
In Gravity.sol#updateValset()
, while the signatures of the current validators are verified and >= powerThreshold is checked, there is one important validation should be done: check the cumulative power of the new validator set to ensure the contract has sufficient power to actually pass a vote.
With this check missing, a malformed update with the new validator set unable to pass any vote even with the signatures of all the validators, the Gravity.sol
contract will be bricked.
Given:
When the 3 validators: A, B, C voted to remove ValidatorD with updateValset()
, the transcation will success and as a result the contract will be bricked and can no longer execute any transcation that requires a vote from the validators.
Consider adding the following code into updateValset()
before L320:
// Check cumulative power to ensure the contract has sufficient power to actually pass a vote uint256 cumulativePower = 0; for (uint256 i = 0; i < _newValset.powers.length; i++) { cumulativePower = cumulativePower + _newValset.powers[i]; if (cumulativePower > constant_powerThreshold) { break; } } require( cumulativePower > constant_powerThreshold, "Submitted validator set signatures do not have enough power." );
#0 - V-Staykov
2022-05-11T11:05:24Z
Duplicate of #123
🌟 Selected for report: p_crypt0
Also found by: 0x1337, GermanKuber, IllIllI, WatchPug, csanuragjain, danb, dirk_y, kirk-baird
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); }
The addresses hasAdminRole
in cudosAccessControls
can call withdrawERC20()
and take all the locked funds in the Gravity.sol
contract.
We believe this is extremely dangerous and unnecessary.
A malicious or compromised admin can run all the locked funds from the contract and cause huge loss to the users.
Consider removing this method.
#0 - maptuhec
2022-05-11T08:25:49Z
Duplicate of #14
There are ERC20 tokens that charge fee for every transfer()
or transferFrom()
.
In the current implementation, sendToCosmos()
assumes that the received amount is the same as the transfer amount, and uses it to emit SendToCosmosEvent
event.
As a result, when bridging the token back from Cosmos, it may revert because of insufficient balance.
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 ); }
Consider comparing before and after balance to get the actual transferred amount:
function sendToCosmos( address _tokenContract, bytes32 _destination, uint256 _amount ) public nonReentrant { uint256 preBalance = IERC20(_tokenContract).balanceOf(address(this)); IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); uint256 postBalance = IERC20(_tokenContract).balanceOf(address(this)); state_lastEventNonce = state_lastEventNonce.add(1); emit SendToCosmosEvent( _tokenContract, msg.sender, _destination, postBalance.sub(preBalance), state_lastEventNonce ); }
#0 - mlukanova
2022-05-10T14:49:59Z
Duplicate of #3
🌟 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
113.5584 USDC - $113.56
for (uint256 i = 0; i < _currentValidators.length; i++) { // If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation // (In a valid signature, it is either 27 or 28) if (_v[i] != 0) { // Check that the current validator has signed off on the hash require( verifySig(_currentValidators[i], _theHash, _v[i], _r[i], _s[i]), "Validator signature does not match." ); // Sum up cumulative power cumulativePower = cumulativePower + _currentPowers[i]; // Break early to avoid wasting gas if (cumulativePower > _powerThreshold) { break; } } }
In L244, even though it's unlikely to overflow in this particular case, we still recommend using SafeMath instead.
It's a best practice to use the latest compiler version.
The specified minimum compiler version is quite old. Older compilers might be susceptible to some bugs. We recommend changing the solidity version pragma to the latest version to enforce the use of an up to date compiler.
List of known compiler bugs and their severity can be found here: https://etherscan.io/solcbuginfo
function manageWhitelist( address[] memory _users, bool _isWhitelisted ) public onlyWhitelisted { for (uint256 i = 0; i < _users.length; i++) { require( _users[i] != address(0), "User is the zero address" ); whitelisted[_users[i]] = _isWhitelisted; } emit WhitelistedStatusModified(msg.sender, _users, _isWhitelisted); }
SendToCosmosEvent._destination
can be changed to string
rather than bytes32
to support different chains in the future to indicate almost anything else we mayevent SendToCosmosEvent( address indexed _tokenContract, address indexed _sender, bytes32 indexed _destination, uint256 _amount, uint256 _eventNonce );
#0 - V-Staykov
2022-05-10T14:10:52Z
[L] Arithmetic operations without using SafeMath may over/underflow - on the Cosmos side we have a limit of 10 billion total voting power, so overflowing is not an issue here.
🌟 Selected for report: GermanKuber
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xNazgul, 0xf15ers, 0xkatana, AlleyCat, CertoraInc, Dravee, Funen, GimelSec, IllIllI, JC, MaratCerby, WatchPug, Waze, defsec, delfin454000, ellahi, gzeon, hake, hansfriese, ilan, jonatascm, nahnah, oyc_109, peritoflores, rfa, robee, simon135, slywaters, sorrynotsorry
68.1519 USDC - $68.15
[S]: Suggested optimation, save a decent amount of gas without compromising readability;
[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;
[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.
++i
is more efficient than i++
Using ++i
is more gas efficient than i++
, especially in for loops.
For example:
function manageWhitelist( address[] memory _users, bool _isWhitelisted ) public onlyWhitelisted { for (uint256 i = 0; i < _users.length; i++) { require( _users[i] != address(0), "User is the zero address" ); whitelisted[_users[i]] = _isWhitelisted; } emit WhitelistedStatusModified(msg.sender, _users, _isWhitelisted); }
for (uint256 i = 0; i < _currentValidators.length; i++) {
for (uint256 i = 0; i < _newValset.validators.length; i++) {
Outdated versions of OpenZeppelin library are used.
It's a best practice to use the latest version of libraries.
New versions of OpenZeppelin libraries can be more gas effeicant.
For exmaple:
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
Unused code increase deploy codesize and add gas cost.
function testCheckValidatorSignatures( address[] memory _currentValidators, uint256[] memory _currentPowers, uint8[] memory _v, bytes32[] memory _r, bytes32[] memory _s, bytes32 _theHash, uint256 _powerThreshold ) public pure { checkValidatorSignatures( _currentValidators, _currentPowers, _v, _r, _s, _theHash, _powerThreshold ); }
Every reason string takes at least 32 bytes.
Use short reason strings that fits in 32 bytes or it will become more expensive.
Instances include:
require( cumulativePower > _powerThreshold, "Submitted validator set signatures do not have enough power." );
require( _newValset.valsetNonce > _currentValset.valsetNonce, "New valset nonce must be greater than the current nonce" );
require( makeCheckpoint(_currentValset, state_gravityId) == state_lastValsetCheckpoint, "Supplied current validators and powers do not match checkpoint." );
require( isOrchestrator(_currentValset, msg.sender), "The sender of the transaction is not validated orchestrator" );
require( state_lastBatchNonces[_tokenContract] < _batchNonce, "New batch nonce must be greater than the current nonce" );
require( block.number < _batchTimeout, "Batch timeout must be greater than the current block height" );
require( makeCheckpoint(_currentValset, state_gravityId) == state_lastValsetCheckpoint, "Supplied current validators and powers do not match checkpoint." );
require( isOrchestrator(_currentValset, msg.sender), "The sender of the transaction is not validated orchestrator" );
require( state_invalidationMapping[_args.invalidationId] < _args.invalidationNonce, "New invalidation nonce must be greater than the current nonce" );
require( makeCheckpoint(_currentValset, state_gravityId) == state_lastValsetCheckpoint, "Supplied current validators and powers do not match checkpoint." );
require( isOrchestrator(_currentValset, msg.sender), "The sender of the transaction is not validated orchestrator" );
require( cumulativePower > _powerThreshold, "Submitted validator set signatures do not have enough power." );
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Instances include:
Gravity.sol#manageWhitelist()
Gravity.sol#checkValidatorSignatures()
Gravity.sol#isOrchestrator()
Gravity.sol#submitBatch()
Gravity.sol#submitLogicCall()
Gravity.sol#constructor()
hhttps://github.com/code-423n4/2022-05-cudos/blob/de39cf3cd1f1e1cf211819b06d4acf6a043acda0/solidity/contracts/CosmosToken.sol#L5
uint256 MAX_UINT = 2**256 - 1;
// These are set once at initialization bytes32 public state_gravityId; uint256 public state_powerThreshold;
Considering that state_gravityId
should not change, changing it to immutable variable instead of storage variable can save gas.
CosmosERC20.sol
use immutable for ERC20 decimals()
can save gasimmutable
is much cheaper in gas compared to storage
.
Consider changing to:
pragma solidity ^0.6.6; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract CosmosERC20 is ERC20 { uint256 MAX_UINT = 2**256 - 1; uint8 private immutable decimals; constructor( address _gravityAddress, string memory _name, string memory _symbol, uint8 _decimals ) public ERC20(_name, _symbol) { decimals = _decimals; _mint(_gravityAddress, MAX_UINT); } function decimals() public view override returns (uint8) { return decimals; } }
For public functions, the input parameters are copied to memory automatically which costs gas. If a function is only called externally, making its visibility as external will save gas because external functions' parameters are not copied into memory and are instead read from calldata directly.
For example:
function deployERC20( string memory _cosmosDenom, string memory _name, string memory _symbol, uint8 _decimals ) public { // ... }
function updateValset( // The new version of the validator set ValsetArgs memory _newValset, // The current validators that approve the change ValsetArgs memory _currentValset, // These are arrays of the parts of the current validator's signatures uint8[] memory _v, bytes32[] memory _r, bytes32[] memory _s ) public nonReentrant { // ... }
function submitBatch ( // The validators that approve the batch ValsetArgs memory _currentValset, // These are arrays of the parts of the validators signatures uint8[] memory _v, bytes32[] memory _r, bytes32[] memory _s, // The batch of transactions uint256[] memory _amounts, address[] memory _destinations, uint256[] memory _fees, uint256 _batchNonce, address _tokenContract, // a block height beyond which this batch is not valid // used to provide a fee-free timeout uint256 _batchTimeout ) public nonReentrant { // ... }
function submitLogicCall( // The validators that approve the call ValsetArgs memory _currentValset, // These are arrays of the parts of the validators signatures uint8[] memory _v, bytes32[] memory _r, bytes32[] memory _s, LogicCallArgs memory _args ) public nonReentrant { // ... }