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: 14/60
Findings: 3
Award: $653.55
🌟 Selected for report: 0
🚀 Solo Findings: 0
312.7392 USDC - $312.74
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L363-L371
function setMultiplier(address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager { if (multiplierValue > MAX_MULTIPLIER) revert NBV_MultiplierLimit(); NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId]; // set multiplier value multiplierData.multiplier = multiplierValue; emit MultiplierSet(tokenAddress, tokenId, multiplierValue); }
In NFTBoostVault.setMultiplier
, the latestVotingPower
should be updated to reflect the correct NFT multiplier for current votes and voter's registrations. Not updating multiplier can result in a window where voters have lower/higher than expected votes until updateVotingPower()
is called. This can result in 2 scenarios:
setMultiplier()
for decreasing multiplier by adding more tokens via addTokens()
and then vote first to side step reduction in voting power.
<br/>
Manual Analysis
In setMultiplier()
, update latestVotingPower by calling _syncVotingPower()
after updating multiplier.
function setMultiplier(address user, address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager { if (multiplierValue > MAX_MULTIPLIER) revert NBV_MultiplierLimit(); NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId]; // set multiplier value multiplierData.multiplier = multiplierValue; ++ NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[user]; ++ _syncVotingPower(user, registration); emit MultiplierSet(tokenAddress, tokenId, multiplierValue); }
Context
#0 - c4-pre-sort
2023-07-30T08:46:24Z
141345 marked the issue as duplicate of #160
#1 - c4-pre-sort
2023-08-01T09:15:13Z
141345 marked the issue as duplicate of #431
#2 - c4-judge
2023-08-11T16:05:58Z
0xean marked the issue as satisfactory
🌟 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
| Count | Title |
|:--:|:-------|:--:|
| [L-01] | ArcadeTreasury._spend()/_approve()
: Can spend tokens where thresholds has not been set |
| [L-02] | ARCDVestingVault.revokeGrant()
: If claiming and revoking grants occurs in same block, manager maybe DoS from revoking grants|
Total Low Risk Issues | 2 |
---|
| Count | Title |
|:--:|:-------|:--:|
| [N-01] | Add zero address checks for delegate()
|
| [N-02] | NFTBoostVault.setMultiplier()
: Add minimum multiplier check in setMultiplier
|
Total Non-Critical Issues | 2 |
---|
| Count | Title |
|:--:|:-------|:--:|
| [R-01] | ARCDVestingVault.revokeGrant()
: grant.latestVotingPower
to zero not needed in |
| [R-02] | ARCDVestingVault.revokeGrant()
: Check zero withdrawals |
Total Refactor Issues | 2 |
---|
ArcadeTreasury._spend()/_approve()
: Proposals can spend tokens where thresholds has not been sethttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L358
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L384
Proposals can spend Arcade tokens from token addresses that has yet to been set, potentially by-passing the intended spending limits. Consider adding a check in _spend()
and _approve()
to prevent this from occuring.
function _spend(address token, uint256 amount, address destination, uint256 limit) internal { ++ if (limit == 0) revert T_ThresholdNotSet; // check that after processing this we will not have spent more than the block limit uint256 spentThisBlock = blockExpenditure[block.number]; if (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = amount + spentThisBlock; // transfer tokens if (address(token) == ETH_CONSTANT) { // will out-of-gas revert if recipient is a contract with logic inside receive() payable(destination).transfer(amount); } else { IERC20(token).safeTransfer(destination, amount); } emit TreasuryTransfer(token, destination, amount); }
function _approve(address token, address spender, uint256 amount, uint256 limit) internal { ++ if (limit == 0) revert T_ThresholdNotSet; // check that after processing this we will not have spent more than the block limit uint256 spentThisBlock = blockExpenditure[block.number]; if (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); blockExpenditure[block.number] = amount + spentThisBlock; // approve tokens IERC20(token).approve(spender, amount); emit TreasuryApproval(token, spender, amount); }
ARCDVestingVault.revokeGrant()
: If claiming and revoking grants occurs in same block, manager maybe DoS from revoking grantshttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ARCDVestingVault.sol#L157-L186
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ARCDVestingVault.sol#L228-L253
If grant is revoked at the same block where claiming of grants is available, then revoking of grant will be side stepped and grant owners will receive arcade tokens for voting and proposing (delegatees if assigned).
Add a additional delay (1 block) before allowing claiming of grants.
delegate()
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ARCDVestingVault.sol#L260-L282
Voters can accidentally delegate voting power to zero-addresses causing temporary losing of voting power
function delegate(address to) external { ARCDVestingVaultStorage.Grant storage grant = _grants()[msg.sender]; ++ if (to == address(0)) revert AVV_AlreadyDelegated(); if (to == grant.delegatee) revert AVV_AlreadyDelegated(); History.HistoricalBalances memory votingPower = _votingPower(); uint256 oldDelegateeVotes = votingPower.loadTop(grant.delegatee); // Remove old delegatee's voting power and emit event votingPower.push(grant.delegatee, oldDelegateeVotes - grant.latestVotingPower); emit VoteChange(grant.delegatee, msg.sender, -1 * int256(grant.latestVotingPower)); // Note - It is important that this is loaded here and not before the previous state change because if // to == grant.delegatee and re-delegation was allowed we could be working with out of date state. uint256 newDelegateeVotes = votingPower.loadTop(to); // add voting power to the target delegatee and emit event votingPower.push(to, newDelegateeVotes + grant.latestVotingPower); // update grant delgatee info grant.delegatee = to; emit VoteChange(to, msg.sender, int256(uint256(grant.latestVotingPower))); }
Add zero address checks for delegate()
functions in ARCDVestingVault
similar to that in NFTBoostVault
.
NFTBoostVault.setMultiplier()
: Add minimum multiplier check in setMultiplier
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L363-L371
The minimum multiplier is 1e13 (100%). Consider adding a check to not allow ADMIN_ROLE setting voter's multiplier below 100% that can potentially unfairly reduce voting power
function setMultiplier(address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager { -- if (multiplierValue > MAX_MULTIPLIER) revert NBV_MultiplierLimit(); ++ if (multiplierValue > MAX_MULTIPLIER && multiplierValue < 1e3) revert NBV_MultiplierLimit(); NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId]; // set multiplier value multiplierData.multiplier = multiplierValue; emit MultiplierSet(tokenAddress, tokenId, multiplierValue); }
ARCDVestingVault.revokeGrant()
: grant.latestVotingPower
to zero not needed inIn ARCDVestingVault.revokeGrant()
, the reassignment of grant.latestVotingPower
to zero is not needed as this is already updated in _syncVotingPower()
.
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ARCDVestingVault.sol#L184
function revokeGrant(address who) external virtual onlyManager { // load the grant ARCDVestingVaultStorage.Grant storage grant = _grants()[who]; // if the grant has already been removed or no grant available, revert if (grant.allocation == 0) revert AVV_NoGrantSet(); // get the amount of withdrawable tokens uint256 withdrawable = _getWithdrawableAmount(grant); grant.withdrawn += uint128(withdrawable); token.safeTransfer(who, withdrawable); // transfer the remaining tokens to the vesting manager uint256 remaining = grant.allocation - grant.withdrawn; grant.withdrawn += uint128(remaining); token.safeTransfer(msg.sender, remaining); // update the delegatee's voting power _syncVotingPower(who, grant); // delete the grant grant.allocation = 0; grant.cliffAmount = 0; grant.withdrawn = 0; grant.created = 0; grant.expiration = 0; grant.cliff = 0; -- grant.latestVotingPower = 0; grant.delegatee = address(0); }
ARCDVestingVault.revokeGrant()
: Check zero withdrawalsIf withdrawable
is assigned to zero via _getWithdrawableAmount()
, consider reverting the function to save gas for manager calling the revokeGrant()
function.
https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ARCDVestingVault.sol#L165
function revokeGrant(address who) external virtual onlyManager { // load the grant ARCDVestingVaultStorage.Grant storage grant = _grants()[who]; // if the grant has already been removed or no grant available, revert if (grant.allocation == 0) revert AVV_NoGrantSet(); // get the amount of withdrawable tokens uint256 withdrawable = _getWithdrawableAmount(grant); ++ if (withdrawable == 0) revert CannotRevoke(); grant.withdrawn += uint128(withdrawable); token.safeTransfer(who, withdrawable); // transfer the remaining tokens to the vesting manager uint256 remaining = grant.allocation - grant.withdrawn; grant.withdrawn += uint128(remaining); token.safeTransfer(msg.sender, remaining); // update the delegatee's voting power _syncVotingPower(who, grant); // delete the grant grant.allocation = 0; grant.cliffAmount = 0; grant.withdrawn = 0; grant.created = 0; grant.expiration = 0; grant.cliff = 0; grant.latestVotingPower = 0; grant.delegatee = address(0); }
#0 - c4-judge
2023-08-10T22:40:53Z
0xean marked the issue as grade-c
#1 - nevillehuang
2023-08-14T18:48:00Z
Hi @0xean, any reason why this is unsatisfactory? Understand theres some formatting issues
#2 - 0xean
2023-08-15T17:16:46Z
@141345 scored the QA for this contest and will let him comment on the grading
#3 - 141345
2023-08-16T02:42:28Z
2 L(low) + 4 N(non-critical), total score is grade C.
grade is based on the score percentage of the top QA report.
#4 - c4-judge
2023-08-16T20:10:03Z
0xean marked the issue as grade-b
🌟 Selected for report: Sathish9098
Also found by: 0x3b, 0xnev, 3agle, BugBusters, DadeKuma, K42, Udsen, foxb868, ktg, kutugu, oakcobalt, peanuts, squeaky_cactus
319.2125 USDC - $319.21
Arcade.xyz is a borrow lending platform where NFTs are escrowed as collateral and its valuation is put up for loans. This audit focuses on the Arcade.xyz token-based on-chain governance system built with reference to the Council Framework.
The audit in scope can be separated into the following mechanisms:
Voting vaults are vaults where governance tokens are deposited to gain voting power for voting and/or proposing proposals. It supports delegation of full voting power to other parties, but does not support partial delegations.
This is the general, core community voting vault for governance, where a NFT (ReputationBadge.sol
) minted to voter can be attached to voting power to boost votes depending on multiplier set by protocol admins. Here is a quick summary
Entry
addNftAndDelegate()
: Deposit arcade governance tokens to vault with an option to add a owned ReputationBadge NFT that boosts votes by multiplier set by protocol admins. If not, the base multiplier is 1 and amount of governance tokens deposited will equate to voting power assigned to user himself or his chosen delegatee (automatically defaults to user if no delegatee is passed)Adjustments
addTokens()
: Allows additional deposit of governanace tokens to increase voting power of user himself or delegateeairDropReceive()
: Allows airdrop contract to add more tokens to a user to increase voting power of user himself or delegateedelegate()
: Allows regisration owner that originally deposited governance tokens to change delegatee that receives the user's voting powerupdateNFT()
: Allows regisration owner that originally deposited governance tokens to change the attached NFT, presumably to increase multiplier. Votes are updated after multiplier assigned to NFT is registered.Exit
withdraw()
: Allows full or partial withdrawals of governance tokens, with voting power subsequently decreased. If user performs a full withdrawal, all voting power is lost and his registration (voting details) is deleted.This is a specialized voting vault designed for early Arcade team members, contributors, and dev partners, that holds tokens in escrow subject to a vesting timeline. Both locked and unlocked tokens held by the vault contribute governance voting power, Since locked tokens are held by the ARCDVestingVault, they are not eligible for NFT boosts.
Entry
addGrantAndDelegate()
: Manager only function that sets a grant where voting power will be delegated to (either grant reciepient himself or his chosen delegatee). There is a cliff amount of tokens to encourage grant recipients to stay committed to the Arcade governance where a portion or cliff
amount of tokens is periodically unlocked for grant recipient usageAdjustments
revokeGrant()
: At any point of time, manager can revoke the full grant of grant recipient or any remaining unclaimed grant tokens, causing grant recipient or delegatee to lose all remaining voting powerdelegate()
: Allows grant recipient to change delegatee that receives his voting power assigned by managerExit
claim()
: Allows grant recipient to fully or partially claim unlocked cliff
tokens after cliff period has passed. Else it allows partial or full claiming of all tokens after grant expiration (vesting) period ends. The voting power of grant recipient or his delegatee is then decrementedThis vault is exactly identical to ARCDVestingVault, with the exception that grants cannot be revoked by revokeGrant()
once it is assigned via addGrantAndDelegate()
. This is presumably for founders/important stakeholders of Arcade protocol.
This is a specialized voting vault for voters that have met the minimum voting power set by the Arcade governance to be part of the Arcade governance General Steering Council (GSC).
Entry
proveMembership()
: For voters that have met the minimum threshold set by arcade governance, they will be able to be a member of Arcade governance GSC. This is achieved through:
Exit
kick()
: Any GSC member can be kicked by anybody once total voting power queried via registered vaults falls below minimum thresholds. This can be because of:
This is simply an NFT that any whitelisted voter can pay to mint and then added to the general community NFTBoostVault to increase voting power. The whitelisted address and mint price is managed by the BADGE_MANAGER_ROLE
.
This is simply the governance token that is used to determine voting power. Generally, amount of tokens deposited into voting vaults equals the voting power received (excluding multiplier from attached ReputationBadge NFT), with the exception of GSC members who votes in a separate voting contract and receives 1 vote after proving membership status.
This contract simply mints the tokens assigned to each stakeholder, with a initial maximum token supply of 100_000_000e18 tokens.
This contract allows airdrop of tokens to voters which can increase their voting power in NFTBoostVault.airDropReceive()
. 10_000_000e18 tokens is assigned for this feature.
This is the main contract where successful proposals from community or GSC voting can spend tokens within the treasury. Any type of ERC20 token and native ETH is supported to be held by treasury as long as spending threshold is set.
It allows small, medium and large approving and spending of tokens, with corresponding small, medium and large spending limits assigned to each token. It also has a block expenditure limit to limit spendings each block for successful proposals. Note: GSC are only authorize to spend or approve smaller spendings.
Since grant assignment is done by a trusted manager, consider allowing adjustment of grant to avoid having to revoke and assign again if grant amount is assigned wrongly or simply to decrease or increase amount of grants assigned to a grant recipient.
Consider adding a function to describe the status of a proposal and refactor logic of checks based on this statuses. This will make checking proposal status easy and introduce useful features for proposal editing and cancellation, along with fail-safe options to remove malicious proposals.
Many DAOS have a function to view the current status of the proposal, but this is missing in Arcade's voting contracts. Consider implementing and integrating with UI a feature and function to allow voters to check status of an proposal.
To avoid spamming of proposals, consider implementing a cap for the number of active proposals a proposer can propose at any given time. Many DAOs only allow a single active proposal at any given time.
Note: Only roles that can intentionally impact voters/users from losing voting power or funds are included
setMultiplier()
: Can change multiplier of any voter owning a ReputationBadge NFT. Since there is no minimum multiplier check, the multiplier can be set below 100% (less than 1e3)addGrantAndDelegate()
: Can add anybody to be assigned a grant giving the grant recipient or delegatee voting powerrevokeGrant()
: Revoke any grant at any time, preventing claiming of grant tokens and making grant recipient or delegatee lose voting powerwithdraw()
: withdraw governance tokens at anytime, potentially preventing grant recipients from receiving grants assignedsetManager()
: Can change address of manager role, preventing current manager role from executing privileged functionsunlock()
: Can lock or unlock withdrawals in NFTBoostVaults, preventing withdrawals of voter's governance tokens48 hours
#0 - c4-pre-sort
2023-07-31T16:39:31Z
141345 marked the issue as high quality report
#1 - c4-sponsor
2023-08-02T20:46:05Z
PowVT marked the issue as sponsor acknowledged
#2 - PowVT
2023-08-02T20:46:52Z
One action item I see in this report, but is a duplicate.
All seems to be information/ ack
#3 - c4-judge
2023-08-10T23:01:41Z
0xean marked the issue as grade-a