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: 15/55
Findings: 3
Award: $871.71
π Selected for report: 0
π Solo Findings: 0
https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L595-L609
Some ERC tokens have a fee on each transfer
. The protocol doesnβt handle the fee when transferring this kind of ERC20 tokens, leading to the inconsistent amount of token actually received in the contract. Validators on the Cudos will mint more tokens to the destination address than it received in sendToCosmos()
.
In sendToCosmos
, _amount
is used in SendToCosmosEvent
. However, if _tokenContract
collects a fee on every transfer. The actual amount of token received is not equal to _amount
https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L595-L609
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 ); }
vim
Use balanceAfter - balanceBefore
rather than using amount
directly:
function retrieveTokens(address sender, uint256 amount) public { uint256 balanceBefore = deflationaryToken.balanceOf(address(this)); deflationaryToken.transferFrom(sender, address(this), amount); uint256 balanceAfter = deflationaryToken.balanceOf(address(this)); amount = balanceAfter.sub(balanceBefore); totalTokenTransferred += amount; }
Token Transfer Calculation Accuracy
in this article: https://blog.chain.link/defi-security-best-practices/
#0 - mlukanova
2022-05-11T11:09:49Z
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
302.6663 USDC - $302.67
https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L337 https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L406 https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L510
updateValset
operation doesn't set a timeout, leading to the signed newCheckpoint
(new Valset) being used indefinitely.
It doesn't check the operation timeout in the updateValset
function, so the new Valset can be delayed and change the current Valset abruptly at any time.
Additionally, submitBatch
and submitLogicCall
function check the checkpoint:
require( makeCheckpoint(_currentValset, state_gravityId) == state_lastValsetCheckpoint, "Supplied current validators and powers do not match checkpoint." );
An attacker can hold the newCheckpoint
and call updateValset
abruptly to change validators list to deny all valid operations, which has been signed by previous _currentValset.validators
.
vim
Use timeout in updateValset
:
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 { // CHECKS + require(block.number < _newValset.updateTimeout, "Timed out"); ...
#0 - V-Staykov
2022-05-11T13:40:57Z
The update valset function is being called by all the orchestrators and basically the first of them to pass is being stored. So if a validator decides to hold a updateValset transaction he doesn't do any harm oher than making other validators incur the fees of the transaction. Also each valsetupdate has an once which would prevent overiding an update that was sent after it, since its nonce would be higher.
#1 - albertchon
2022-05-18T22:25:44Z
Agreed with @V-Staykov
π 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
66.5732 USDC - $66.57
The for loop has no overflow risk of i
. Use an unchecked block to save gas.
Gravity.sol 128: for (uint256 i = 0; i < _users.length; i++) { 233: for (uint256 i = 0; i < _currentValidators.length; i++) { 263: for (uint256 i = 0; i < _newValset.validators.length; i++) { 453: for (uint256 i = 0; i < _amounts.length; i++) { 568: for (uint256 i = 0; i < _args.transferAmounts.length; i++) { 579: for (uint256 i = 0; i < _args.feeAmounts.length; i++) { 660: for (uint256 i = 0; i < _powers.length; i++) {
Use unchecked
blocks to avoid overflow checks, or use ++i
rather than i++
if you don't use unchecked blocks.
for (uint256 i = 0; i < length; ) { ... unchecked { ++i; } }
https://github.com/code-423n4/2022-05-cudos/blob/main/solidity/contracts/Gravity.sol#L59-L61
// These are set once at initialization bytes32 public immutable state_gravityId; uint256 public immutable state_powerThreshold;