Platform: Code4rena
Start Date: 25/08/2022
Pot Size: $75,000 USDC
Total HM: 35
Participants: 147
Period: 7 days
Judge: 0xean
Total Solo HM: 15
Id: 156
League: ETH
Rank: 28/147
Findings: 5
Award: $872.66
🌟 Selected for report: 2
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: GalloDaSballo, Guardian, Lambda, __141345__, cccz, csanuragjain, m9800, zzzitron
vote
allows a user to vote only once.
Because of this, if a person where to cast a vote, and then increase their VOTE
balance, their vote would only reflect the old balance, without any way to vote again for the activeProposal.
if (userVotesForProposal[activeProposal.proposalId][msg.sender] > 0) { revert UserAlreadyVoted(); }
VOTES
, I forgot to claim some from the old proposal.vote
Because userVotesForProposal[activeProposal.proposalId][msg.sender]
already tracks the vote amount, just subtract the old votes and add a new one, allowing people to re-cast the vote if their VOTE
balance changes
#0 - fullyallocated
2022-09-04T02:51:19Z
Duplicate of #275
#1 - 0xean
2022-09-19T15:26:17Z
this is slightly different from #275 - but close enough I agree it should be duped.
🌟 Selected for report: GalloDaSballo
347.2615 DAI - $347.26
Rewards for Heart beat
are sent via _issueReward
function _issueReward(address to_) internal { rewardToken.safeTransfer(to_, reward); emit RewardIssued(to_, reward); }
The function doesn't check for available tokens e.g.
min(reward, rewardToken.balanceOf(address(this)));
In case of calling withdrawUnspentRewards
/// @inheritdoc IHeart function withdrawUnspentRewards(ERC20 token_) external onlyRole("heart_admin") { token_.safeTransfer(msg.sender, token_.balanceOf(address(this))); }
Because the function withdraws the entire amount, the heart will stop until a caller incentive is deposited again.
While a profitable searches will stop calling the Heart without an incentive, allowing the heart to beat when no rewards are available is preferable to having it self-DOS until a DAO aligned caller donates rewardToken
or the DAO deals with the lack of tokens.
Add a check for available tokens
min(reward, rewardToken.balanceOf(address(this)));
#0 - Oighty
2022-09-07T20:43:30Z
Agree based on the anti-DOS characteristics of using a min operation.
🌟 Selected for report: GalloDaSballo
347.2615 DAI - $347.26
Because VOTES
can be minted by voter_admin
, and there is no cap on totalSupply, the voter_admin
has the privileged ability to mint as many VOTES
as they want in order to get any proposal to pass or veto it.
voter_admin
voter_admin
netVotes * 100 < VOTES.totalSupply() * EXECUTION_THRESHOLD
check to passVOTES
to selfAdd a total supply cap to VOTES
#0 - fullyallocated
2022-09-04T03:11:45Z
This is possible but will not happen in a production environment because we're using this for internal testing.
#1 - 0xean
2022-09-19T22:30:12Z
Given the scope of the contracts the warden's were asked to review I think this issue is valid. While I understand that the voter_admin is trusted, I don't think users expect the admin to be able to bypass any votes results in this manner.
🌟 Selected for report: zzzitron
Also found by: 0x040, 0x1f8b, 0x52, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSky, 0xSmartContract, 0xkatana, 8olidity, Aymen0909, Bahurum, BipinSah, Bnke0x0, CRYP70, CertoraInc, Ch_301, Chandr, Chom, CodingNameKiki, Deivitto, DimSon, Diraco, ElKu, EthLedger, Funen, GalloDaSballo, Guardian, IllIllI, JansenC, Jeiwan, Lambda, LeoS, Margaret, MasterCookie, PPrieditis, PaludoX0, Picodes, PwnPatrol, RaymondFam, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, StevenL, The_GUILD, TomJ, Tomo, Trust, Waze, __141345__, ajtra, ak1, apostle0x01, aviggiano, bin2chen, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch13fd357r0y3r, cloudjunky, cryptphi, csanuragjain, d3e4, datapunk, delfin454000, devtooligan, dipp, djxploit, durianSausage, eierina, enckrish, erictee, fatherOfBlocks, gogo, grGred, hansfriese, hyh, ignacio, indijanc, itsmeSTYJ, ladboy233, lukris02, martin, medikko, mics, natzuu, ne0n, nxrblsrpr, okkothejawa, oyc_109, p_crypt0, pfapostol, prasantgupta52, rajatbeladiya, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, shenwilly, sikorico, sorrynotsorry, tnevler, tonisives, w0Lfrum, yixxas
54.4097 DAI - $54.41
The idea of Modules and Policies is brilliant!
Most of the codebase is well written and well thought out, the one exception to me was Governance which I don't believe will withstand an adversarial environment.
Minor Code smells are listed below rated via the following standard
VOTES
from Governance will break accountingWhile burning VOTES
from the Governance
contract is questionable, the code has no check to prevent that.
function burnFrom(address wallet_, uint256 amount_) external permissioned { _burn(wallet_, amount_); }
Because Governance
and VOTES.transferFrom
relies on a "use -> refund" pattern, losing even 1 wei of token will cause reclaimVotes
to revert, effectively denying a user from being able to vote again.
Voting can be denied by simply burning their VOTES
hence why I set the severity to Low as this is a Ban with extra steps as the voter_admin
can just burn the votes from the user
repayLoan
allows only the caller to repay their own debt, this can create situations in which insolvency or a smart contract bug prevent from making the TRSRY whole.
A straightforward solution would be to allow anyone to repay the loan on behalf of a specific address
/// @notice Lets an address with debt repay their loan. function repayLoan(ERC20 token_, uint256 amount_) external nonReentrant { if (reserveDebt[token_][msg.sender] == 0) revert TRSRY_NoDebtOutstanding(); // Deposit from caller first (to handle nonstandard token transfers) uint256 prevBalance = token_.balanceOf(address(this)); token_.safeTransferFrom(msg.sender, address(this), amount_);
By allowing other addresses a softer approach to repaying debt can be achieved.
This avoids having to manually reset the debt.
_activatePolicy
is non CEI conformantThe function _activatePolicy
will perform an external call to policy_.configureDependencies()
and then it will change storage.
// Add policy to list of active policies activePolicies.push(policy_); getPolicyIndex[policy_] = activePolicies.length - 1; // Record module dependencies Keycode[] memory dependencies = policy_.configureDependencies(); uint256 depLength = dependencies.length; for (uint256 i; i < depLength; ) { Keycode keycode = dependencies[i]; moduleDependents[keycode].push(policy_); getDependentIndex[keycode][policy_] = moduleDependents[keycode].length - 1; unchecked { ++i; } }
I wasn't able to find any exploit as the function is privileged
get
for a state changing functiongetXyz
is typically used for retrieving values from view functions, however in the case of TRSRY
the function is used to receive a loan.
Because the codebase already uses get
for view functions, I'd recommend renaming the function below to receiveLoan
or just loan
to keep the coding convention
function getLoan(ERC20 token_, uint256 amount_) external permissioned {
function ensureContract(address target_) view { uint256 size; assembly { size := extcodesize(target_) } if (size == 0) revert TargetNotAContract(target_); }
Can be changed to
target_.code.length
Throughout the codebase, most setters emit events, however setActiveStatus
doesn't
isActive = activate_;
While setters emit events, the constructor doesn't, this may cause issues with tracking, e.g. theGraph as an event is for the initial setting is not emitted
constructor() { executor = msg.sender; admin = msg.sender; }
You may instead want to emit only when a valid action is executed Or add a comment to the function mentioning that
As it stands the code will emit even if the action data is not recognized
emit ActionExecuted(action_, target_);
🌟 Selected for report: pfapostol
Also found by: 0x040, 0x1f8b, 0x85102, 0xDjango, 0xNazgul, 0xNineDec, 0xSmartContract, 0xkatana, Amithuddar, Aymen0909, Bnke0x0, CertoraInc, Chandr, CodingNameKiki, Deivitto, Dionysus, Diraco, ElKu, Fitraldys, Funen, GalloDaSballo, Guardian, IllIllI, JC, JansenC, Jeiwan, LeoS, Metatron, Noah3o6, RaymondFam, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Saintcode_, Shishigami, Sm4rty, SooYa, StevenL, Tagir2003, The_GUILD, TomJ, Tomo, Waze, __141345__, ajtra, apostle0x01, aviggiano, bobirichman, brgltd, c3phas, cRat1st0s, carlitox477, cccz, ch0bu, chrisdior4, d3e4, delfin454000, djxploit, durianSausage, erictee, exolorkistis, fatherOfBlocks, gogo, grGred, hyh, ignacio, jag, karanctf, kris, ladboy233, lukris02, m_Rassska, martin, medikko, natzuu, ne0n, newfork01, oyc_109, peiw, rbserver, ret2basic, robee, rokinot, rvierdiiev, sikorico, simon135, tnevler, zishansami
32.5943 DAI - $32.59
Codebase is gas conscious and basic gas saving advice is followed pretty thoroughly, below are listed a few extra optimizations, sorted by efficacy
updateMovingAverage
- 200+ gas saved per function call// Calculate new moving average if (currentPrice > earliestPrice) { _movingAverage += (currentPrice - earliestPrice) / numObs; } else { _movingAverage -= (earliestPrice - currentPrice) / numObs; } // Push new observation into storage and store timestamp taken at observations[nextObsIndex] = currentPrice; lastObservationTime = uint48(block.timestamp); nextObsIndex = (nextObsIndex + 1) % numObs;
Can be changed to
// Calculate new moving average /// @audit Use unchecked as you already checked for overflow unchecked { if (currentPrice > earliestPrice) { _movingAverage += (currentPrice - earliestPrice) / numObs; } else { _movingAverage -= (earliestPrice - currentPrice) / numObs; } // Push new observation into storage and store timestamp taken at /// @audit also unchecked addition /// @audit Cache the value of `nextObsIndex` to save an SLOAD uint32 cachedNextObsIndex = nextObsIndex; observations[cachedNextObsIndex] = currentPrice; lastObservationTime = uint48(block.timestamp); nextObsIndex = (cachedNextObsIndex + 1) % numObs; }
ensureValidKeycode(Module(target_).KEYCODE());
Can instead cache keycode = Module(target_).KEYCODE(); and pass it to the next function `installModule(target, keycode);
Saving over 100 gas (STATICCALL + cost of processing the string for the return value)
if (updatedAt < block.timestamp - 3 * uint256(observationFrequency))
Cache the value of observationFrequency
to save 100 gas
You can wrap the code below in unchecked to gain around 80 gas;
uint256 instructionsId = ++totalInstructions;
for (uint256 step; step < instructions.length; ) {