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: 45/60
Findings: 1
Award: $253.08
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 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
253.0813 USDC - $253.08
0x00
If merkle root is set to 0x00
could leave the system in an insecure state where system will accept any message that it has never seen before and process it as if it were genuine.
This has happen before in the nomad bridge incident.
The Nomad team initialized the trusted root to be 0x00. To be clear, using zero values as initialization values is a common practice.
Instances:
delegate()
can be called with no voting powerThe delegate()
functions in ARCDVestingVault
and NFTBoostVault
can be called by anyone, despite them not having any voting power.
This leads to changes in the storage variables, like pushing empty entries to the votingPower
, and setting a delegatee
, despite not delegating anything.
As it will set a registration.delegatee
, it will bypass the validations if (registration.delegatee == address(0)) revert NBV_NoRegistration();
on addTokens()
, and updateNft()
. Making it possible to call those functions without "initializing" the corresponding registration storage via addNftAndDelegate()
.
This can lead to unexpected effects.
Instances:
Verify that _currentVotingPower(registration) > 0
pure
functions should be reading or writing memoryFrom solidity lang pure functions
Pure functions promise not to read from or modify the state.
Instances:
ArcadeToken
The ArcadeToken
has a very important Minter role which can be changed via a setMinter()
function.
In order to prevent the role from mistakenly being transferred to an address that cannot handle it (e.g. due to a typo in the address), require that the recipient of the new minter role actively accept via a contract call of its own.
Instances:
NFTBoostVault::updateNft()
allows to deposit any NFTaddNftAndDelegate
reverts when depositing any random NFT because of the if (multiplier == 0) revert NBV_NoMultiplierSet();
in _registerAndDelegate()
.
But updateNft()
does not have that check, and allows anyone to update and deposit any NFT, leading to a multiplier of 0, and thus a voting power of 0, despite the user having deposited any other tokens.
This might also be combined with other issues for a bigger severity issue. We recommend that the same check to revert on multiplier == 0
is applied on updateNft()
.
Instances:
token
has hooks the grant owner can revert when admin try to revoke roleA evil granted owner can avoid being revoke
, he can revert triggering a DOS to avoid manager to revoke role.
token
has hooks the manager could reenter to revokeGrant
draining the contractA token with hook could leave a reentrancy issue, letting manager to draind funds.
Knowledge of pre-images might allow manipulation of specific mapping parameters by crafting special keys. This is possible if a mapping parameter's storage slot lands on a specific slot.
It might be best to decrement them by one so that finding the pre-image would be harder.
Instances:
Example:
bytes32 typehash = keccak256("mapping(address => AddressUint)"); bytes32 offset = keccak256(abi.encodePacked(typehash, name)); assembly { data.slot := sub(offset, 1) // keccak256(NAME) - 1, to avoid preimage attack }
Also please review this eip-1967
tokenId
in ReputationBadge
and NftBoostVault
do not match, and can lead to tokens not being usable as multipliersReputationBadge
tokens are meant to be used as multipliers of voting power in NftBoostVault
.
Users can mint badges with tokenId
up to uint256
, but NftBoostVault
only allows to set tokenId
up to uint128
to work as multipliers.
Tokens with tokenId > type(uint128).max
won't be able to be used as multipliers.
tokenId
in ReputationBadge
(uint256):
function mint( address recipient, @> uint256 tokenId, uint256 amount, uint256 totalClaimable, bytes32[] calldata merkleProof ) external payable {
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/nft/ReputationBadge.sol#L100
tokenId
in NftBoostVault
(uint128):
@> function setMultiplier(address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager {
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L363
Consider setting the same tokenId
type for the expected NFTs to be used in the contract.
tokenId == 0
The check on NFTBoostVault.sol#L472 can be easily bypass if the ERC1155 token id is 0
:
if (_tokenAddress != address(0) && _tokenId != 0) {
POC:
// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../contracts/NFTBoostVault.sol"; import "../contracts/token/ArcadeToken.sol"; contract ExampleTest is Test { NFTBoostVault public boostVault; ArcadeToken public token; function setUp() public { vm.roll(10); token = new ArcadeToken(address(this), address(this)); boostVault = new NFTBoostVault(IERC20(address(token)), 1, address(this), address(this)); } function testDepositAnyERC1155() public { // create a fake erc1155 (could also happen with a ) FakeERC1155 f = new FakeERC1155(); token.approve(address(boostVault),100); boostVault.addNftAndDelegate( 1, 0, // uint128 tokenId, address(f), //address tokenAddress, address(0xdead) // delegate ); token.transfer(address(0x02), 1); vm.prank(address(0x02)); token.approve(address(boostVault),100); vm.prank(address(0x02)); boostVault.addNftAndDelegate( 1, 0, // uint128 tokenId, address(f), //address tokenAddress, address(0xdead01) ); vm.prank(address(0xdead)); boostVault.delegate(address(0x01)); vm.prank(address(0xdead01)); boostVault.delegate(address(0x01)); } } contract FakeERC1155 { fallback() external {} }
Some ERC20 tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example Tether (USDT)'s approve() function will revert if the current approval is not zero, to protect against front-running changes of approvals.
Instances:
NFTBoostVault
sets an unused initialized
value in storageThe "initialized"
storage variable is set on the constructor, but not used by the NFTBoostVault
, nor other contract, and can't be accessed by any public or external method.
Recommendation: Verify if it is actually needed, or remove it in case it is not.
Storage.set(Storage.uint256Ptr("initialized"), 1); // @audit check
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L91
ARCDVestingVault
inherits twice from HashedStorageReentrancyBlock
ARCDVestingVault
inherits both from HashedStorageReentrancyBlock
, and BaseVotingVault
.
But BaseVotingVault
is already inheriting from HashedStorageReentrancyBlock
, as well
Remove the unncessary inheritance from HashedStorageReentrancyBlock
on ARCDVestingVault
.
Instances:
#0 - 141345
2023-07-30T15:45:39Z
L-11 is duplicate of https://github.com/code-423n4/2023-07-arcade-findings/issues/113
#1 - c4-pre-sort
2023-08-01T14:33:33Z
141345 marked the issue as high quality report
#2 - c4-sponsor
2023-08-02T23:15:29Z
PowVT marked the issue as sponsor confirmed
#3 - PowVT
2023-08-02T23:15:55Z
There are some good valid fixes in this report, we will address.
#4 - c4-judge
2023-08-10T22:43:16Z
0xean marked the issue as selected for report
#5 - c4-judge
2023-08-10T23:22:16Z
0xean marked the issue as grade-a
#6 - 0xean
2023-08-15T19:44:30Z
L03 - looks invalid and includes out of scope contracts. L04 - is largely already covered in the bot report, this is slightly different only its location in the codebase so doesn't hurt to include in report L11 - is in the bot report already
Otherwise severities look reasonable to me.
#7 - thebrittfactor
2023-08-30T20:11:44Z
Just a note that C4 is excluding the invalid entries from the official report.