Platform: Code4rena
Start Date: 12/08/2022
Pot Size: $35,000 USDC
Total HM: 10
Participants: 126
Period: 3 days
Judge: Justin Goro
Total Solo HM: 3
Id: 154
League: ETH
Rank: 2/126
Findings: 8
Award: $4,482.50
🌟 Selected for report: 4
🚀 Solo Findings: 1
🌟 Selected for report: CertoraInc
Also found by: 0x1f8b, 0xSky, CodingNameKiki, DecorativePineapple, Noah3o6, Waze, jonatascm, oyc_109, pedr02b2, peritoflores
314.0226 USDC - $314.02
https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L425-L428 https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L485-L488 https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L546 https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L657 https://github.com/code-423n4/2022-08-fiatdao/blob/fece3bdb79ccacb501099c24b60312cd0b2e4bb2/contracts/VotingEscrow.sol#L676
Some ERC20 tokens functions don't return a boolean, for example USDT, BNB, OMG. So the VotingEscrow
contract simply won't work with tokens like that as the token
.
The USDT's transfer
and transferFrom
functions doesn't return a bool, so the call to these functions will revert although the user has enough balance and the VotingEscrow
contract won't work, assuming that token is USDT.
Manual auditing - VS Code, some hardhat tests and me :)
Use the OpenZepplin's safeTransfer
and safeTransferFrom
functions
#0 - lacoop6tu
2022-08-16T08:52:10Z
In our case the token is a BalancerV2 Pool Token which returns the boolean
#1 - gititGoro
2022-08-31T02:37:45Z
This should be acknowledged, not disputed, since there is nothing in documentation suggesting the token is inherently safe to use.
#2 - elnilz
2022-09-09T07:16:26Z
@gititGoro it's a no-issue in our specific case bc we will use VotingEscrow in combination with token
which returns bool upon transfer/transferFrom. So at best this is a QA issue bc we should document that. some wardens actually asked us about what token we will be using pointing out the issue.
Now even if you'd want to award wardens who reported the issue it should then be a Med Risk bc if VotingEscrow is deployed with an unsafe token
ppl would simply not be able to deposit into the contract but no funds would be at risk.
#3 - elnilz
2022-09-09T15:08:08Z
fyi, even though we dont think this is an issue we will make use of safeTransfer and safeTransferFrom so its a helpful submission nonetheless
#4 - gititGoro
2022-09-11T06:57:07Z
It's tokens like BNB that led me to maintain the high risk rating. For BNB, transferFrom returns a bool but transfer doesn't. In other words, users can stake but not unstake on any protocol that doesn't use safeTransfer.
I agree that wardens should contact sponsors but it's not a channel we can really monitor. So although the net result is a documentation fix rather than a bug fix, it's a documentation fix informed by the identification of a potentially show stopping bug rather than something like "Comment typo: it should be Bitcoin, not bit coin".
🌟 Selected for report: CertoraInc
Also found by: cccz, csanuragjain, jonatascm, scaraven
389.9867 USDC - $389.99
Some ERC20 tokens implemented so a fee is taken when transferring them, for example STA
and PAXG
. The current implementation of the VotingEscrow
contract will mess up the accounting of the locked amounts if token
will be a token like that, what will lead to a state where users won't be able to receive their funds.
This will happen because the value that is added to the locked amount is not the actual value received by the contract, but the value supplied by the user (the value which the fee is taken from).
The STA
token burns 1% of the value provided to the transfer
function, which means the recipient gets only 99% of the transferred asset. Let's assume that token
is the address of the STA
token.
createLock(100 * 10**18, unlockTime)
.100 * 10**18
, while the actual amount that was received by the contract is 99 * 10**18
.100 * 10**18
). This might succeed due to other users locking too, so the transferred tokens will be taken from "their tokens", but in the end there will be users left without an option to withdraw their funds, because the balance of the contract will be less than the locked amount that the contract is trying to transfer.Manual auditing - VS Code and me :)
Calculate the amount to add to the locked amount by the difference between the balances before and after the transfer instead of using the supplied value.
#0 - lacoop6tu
2022-08-16T14:28:28Z
In our case, the token will be BalancerV2 Pool Token , which has no fee on transfer, in case someone else would like to fork this contract and use it, that fix will be required
#1 - gititGoro
2022-09-02T02:26:39Z
Given that the warden couldn't know the use of Balancer only tokens, the severity will still be upheld.
#2 - elnilz
2022-09-09T09:12:30Z
@gititGoro so should we explicitly exclude all weird implementations of e.g. ERC20 in the future in the contest docs? I mean there are other wild examples of ERC20 implementations that someone could point out would cause problems with this contract. I am not trying to discount the work of any warden here but I think the correct response here as well as for #231 would be to improve docs stating which ERC20 implementations are safe to use in combination with VotingEscrow
#3 - gititGoro
2022-09-10T22:58:00Z
@elnilz This topic is very important to get right and the more it's debated, the more it's clear that there is no one size fits all answer. When I sponsored a contest, I only figured out after the fact the sorts of things that would have reduced unhelpful warden submissions, through no fault of C4. My suggestion is that we curate an open source optional questionnaire for sponsors. The more detail the sponsor gives a priori, the more we can mark unhelpful issues as invalid. As an example, the tools I use to deploy my dapps do not allow me to accidentally omit addresses in constructor arguments. So wardens who warn me that I don't check for the zero address in my constructors are not helping me. The questionnaire would cover many common case submissions such as that.
In your case, I had to dance a bit of a fine line: on the one hand, the wardens are not wrong in the event that you're unaware of token design nuance and so they shouldn't be penalized. On the other hand, this is a DAO and the wardens should at least suspect that the choice of token is a central decision. In the end, since both parties have good points to make and there's no clear decider, I chose to side with the wardens since it doesn't incur any additional cost to you as the sponsor and since it would seem unfair to penalize the wardens for an honest report with no flaws at the correct severity level.
🌟 Selected for report: Aymen0909
Also found by: 0xSky, 0xf15ers, CertoraInc, JohnSmith, auditor0517, bin2chen, csanuragjain, scaraven, tabish, wagmi, yixxas
The increaseUnlockTime
function sends a wrong unlock time to the _checkpoint
function in the oldLocked
variable - the locked_
variable (which is the new LockedBalance) is copied into the oldLocked
variable (which is supposed to be the previous LockedBalance), but instead of assigning the previous unlock time to it, the new unlock time is assigned to it which basically leaves it like the new LockedBalance.
if (locked_.delegatee == msg.sender) { // Undelegated lock require(oldUnlockTime > block.timestamp, "Lock expired"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; _checkpoint(msg.sender, oldLocked, locked_); }
That messes up the checkpoint accounting and should be fixed.
As I mentioned before, the bug can be seen in these lines of the increaseUnlockTime
function:
if (locked_.delegatee == msg.sender) { // Undelegated lock require(oldUnlockTime > block.timestamp, "Lock expired"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; _checkpoint(msg.sender, oldLocked, locked_); }
Here we can see that the end
field of the oldLocked
variable is set to the new unlock time instead of the old one.
I modified the test that checks the increaseUnlockTime
to print the difference between the total supply and the user's balance, and it seems that the total supply after the call to increaseUnlockTime
is less than the balance of the user.
The test:
it("allows user to extend lock", async () => { await goToNextUnixWeekStart(); const bobSnapBefore = await snapshotData(bob); var balanceBefore = await votingLockup.balanceOf(bob.address); var totalSupplyBefore = await votingLockup.totalSupply(); await votingLockup .connect(bob) .increaseUnlockTime(start.add(ONE_YEAR)); const bobSnapAfter = await snapshotData(bob); var balanceAfter = await votingLockup.balanceOf(bob.address); var totalSupplyAfter = await votingLockup.totalSupply(); console.log("total supply - balance of bob before: "); console.log(totalSupplyBefore.sub(balanceBefore)); console.log("total supply - balance of bob after: "); console.log(totalSupplyAfter.sub(balanceAfter)); });
The output:
total supply - balance of bob before: BigNumber { _hex: '0x0e76231788418c10d3', _isBigNumber: true } total supply - balance of bob after: BigNumber { _hex: '-0x0c91bf21e7fb2d2bda', _isBigNumber: true }
The output after correcting the value passed to the _checkpoint
function:
total supply - balance of bob before: BigNumber { _hex: '0x0e76231788418c10d3', _isBigNumber: true } total supply - balance of bob after: BigNumber { _hex: '0x0e7622f9d3c1222826', _isBigNumber: true }
Another thing I noticed here is the fact that the differences between the total supply and the user's balance are not equal also in the fixed version, but I couldn't understand why (I asked @elnilz and he said he'll check it)
Manual auditing - VS Code and me :)
Replace oldLocked.end = unlock_time;
on line 513 with oldLocked.end = oldUnlockTime;
#0 - lacoop6tu
2022-08-16T09:11:09Z
Duplicate of #217
🌟 Selected for report: CertoraInc
Also found by: 0x1f8b, DecorativePineapple, cRat1st0s, carlitox477, joestakey, ladboy233, reassor, rvierdiiev
142.1501 USDC - $142.15
The unsafe casting to int128 variable can cause its value to be different from the correct value. For example in the createLock function, the addition to the locked amount variable is done by locked_.amount += int128(int256(_value))
. In that case, if _value
is greater than type(int128).max
which is 2**127 - 1
, then the accounting will be wrong and the amount that will be added to locked_.amount
will be less than the amount of token that will be transferred from the user. Then the user won't be able to withdraw the tokens that he transferred, and they'll be stuck in the contract forever.
2**128
tokens. She calls createLock(2**128, unlockTime)
with the time she wants to lock for.locked_.amount += int128(int256(_value))
, which actually does nothing to the locked_.amount
variable and it remains 0. That's because when casting int128(int256(2**128))
truncates to 0, and that leaves the locked amount unchanged but the tokens are transferred.Manual auditing - VS Code and me :)
Make sure that the values fit in the variables you are trying to assign them to when casting variables to smaller types.
#0 - lacoop6tu
2022-08-16T08:59:08Z
This is true but doesn't apply in our case, we use a BalV2 Pool Token which would never reach those values in terms of existing supply
#1 - gititGoro
2022-09-01T23:57:54Z
The use of Balancer tokens doesn't preclude numbers above 128bit. In the BalancerV2 source code, all amounts are in uint256. However, the wisespread practice of standard Ethereum tokens makes the likelihood of even encountering a token balance above 128 bits is negligible and Balancer does scale down big tokens if the other tokens in the pool are lower when minting.
Marking this as high risk is simply not realistic. This and its duplicates will be downgraded to medium risk (2) as it's a type of technicality that will have no bite in reality.
🌟 Selected for report: PwnedNoMore
Also found by: CertoraInc, ak1, scaraven
541.6482 USDC - $541.65
The virtual balance of a user is calculated using 2 values - the amount that is delegated to that user, and his lock period. When calling the increaseUnlockTime
function, we want to checkpoint the user's data as long as he doesn't have any funds. This is because if he has funds, the change to the lock time will effect his virtual balance.
The code checks that the user doesn't have any funds by checking that he is not delegating his funds to someone else. This check can be seen here:
if (locked_.delegatee == msg.sender) { // Undelegated lock require(oldUnlockTime > block.timestamp, "Lock expired"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; // oldLocked.end = oldUnlockTime; _checkpoint(msg.sender, oldLocked, locked_); }
However, this check is wrong, because a user can delegate his funds but still has funds delegated to him, so the change to the unlock time will effect his virtual balance and is needed to be accounted by the _checkpoint
function.
locked[alice].delegatee == bob
locked[bob].delegatee == charlie
Now:
locked[alice].delegated
is 0 because all of Alice's funds are delegated to Bob and she doesn't have any funds delegated to her.locked[bob].delegated
is X because all of Bob's funds are delegated to Charlie, but Bob has Alice's funds delegated to him.locked[charlie].delegated
is Y because Charlie doesn't have any funds locked, but he has Bob's funds delegated to him.increaseUnlockTime
with the new unlock time he wants.
In that call, the virtual balance of Bob is changed, because although he delegated his funds to Charlie, he still has Alice's funds delegated to him and the change to the unlock time will effect it. However, because of the wrong condition shown before, the _checkpoint
function won't be called and his virtual balance won't be updated - locked[bob].delegatee == charlie
.Manual auditing - VS Code and me :)
The correct condition here needs to check whether the user has ANY funds in the system, not if the user has delegated his funds or not. This can be checked by this condition:
if (locked_.delegated > 0) { // Undelegated lock require(oldUnlockTime > block.timestamp, "Lock expired"); LockedBalance memory oldLocked = _copyLock(locked_); oldLocked.end = unlock_time; // oldLocked.end = oldUnlockTime; _checkpoint(msg.sender, oldLocked, locked_); }
#0 - lacoop6tu
2022-08-16T08:34:44Z
Duplicate of #318
🌟 Selected for report: CertoraInc
2972.0064 USDC - $2,972.01
An attacker can use a flashloan and the quitLock function to achieve a large amount of votes for one transaction. It can, depends on the implementation of the modules that will use this contract, be used to pass malicious proposals or exploit any feature that depends on the voting balance.
We assume here that there is a contract that provides a flashloan (for simplicity without fees, but can also work with fees, just requires the attacker to provide a larger amount of tokens) for the token that is used by the VotingEscrow contract.
VotingEscrow
contract (of course assigning all the values of the variables).attack
function with the amount he wants to take as a flashloan (he will need to cover a penalty based on that amount).attack
function calculates the penalty and transfers it from the attacker.flashloan
function is called, which provides the tokens to the contract and then calls the flashloanCallback
with the lent amount.flashloanCallback
function creates a lock with the amount received from the flashloan for a week (the unlock time can be changed to achieve larger votes balance, but it must be considered when calculating the penalty).contract VotingEscrowAttack { IERC20 constant token = IERC20(0x...); // the token used in the VotingEscrow contract IVotingEscrow constant votingEscrow = IVotingEscrow(0x...); // the address of the VotingEscrow contract uint constant WEEK = 1 weeks; uint constant MAXTIME = 365 days; uint constant MAXPENALTY = 10**18; uint constant PENALTY_RATE = (WEEK * MAXPENALTY) / MAXTIME; IFlashLoan constant flashloanContract = IFlashLoan(0x...); // the address of the flashloan provider function attack(uint amount) external { uint penalty = (amount * PENALTY_RATE) / (10 ** 18); token.transferFrom(msg.sender, penalty); // assuming no flashloan fee IFlashLoan.flashloan(token, amount); } function flashloanCallback(uint amount) { votingEscrow.createLock(amount, block.timestamp + WEEK); // create a lock for a week with a very large of token // do whatever you want with your large amount of votes votingEscrow.quitLock(); token.transfer(msg.sender, amount); // pay back the flashloan } }
Manual auditing - VS Code and me :)
Think about implementing a mechanism that prevents users from creating a lock and quitting it in the same transaction, that way attackers won't be able to use flashloan in order to achieve large voting power. However, regular loans will still be a problem with that fix, and if this isn't a wanted behavior addition fix is needed to be thought of.
#0 - elnilz
2022-08-18T09:56:07Z
we actually could mitigate the issue by restricting increasing voting power and quitting in the same block. this would make the implementation safer
but even then, the severity is rather 2 as funds are not directly at risk
#1 - gititGoro
2022-09-03T03:13:02Z
The scope of voting power is unclear. It may be that a proposal becomes possible that enables large funds transfers. For this reason, severity is maintained.
#2 - elnilz
2022-09-09T08:48:36Z
Did review the economics of this attack and they are the same than without quitLock:
First, note that both balanceOf (voting power) and fee paid in quitLock are proportional to (locked.end-block.timestamp)/MAXTIME
or remaining lock duration. In other words, in order to gain voting power, user also accepts higher quitLock fee.
For max voting power user locks with MAXTIME. When quitting, this also results in the loss of all her locked tokens. Because the voting power decreases linearly with remaining lock duration, the amount of tokens that need be locked in order to achieve same voting power increases with lower lock durations. This results in the same quitLock fee as if the user would chose max lock duration or if user would not be able to quit at all.
See this interactive graph and play with the t (remaining lock duration) and N (tokens locked) sliders: https://www.desmos.com/calculator/yjby9zempb
Thus, quitLock doesn't change the economics of governance attacks and so we dispute this issue.
#3 - gititGoro
2022-09-10T22:47:17Z
Upon reconsideration, it appears the dampening effect of the penalty ensures that this FlashLoan attack is only really viable at very low impact levels. The larger the attack, the more the cost of the attack will exceed any benefit. The only case where this wouldn't be the case is when a small vote can tip the balance of an important decision which is unlikely in a gauge style vote. But even if a threshold emerges, the attacker may as well just get the votes through normal channels.
Nonetheless, the vector is only dampened, not eradicated and so I'll be downgrading this to Medium severity.
#4 - elnilz
2022-09-12T07:16:57Z
thanks for reconsidering, Id also be curious about feedback from CertoraInc as its been reported by the user.
the implications of the above analysis are, however, that the cost of an attack is the same as with Curve's VotingEscrow which doesn't implement a quitLock function. So unless the attack vector on Curve itself is also a medium severity this issue should be invalidated.
🌟 Selected for report: oyc_109
Also found by: 0x1f8b, 0x52, 0xDjango, 0xLovesleep, 0xNazgul, 0xNineDec, 0xbepresent, 0xmatt, 0xsolstars, Aymen0909, Bahurum, Bnke0x0, CertoraInc, Chom, CodingNameKiki, DecorativePineapple, Deivitto, Dravee, ElKu, Funen, GalloDaSballo, IllIllI, JC, JohnSmith, Junnon, KIntern_NA, Lambda, LeoS, MiloTruck, Noah3o6, PaludoX0, RedOneN, Respx, ReyAdmirado, Rohan16, RoiEvenHaim, Rolezn, Ruhum, Sm4rty, TomJ, Vexjon, Waze, Yiko, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, auditor0517, bin2chen, bobirichman, brgltd, bulej93, byndooa, c3phas, cRat1st0s, cryptphi, csanuragjain, d3e4, defsec, delfin454000, djxploit, durianSausage, ellahi, erictee, exd0tpy, fatherOfBlocks, gogo, jonatascm, ladboy233, medikko, mics, natzuu, neumo, p_crypt0, paribus, pfapostol, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, sach1r0, saneryee, seyni, sikorico, simon135, sseefried, wagmi, wastewa
29.8943 USDC - $29.89
Use accept ownership to avoid transferring ownership to a non-existing address. In the current implementation, if the transferOwnership function is called with an invalid address, the ownership will be transferred to this address and it can DoS the system. It will be better to make the new owner call a function before actually transferring the ownership, in order to validate that the new owner's address is valid and reachable.
Lines 257-259 are redundant - lines 257-259 checks a condition and assigns a value to userPointHistory[_addr][uEpoch + 1]
if the condition is true. However, a few lines after this assignment (in line 264), another value is assigned to userPointHistory[_addr][uEpoch + 1]
, which simply override the previous value. The previous value is not used between these two assignment, so the first assignment is redundant.
Typo on line 270 - "by" is written instead of "be"
Unspecific compiler version pragma - pragmas should be locked to a specific compiler version, to avoid contracts getting deployed using a different version, which may have a greater risk of undiscovered bugs.
🌟 Selected for report: IllIllI
Also found by: 0x040, 0x1f8b, 0xDjango, 0xHarry, 0xLovesleep, 0xNazgul, 0xNineDec, 0xSmartContract, 0xackermann, 0xbepresent, 2997ms, Amithuddar, Aymen0909, Bnke0x0, CRYP70, CertoraInc, Chom, CodingNameKiki, Deivitto, Dravee, ElKu, Fitraldys, Funen, GalloDaSballo, JC, JohnSmith, Junnon, LeoS, Metatron, MiloTruck, Noah3o6, NoamYakov, PaludoX0, RedOneN, Respx, ReyAdmirado, Rohan16, Rolezn, Ruhum, Sm4rty, SooYa, SpaceCake, TomJ, Tomio, Waze, Yiko, __141345__, a12jmx, ajtra, ak1, apostle0x01, asutorufos, bobirichman, brgltd, bulej93, c3phas, cRat1st0s, carlitox477, chrisdior4, csanuragjain, d3e4, defsec, delfin454000, djxploit, durianSausage, ellahi, erictee, fatherOfBlocks, gerdusx, gogo, ignacio, jag, ladboy233, m_Rassska, medikko, mics, natzuu, newfork01, oyc_109, paribus, pfapostol, rbserver, reassor, ret2basic, robee, rokinot, rvierdiiev, sach1r0, saian, sashik_eth, sikorico, simon135
15.0735 USDC - $15.07
Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition is met. Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc. An even better and gas efficient approach will be to use Solidity Custom Errors instead of revert strings.
Add 1 to uEpoch
in line 256 when initializing it instead of calculating uEpoch + 1
multiple times
Cache decimals
in the constructor
instead of accessing the storage twice
Current code:
constructor( address _owner, address _penaltyRecipient, address _token, string memory _name, string memory _symbol ) { // ... decimals = IERC20(_token).decimals(); require(decimals <= 18, "Exceeds max decimals"); // ... }
Optimized implementation:
constructor( address _owner, address _penaltyRecipient, address _token, string memory _name, string memory _symbol ) { // ... uint _decimals = IERC20(_token).decimals(); require(_decimals <= 18, "Exceeds max decimals"); decimals = _decimals; // ... }
Cache penaltyRecipient
in collectPenalty
to avoid accessing the storage twice
Current version:
function collectPenalty() external { uint256 amount = penaltyAccumulated; penaltyAccumulated = 0; require(token.transfer(penaltyRecipient, amount), "Transfer failed"); emit CollectPenalty(amount, penaltyRecipient); }
Optimized version:
function collectPenalty() external { uint256 amount = penaltyAccumulated; penaltyAccumulated = 0; address _penaltyRecipient = penaltyRecipient; require(token.transfer(_penaltyRecipient, amount), "Transfer failed"); emit CollectPenalty(amount, _penaltyRecipient); }
Reduce the size of end
in the LockedBalance
struct so the struct will fit in 2 words instead of 3 (end is a timestamp and if we'll reach the maximum value of uint96
the contract probably won't be active due to the limited size of the Point arrays)
Current struct:
struct LockedBalance { int128 amount; uint256 end; int128 delegated; address delegatee; }
Optimized struct:
// -------------- first word ----------------- --------------- second word ------------ // [ amount - 128 bits | delegated - 128 bits] [ delegatee - 160 bits | end - 96 bits ] struct LockedBalance { // amount and delegated are zipped to one word int128 amount; int128 delegated; // delegatee and end are zipped to the second word address delegatee; uint96 end; }
No need to copy the locked balance in _delegate
(all of its calls are in the end of the calling function, so the copied value won't actually be used after the function call)
Move line 506 before line 503 in order to avoid accessing locked_.end
twice
Use Shift right/left instead of division/multiplication - in lines 720 and 744 you can optimize the calculation uint256 mid = (min + max + 1) / 2;
by using shifting. The optimized version will look like this uint256 mid = (min + max + 1) >> 1;
(right shifting by i is like dividing by 2**i
, and left shifting by i is like multiplying by 2**i
).
Use != 0
instead of > 0
for unsigned integer comparison - this can be seen in multiple places in the code:
Loops can be optimized in several ways. Let's take for example the loop in the _checkpoint
function.
for (uint256 i = 0; i < 255; i++) { // Hopefully it won't happen that this won't get used in 5 years! // If it does, users will be able to withdraw but vote weight will be broken iterativeTime = iterativeTime + WEEK; int128 dSlope = 0; if (iterativeTime > block.timestamp) { iterativeTime = block.timestamp; } else { dSlope = slopeChanges[iterativeTime]; } int128 biasDelta = lastPoint.slope * int128(int256((iterativeTime - lastCheckpoint))); lastPoint.bias = lastPoint.bias - biasDelta; lastPoint.slope = lastPoint.slope + dSlope; // This can happen if (lastPoint.bias < 0) { lastPoint.bias = 0; } // This cannot happen - just in case if (lastPoint.slope < 0) { lastPoint.slope = 0; } lastCheckpoint = iterativeTime; lastPoint.ts = iterativeTime; lastPoint.blk = initialLastPoint.blk + (blockSlope * (iterativeTime - initialLastPoint.ts)) / MULTIPLIER; // when epoch is incremented, we either push here or after slopes updated below epoch = epoch + 1; if (iterativeTime == block.timestamp) { lastPoint.blk = block.number; break; } else { pointHistory[epoch] = lastPoint; } }
We can do multiple things here:
uint256 i;
instead of uint256 i = 0;
. There is another redundant initialization of dSlope to 0.iterativeTime
as the loop index (and limited it by its initial value + 256 weeks)
So after all these changes, the code will look something like this:for (uint256 i; i < 255; ) { // Hopefully it won't happen that this won't get used in 5 years! // If it does, users will be able to withdraw but vote weight will be broken iterativeTime = iterativeTime + WEEK; int128 dSlope; if (iterativeTime > block.timestamp) { iterativeTime = block.timestamp; } else { dSlope = slopeChanges[iterativeTime]; } int128 biasDelta = lastPoint.slope * int128(int256((iterativeTime - lastCheckpoint))); lastPoint.bias = lastPoint.bias - biasDelta; lastPoint.slope = lastPoint.slope + dSlope; // This can happen if (lastPoint.bias < 0) { lastPoint.bias = 0; } // This cannot happen - just in case if (lastPoint.slope < 0) { lastPoint.slope = 0; } lastCheckpoint = iterativeTime; lastPoint.ts = iterativeTime; lastPoint.blk = initialLastPoint.blk + (blockSlope * (iterativeTime - initialLastPoint.ts)) / MULTIPLIER; // when epoch is incremented, we either push here or after slopes updated below ++epoch; if (iterativeTime == block.timestamp) { lastPoint.blk = block.number; break; } else { pointHistory[epoch] = lastPoint; } unchecked { ++i; } }
If you'll choose to use `` as the loop index, it will look like this:
uint timeLimit = iterativeTime + WEEK >> 8; // iterativeTime + WEEK * 256 for (; iterativeTime < timeLimit; ) { // Hopefully it won't happen that this won't get used in 5 years! // If it does, users will be able to withdraw but vote weight will be broken unchecked { iterativeTime = iterativeTime + WEEK; // if it will overflow, the calculation of timeLimit would have overflow too } int128 dSlope; if (iterativeTime > block.timestamp) { iterativeTime = block.timestamp; } else { dSlope = slopeChanges[iterativeTime]; } int128 biasDelta = lastPoint.slope * int128(int256((iterativeTime - lastCheckpoint))); lastPoint.bias = lastPoint.bias - biasDelta; lastPoint.slope = lastPoint.slope + dSlope; // This can happen if (lastPoint.bias < 0) { lastPoint.bias = 0; } // This cannot happen - just in case if (lastPoint.slope < 0) { lastPoint.slope = 0; } lastCheckpoint = iterativeTime; lastPoint.ts = iterativeTime; lastPoint.blk = initialLastPoint.blk + (blockSlope * (iterativeTime - initialLastPoint.ts)) / MULTIPLIER; // when epoch is incremented, we either push here or after slopes updated below ++epoch; if (iterativeTime == block.timestamp) { lastPoint.blk = block.number; break; } else { pointHistory[epoch] = lastPoint; } }
There are more loops in the contract that can be optimized using the same optimizations.