Platform: Code4rena
Start Date: 14/04/2022
Pot Size: $75,000 USDC
Total HM: 8
Participants: 72
Period: 7 days
Judge: Jack the Pug
Total Solo HM: 2
Id: 110
League: ETH
Rank: 19/72
Findings: 3
Award: $789.58
🌟 Selected for report: 1
🚀 Solo Findings: 0
643.8625 USDC - $643.86
https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L143 https://github.com/code-423n4/2022-04-badger-citadel/blob/18f8c392b6fc303fe95602eba6303725023e53da/src/StakedCitadelVester.sol#L109
When vest
is called by xCTDL vault, the previous amount will re-lock according to the new vesting timeline. While this is as described in L127, claimableBalance
might revert due to underflow if vesting[recipient].claimedAmounts
> 0 because the user will need to vest the claimedAmounts
again which should not be an expected behavior as it is already vested.
vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts + _amount; vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration;
uint256 locked = vesting[recipient].lockedAmounts; uint256 claimed = vesting[recipient].claimedAmounts; if (block.timestamp >= vesting[recipient].unlockEnd) { return locked - claimed; } return ((locked * (block.timestamp - vesting[recipient].unlockBegin)) / (vesting[recipient].unlockEnd - vesting[recipient].unlockBegin)) - claimed;
Reset claimedAmounts on new vest
vesting[recipient].lockedAmounts = vesting[recipient].lockedAmounts - vesting[recipient].claimedAmounts + _amount; vesting[recipient].claimedAmounts = 0 vesting[recipient].unlockBegin = _unlockBegin; vesting[recipient].unlockEnd = _unlockBegin + vestingDuration;
#0 - GalloDaSballo
2022-04-23T00:54:41Z
@dapp-whisperer wdyt?
#1 - shuklaayush
2022-04-26T21:37:32Z
I think this is valid and was fixed in https://github.com/Citadel-DAO/citadel-contracts/pull/44
#2 - jack-the-pug
2022-05-30T06:40:08Z
I'm downgrading this to Medium as there are no funds directly at risk, but a malfunction and leak of value. The user will have to wait for a longer than expected time to claim their vested funds.
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0xDjango, 0xkatana, AmitN, CertoraInc, Dravee, Funen, Hawkeye, Jujic, MaratCerby, Picodes, Ruhum, SolidityScan, TerrierLover, TomFrenchBlockchain, TrungOre, VAD37, Yiko, berndartmueller, cmichel, csanuragjain, danb, defsec, delfin454000, dipp, ellahi, fatherOfBlocks, georgypetrov, gs8nrv, gzeon, horsefacts, hubble, hyh, ilan, jah, joestakey, kebabsec, kenta, kyliek, m9800, minhquanym, oyc_109, p_crypt0, peritoflores, rayn, reassor, remora, rfa, robee, scaraven, securerodd, shenwilly, sorrynotsorry, tchkvsky, teryanarmen, z3s
91.3943 USDC - $91.39
tokenOutPrice
can be decreased but earlier buyer still pay the higher priceKnightingRound
./src/SupplySchedule.sol:159: // TODO: Require this epoch is in the future. What happens if no data is set? (It just fails to mint until set) ./src/Funding.sol:15: * TODO: Better revert strings ./src/Funding.sol:61: // TODO: we should conform to some interface here ./src/Funding.sol:183: // TODO: Check gas costs. How does this relate to market buying if you do want to deposit to xCTDL? ./src/KnightingRound.sol:14: * TODO: Better revert strings ./src/GlobalAccessControl.sol:106: /// TODO: Add string -> hash EnumerableSet to a new RoleRegistry contract for easy on-chain viewing.
🌟 Selected for report: Dravee
Also found by: 0v3rf10w, 0x1f8b, 0xAsm0d3us, 0xBug, 0xDjango, 0xNazgul, 0xkatana, CertoraInc, Cityscape, Funen, Hawkeye, IllIllI, MaratCerby, SolidityScan, TerrierLover, TomFrenchBlockchain, Tomio, TrungOre, bae11, berndartmueller, csanuragjain, defsec, delfin454000, ellahi, fatherOfBlocks, gs8nrv, gzeon, horsefacts, ilan, jah, joestakey, joshie, kebabsec, kenta, nahnah, oyc_109, rayn, rfa, robee, saian, securerodd, simon135, slywaters, sorrynotsorry, tchkvsky, teryanarmen, z3s
54.333 USDC - $54.33
> 0
is less efficient than != 0
for uint in require conditionRef: https://twitter.com/GalloDaSballo/status/1485430908165443590
src/StakedCitadelVester.sol:138: require(_amount > 0, "StakedCitadelVester: cannot vest 0"); src/Funding.sol:170: require(_assetAmountIn > 0, "_assetAmountIn must not be 0"); src/Funding.sol:322: require(amount > 0, "nothing to sweep"); src/Funding.sol:340: require(amount > 0, "nothing to claim"); src/Funding.sol:424: require(_citadelPriceInAsset > 0, "citadel price must not be zero"); src/Funding.sol:452: require(_citadelPriceInAsset > 0, "citadel price must not be zero"); src/CitadelMinter.sol:343: require(length > 0, "CitadelMinter: no funding pools"); src/KnightingRound.sol:172: require(_tokenInAmount > 0, "_tokenInAmount should be > 0"); src/KnightingRound.sol:215: require(tokenOutAmount_ > 0, "nothing to claim"); src/KnightingRound.sol:411: require(amount > 0, "nothing to sweep");
We can use the following function to save gas on float multiplications
// out = x * y unchecked{/} z function fmul(uint256 x, uint256 y, uint256 z) internal pure returns(uint256 out){ assembly{ if iszero(eq(div(mul(x,y),x),y)) {revert(0,0)} out := div(mul(x,y),z) } }
e.g.
src/StakedCitadel.sol:300: return (token.balanceOf(address(this)) * toEarnBps) / MAX_BPS; src/StakedCitadel.sol:852: uint256 fee = (amount * feeBps) / MAX_BPS; src/test/MintAndDistribute.t.sol:88: uint expectedToFunding = expectedMint * fundingBps / MAX_BPS; src/test/MintAndDistribute.t.sol:100: uint expectedToStakers = expectedMint * stakingBps / MAX_BPS; src/test/MintAndDistribute.t.sol:111: uint expectedToLockers = expectedMint * stakingBps / MAX_BPS; src/CitadelMinter.sol:191: lockingAmount = (mintable * cachedLockingBps) / MAX_BPS; src/CitadelMinter.sol:215: stakingAmount = (mintable * cachedStakingBps) / MAX_BPS; src/CitadelMinter.sol:228: fundingAmount = (mintable * cachedFundingBps) / MAX_BPS;
function claimableBalance(address recipient) public view returns (uint256) { uint256 locked = vesting[recipient].lockedAmounts; uint256 claimed = vesting[recipient].claimedAmounts; uint256 begintime = vesting[recipient].unlockBegin; uint256 endtime = vesting[recipient].unlockEnd; if (block.timestamp >= endtime) { return locked - claimed; } return ((locked * (block.timestamp - begintime)) / (endtime - begintime)) - claimed; }
require( funding.assetCumulativeFunded + _assetAmountIn <= funding.assetCap, "asset funding cap exceeded" ); unchecked{ funding.assetCumulativeFunded = funding.assetCumulativeFunded + _assetAmountIn; }
unchecked{ totalTokenIn = totalTokenIn + _tokenInAmount; }
Solidity ^0.8.4 allow the use of custom errors to optimize gas usage. https://blog.soliditylang.org/2021/04/21/custom-errors/