Platform: Code4rena
Start Date: 21/07/2023
Pot Size: $90,500 USDC
Total HM: 8
Participants: 60
Period: 7 days
Judge: 0xean
Total Solo HM: 2
Id: 264
League: ETH
Rank: 3/60
Findings: 4
Award: $8,181.31
🌟 Selected for report: 1
🚀 Solo Findings: 0
5771.7265 USDC - $5,771.73
Judge has assessed an item in Issue #513 as 2 risk. The relevant finding follows:
Issue 2
#0 - c4-judge
2023-08-10T20:21:26Z
0xean marked the issue as satisfactory
#1 - c4-judge
2023-08-10T20:21:47Z
0xean marked the issue as primary issue
#2 - c4-judge
2023-08-12T13:58:21Z
0xean marked the issue as selected for report
🌟 Selected for report: bin2chen
Also found by: 0xWaitress, caventa, ladboy233
1798.1148 USDC - $1,798.11
Lose of previous spending power for same address in TreasuryContract
the AracheTreasury, there are three spending relate function:
approveSmallSpend, approveMediumSpend, approveLargeSpend
both of these function call
_approve(token, spender, amount, spendThresholds[token].small);
or the approved amount is spendThresholds[token].medium or spendThresholds[token].large
Then inside the function _approve, approve a certain amount
IERC20(token).approve(spender, amount); emit TreasuryApproval(token, spender, amount);
However, if a spender approve a largeSpending amount, then approve the mdiumSpending amount, the unconsumed largeSpending amount is lost
for example largeSpending amount is 1000 ETH, mediumSpending amount is 300 ETH
a spender has 1000 ETH spending allowance after call approveLargeSpending
he never spend the allowance
then a proposal executes and caled approveMediumSpending
now the spender's largeSpending amount 1000 ETH is lost, he only has 300 ETH spending allowance left
Manual Review
Use increaseAllowance instead of calling approve
Token-Transfer
#0 - c4-pre-sort
2023-07-31T08:39:44Z
141345 marked the issue as duplicate of #85
#1 - c4-judge
2023-08-11T16:30:50Z
0xean marked the issue as satisfactory
🌟 Selected for report: Anirruth
Also found by: DadeKuma, Matin, MohammedRizwan, bart1e, giovannidisiena, ladboy233, rvierdiiev
589.8716 USDC - $589.87
Incorrectly assume the block generation time is 13.3 seconds, while the block generation in ethereum in 12 seconds per block,
result in a gradually delayed voting period
ArcadeGSCCoreVoting.sol is in-scope and it is inherit from the CoreVoting contract
contract ArcadeGSCCoreVoting is CoreVoting {
In CoreVoting Contract:
the DAY_IN_BLOCK is hardcoded to 6496,
// Assumes avg block time of 13.3 seconds. May be longer or shorter due // to ice ages or short term changes in hash power. // @audit // 12 seconds uint256 public constant DAY_IN_BLOCKS = 6496;
as the comment suggest, the code assume the avg block time is 13.3 seconds
In one day we assume it has 86400 seconds / 6496 block = 13.3 seconds
However, the block production time in post merge is 12 seconds
Then one can day can produce: 86400 / 12 = 7200 blocks
the parameter DAY_IN_BLOCKS is used when derving the lock duration and the voting time of the proposal
// minimum time a proposal must be active for before executing // Default to 3 days, this avoids weekend surprise proposals uint256 public lockDuration = DAY_IN_BLOCKS * 3; // The number of blocks after the proposal is unlocked during which // voting can continue. Max vote time = lockDuration + extraVoteTime // Default to ~5 days of blocks, ie 8 days max vote time uint256 public extraVoteTime = DAY_IN_BLOCKS * 5;
and the DAY_IN_BLOCKS is off by (7200 - 6496) = 704 blocks, which result in a much shorter lockDuration and voting time then expected
Manual Review
We recommend the protocol to use the block.timestamp (one day has 86400 seconds) instead of using the DAY_IN_BLOCK to measure the time elapse when deriving lockDuration and vote time in ArcadeGSCCoreVoting.sol
Other
#0 - c4-pre-sort
2023-07-29T13:47:33Z
141345 marked the issue as duplicate of #56
#1 - c4-judge
2023-08-11T16:35:05Z
0xean marked the issue as satisfactory
#2 - c4-judge
2023-08-14T16:26:09Z
0xean changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-08-14T16:29:25Z
0xean marked the issue as grade-b
#4 - c4-judge
2023-08-16T12:34:13Z
This previously downgraded issue has been upgraded by 0xean
#5 - c4-judge
2023-08-16T20:16:59Z
0xean marked the issue as not a duplicate
#6 - c4-judge
2023-08-16T20:17:21Z
0xean marked the issue as duplicate of #56
🌟 Selected for report: LaScaloneta
Also found by: 0xComfyCat, 0xDING99YA, 0xnev, ABA, BugBusters, DadeKuma, MohammedRizwan, QiuhaoLi, Sathish9098, Udsen, ak1, bart1e, immeas, koxuan, ladboy233, matrix_0wl, oakcobalt, squeaky_cactus, zhaojie
21.5977 USDC - $21.60
merkle leaf value should not be 64 bytes before hashing in MerkleReward Distribution
_syncVotingPower can revert in underflow in NFTBoostVault.sol and ARCDVestingVault.sol
merkle leaf value should not be 64 bytes before hashing
In merkle reward distribution code, we have to follow function:
function _validateWithdraw( uint256 amount, uint256 totalGrant, bytes32[] memory merkleProof ) internal { // Hash the user plus the total grant amount bytes32 leafHash = keccak256(abi.encodePacked(msg.sender, totalGrant)); // Verify the proof for this leaf require( MerkleProof.verify(merkleProof, rewardsRoot, leafHash), "Invalid Proof" ); // Check that this claim won't give them more than the total grant then // increase the stored claim amount require(claimed[msg.sender] + amount <= totalGrant, "Claimed too much"); claimed[msg.sender] += amount; }
note:
// Hash the user plus the total grant amount bytes32 leafHash = keccak256(abi.encodePacked(msg.sender, totalGrant)); // Verify the proof for this leaf require( MerkleProof.verify(merkleProof, rewardsRoot, leafHash), "Invalid Proof" );
we cancat the address msg.sender and totalGrant as merkle tree, which is 64 bytes exactly
In Openzepplein library, we have the following comments
* WARNING: You should avoid using leaf values that are 64 bytes long prior to * hashing, or use a hash function other than keccak256 for hashing leaves. * This is because the concatenation of a sorted pair of internal nodes in * the merkle tree could be reinterpreted as a leaf value.
The merkle tree intermediary nodes are just concatenated and hashed during verification, and since they are also of size bytes32/uint256 this type of collision is possible because the leaf of the tree is of the same form in this case (address, uint256) as the internal nodes.
then if the attacker control two msg.sender address + the totalGrant, he can claim the reward for free
Manual Review
Add one bytes to the hashed leaf
_syncVotingPower can revert in underflow in NFTBoostVault.sol
note this function:
function _syncVotingPower(address who, NFTBoostVaultStorage.Registration storage registration) internal { History.HistoricalBalances memory votingPower = _votingPower(); uint256 delegateeVotes = votingPower.loadTop(registration.delegatee); uint256 newVotingPower = _currentVotingPower(registration); // get the change in voting power. Negative if the voting power is reduced int256 change = int256(newVotingPower) - int256(uint256(registration.latestVotingPower)); // do nothing if there is no change if (change == 0) return; if (change > 0) { votingPower.push(registration.delegatee, delegateeVotes + uint256(change)); } else { // if the change is negative, we multiply by -1 to avoid underflow when casting votingPower.push(registration.delegatee, delegateeVotes - uint256(change * -1)); } registration.latestVotingPower = uint128(newVotingPower); emit VoteChange(who, registration.delegatee, change); }
We need to pay special attention to this line of code:
votingPower.push(registration.delegatee, delegateeVotes - uint256(change * -1));
it is possible that delegateeVotes - changes can revert in underflow
this only happens when we try to reduce the voting power
In _withdrawNFT if a NFT with high multipler is removed in NFTBoostVault.sol and cause the voting power to decrease
// update the delegatee's voting power based on multiplier removal _syncVotingPower(msg.sender, registration);
Or in ARCDVestingVault.sol when a admin try to revoke a user's grant
// update the grant's withdrawn amount if (amount == withdrawable) { grant.withdrawn += uint128(withdrawable); } else { grant.withdrawn += uint128(amount); withdrawable = amount; } // update the user's voting power _syncVotingPower(msg.sender, grant);
Manual Review
Before calling delegateeVotes - uint256(change * -1), make sure delegateeVotes > uint256(change * -1), otherwise just set to 0
#0 - 141345
2023-07-31T11:26:31Z
L-1 is duplicate of https://github.com/code-423n4/2023-07-arcade-findings/issues/423
L-2 might need to be escalated to medium duplicate of https://github.com/code-423n4/2023-07-arcade-findings/issues/540
#1 - c4-pre-sort
2023-08-01T14:38:04Z
141345 marked the issue as high quality report
#2 - liveactionllama
2023-08-02T17:43:04Z
After discussion with the lookout, removing the high quality
label here, simply to focus usage of that label on the top few QA reports.
#3 - c4-judge
2023-08-10T22:39:45Z
0xean marked the issue as grade-c
#4 - c4-judge
2023-08-14T16:29:23Z
0xean marked the issue as grade-b