30921 - [SC - Low] Referential assignment causes incorrect block i...

Submitted on May 8th 2024 at 07:08:16 UTC by @NinetyNineCrits for Boost | Alchemix

Report ID: #30921

Report type: Smart Contract

Report severity: Low

Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol

Impacts:

  • Contract fails to deliver promised returns, but doesn't lose value

Description

Brief/Intro

Due to referential assignment of struct variables, the block interpolation is done incorrectly. Since the blocknumbers of that struct are currently not used this does not have a direct impact, beyond storing incorrect values (which may cause issues on future use)

Vulnerability Details

The VotingEscrow._checkpoint function does this assignment:

Point memory initialLastPoint = lastPoint;

This behaves like references in other languages such as Java, where if you manipulate a property on either variables, you would change the data on the underlying instance and both variables would then use the new value. A minimal foundry test to showcase this:

struct TestStruct {
    uint256 a;
    uint256 b;
}

function testReference() public {
    TestStruct memory testStruct = TestStruct(1, 2);
    TestStruct memory testStructReference = testStruct;

    testStruct.a = 3;
    console2.log("testStructReference.a", testStructReference.a); //@note prints 3
}

The same thing happens in _checkpoint where lastPoint.ts is changed and initialLastPoint.ts then uses that value:

lastPoint.ts = _time;
lastPoint.blk = initialLastPoint.blk + (blockSlope * (_time - initialLastPoint.ts)) / MULTIPLIER;

The term _time - initialLastPoint.ts will now always be 0 and lastPoint.blk always be calculated incorrectly (at least if _checkpoint has not been called for more than a week).

A POC that logs the resulting values:

// tested on fork number 19771835
function testMinhCheckpointing() public {
    uint256 week = 1 weeks;
    uint256 blocksPerWeek = 7*24*60*5; //@note assuming 1 block per 12 secs

    veALCX.checkpoint();

    vm.warp(block.timestamp + 3 * week);
    vm.roll(block.number + 3 * blocksPerWeek);

    veALCX.checkpoint();

    console2.log("point.blk for epoch 2:", veALCX.getPointHistory(2).blk);
    console2.log("point.blk for epoch 3:", veALCX.getPointHistory(3).blk);
    console2.log("point.blk for epoch 4:", veALCX.getPointHistory(4).blk);
    console2.log("point.blk for epoch 5:", veALCX.getPointHistory(5).blk);
}

This will log the following:

  point.blk for epoch 2: 19771835
  point.blk for epoch 3: 19771835
  point.blk for epoch 4: 19771835
  point.blk for epoch 5: 19923035

Note how the blk value only changes on the last iteration.

Impact Details

As mentioned in the intro for now the impact is limited to incorrect calculations and storage of the result. The blk field is currently not in use, but is referenced in the unused _findBlockEpoch method, which would be affected on future use.

Recommendation

Direct assignment doesnt copy, thats why Velodrome creates a new struct object explicitly:

GlobalPoint memory initialLastPoint = GlobalPoint({
    bias: lastPoint.bias,
    slope: lastPoint.slope,
    ts: lastPoint.ts,
    blk: lastPoint.blk,
    permanentLockBalance: lastPoint.permanentLockBalance
});

References

None

Proof of Concept

Same test as in the description:

// tested on fork number 19771835
function testMinhCheckpointing() public {
    uint256 week = 1 weeks;
    uint256 blocksPerWeek = 7*24*60*5; //@note assuming 1 block per 12 secs

    veALCX.checkpoint();

    vm.warp(block.timestamp + 3 * week);
    vm.roll(block.number + 3 * blocksPerWeek);

    veALCX.checkpoint();

    console2.log("point.blk for epoch 2:", veALCX.getPointHistory(2).blk);
    console2.log("point.blk for epoch 3:", veALCX.getPointHistory(3).blk);
    console2.log("point.blk for epoch 4:", veALCX.getPointHistory(4).blk);
    console2.log("point.blk for epoch 5:", veALCX.getPointHistory(5).blk);
}

Last updated