Platform: Code4rena
Start Date: 07/08/2023
Pot Size: $36,500 USDC
Total HM: 11
Participants: 125
Period: 3 days
Judge: alcueca
Total Solo HM: 4
Id: 274
League: ETH
Rank: 40/125
Findings: 2
Award: $47.44
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: ltyu
Also found by: 0xDING99YA, 3docSec, KmanOfficial, MrPotatoMagic, RED-LOTUS-REACH, Tendency, Yuki, bart1e, bin2chen, carrotsmuggler, cducrest, kaden, mert_eren, pep7siup, popular00, qpzm, seerether, zhaojie
43.2097 USDC - $43.21
https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L326 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L356
After the user executes VotingEscrow.createLock
, the VotingEscrow.delegate
operation is performed. After the time-lock date expires, the delegate cannot be re-delegate to self. As a result, the user cannot execute withdraw and user funds are locked.
VotingEscrow.withdraw
require locked_.delegatee
being equal to self,if the user's LockedBalance is delegated,withdraw will fail.
function withdraw() external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; // Validate inputs require(locked_.amount > 0, "No lock"); require(locked_.end <= block.timestamp, "Lock not expired"); require(locked_.delegatee == msg.sender, "Lock delegated"); ..... }
Therefore, the user must first call VotingEscrow.delegate(msg.sender)
when withdraw, to set locked_.delegatee to himself.
However, if the user does not execute VotingEscrow.delegate(msg.sender)
before the time lock expires, the VotingEscrow.delegate
execution will fail after the time lock expires.
Because the delegate function depends on require toLocked.end > block.timestamp
.
function delegate(address _addr) external nonReentrant { LockedBalance memory locked_ = locked[msg.sender]; .... require(toLocked.amount > 0, "Delegatee has no lock"); //@audit cannot execute after the time lock expires require(toLocked.end > block.timestamp, "Delegatee lock expired"); require(toLocked.end >= fromLocked.end, "Only delegate to longer lock"); _delegate(delegatee, fromLocked, value, LockAction.UNDELEGATE); _delegate(_addr, toLocked, value, LockAction.DELEGATE); }
In this case, the user cannot execute the withdraw function, and user funds are locked.
test code:
contract VotingEscrowTest is Test { VotingEscrow public ve; address public constant userA = address(0x1000A); address public constant userB = address(0x1000B); uint256 public constant LOCK_AMT = 1 ether; function setUp() public { ve = new VotingEscrow("Voting Escrow", "VE"); vm.deal(userA, 100 ether); vm.deal(userB, 100 ether); } function testWithdrawAfterTimeLockDelegated() public { vm.prank(userA); ve.createLock{value: LOCK_AMT}(LOCK_AMT); vm.prank(userB); ve.createLock{value: LOCK_AMT}(LOCK_AMT); // userA delegate to userB vm.prank(userA); ve.delegate(userB); // userA withdraw after timelock vm.prank(userA); (, uint256 end, , ) = ve.locked(userA); vm.warp(end + 1); //re-delegate to self will fail vm.prank(userA); ve.delegate(userA); vm.prank(userA); ve.withdraw(); } }
forge test --match-test WithdrawAfterTimeLockDelegated -vv
[â ’] Compiling... [â ‘] Compiling 4 files with 0.8.17 [â ˜] Solc 0.8.17 finished in 12.50s Compiler run successful!
Running 1 test for src/test/attack.t.sol:VotingEscrowTest [FAIL. Reason: Delegatee lock expired] testWithdrawAfterTimeLockDelegated() (gas: 864608) Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.16ms Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests: Encountered 1 failing test in src/test/attack.t.sol:VotingEscrowTest [FAIL. Reason: Delegatee lock expired] testWithdrawAfterTimeLockDelegated() (gas: 864608)
Encountered a total of 1 failing tests, 0 tests succeeded
vscode,foundry
VotingEscrow.delegate
allows delegatee to be set to itself after time has elapsed.
Other
#0 - c4-pre-sort
2023-08-12T09:49:03Z
141345 marked the issue as duplicate of #223
#1 - c4-pre-sort
2023-08-13T12:00:37Z
141345 marked the issue as duplicate of #112
#2 - c4-judge
2023-08-24T07:16:01Z
alcueca marked the issue as duplicate of #82
#3 - c4-judge
2023-08-24T07:20:40Z
alcueca changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-08-24T07:38:49Z
alcueca marked the issue as satisfactory
#5 - c4-pre-sort
2023-08-24T08:20:03Z
141345 marked the issue as not a duplicate
#6 - c4-pre-sort
2023-08-24T08:20:24Z
141345 marked the issue as not a duplicate
#7 - c4-pre-sort
2023-08-24T08:22:21Z
141345 marked the issue as duplicate of #211
#8 - c4-judge
2023-08-26T21:24:29Z
alcueca changed the severity to 3 (High Risk)
🌟 Selected for report: RED-LOTUS-REACH
Also found by: 0x3b, 0x4non, 0xCiphky, 0xDING99YA, 0xDetermination, 0xE1, 0xG0P1, 0xStalin, 0xWaitress, 0xbrett8571, 0xhacksmithh, 0xkazim, 0xmuxyz, 0xweb3boy, 14si2o_Flint, AlexCzm, Alhakista, Bube, Bughunter101, Deekshith99, Eeyore, Giorgio, HChang26, InAllHonesty, JP_Courses, KmanOfficial, MatricksDeCoder, Mike_Bello90, MrPotatoMagic, Naubit, QiuhaoLi, RHaO-sec, Raihan, Rolezn, SUPERMAN_I4G, Shubham, Silverskrrrt, Strausses, T1MOH, Topmark, Tripathi, Watermelon, _eperezok, aakansha, auditsea, audityourcontracts, ayden, carlos__alegre, castle_chain, cducrest, ch0bu, d23e, deadrxsezzz, deth, devival, erebus, fatherOfBlocks, halden, hassan-truscova, hpsb, hunter_w3b, imkapadia, immeas, jat, kaden, kaveyjoe, klau5, koxuan, kutugu, ladboy233, lanrebayode77, leasowillow, lsaudit, markus_ether, matrix_0wl, merlin, nemveer, ni8mare, nonseodion, oakcobalt, owadez, p_crypt0, pipidu83, piyushshukla, popular00, ppetrov, rjs, sandy, sl1, supervrijdag, tay054, thekmj, wahedtalash77, windhustler, zhaojie
4.2289 USDC - $4.23
https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L268 https://github.com/code-423n4/2023-08-verwa/blob/a693b4db05b9e202816346a6f9cada94f28a2698/src/VotingEscrow.sol#L115
In VotingEscrow.createLock
, if the amount of ether sent is less than LOCKTIME, the votingPower is 0(slope=0), which is inconsistent with the user's expected result.
Test code:
//send 1 ether , Success function testSlope1() public { vm.prank(userA); uint256 x = 1 ether; console.log("x:%d", x); ve.createLock{value: x}(x); (int128 biasA, int128 slopeA, ) = ve.getLastUserPoint(userA); console.log("slopeA: %d,biasA: %d", uint128(slopeA), uint128(biasA)); assertGt(slopeA, 0); } //Send a smaller amount of Ether , Fail function testSlope2() public { uint256 y = 1825 days - 1; console.log("y:%d", y); vm.prank(userD); ve.createLock{value: y}(y); (int128 biasD, int128 slopeD, ) = ve.getLastUserPoint(userD); console.log("slopeD: %d,biasD: %d", uint128(slopeD), uint128(biasD)); assertGt(slopeD, 0); }
forge test --match-test Slope -vv
Running 2 tests for src/test/attack.t.sol:VotingEscrowTest [PASS] testSlope1() (gas: 322651) Logs: x:1000000000000000000 slopeA: 6341958396,biasA: 997260267512249604 [FAIL. Reason: Assertion failed.] testSlope2() (gas: 274369) Logs: y:157679999 slopeD: 0,biasD: 0 Error: a > b not satisfied [int] Value a: 0 Value b: 0 Test result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 1.33ms Ran 1 test suites: 1 tests passed, 1 failed, 0 skipped (2 total tests) Failing tests: Encountered 1 failing test in src/test/attack.t.sol:VotingEscrowTest [FAIL. Reason: Assertion failed.] testSlope2() (gas: 274369) Encountered a total of 1 failing tests, 1 tests succeeded
Division overflow occurs when _checkpoint
calculates slope. Therefore, the slope value is 0 when the value is smaller than LOCKTIME.
userOldPoint.slope = _oldLocked.delegated / int128(int256(LOCKTIME)); .... userNewPoint.slope = _newLocked.delegated / int128(int256(LOCKTIME));
vscode,foundry
In VotingEscrow.createLock
require(_value > LOCKTIME, "");
Error
#0 - c4-pre-sort
2023-08-13T02:43:37Z
141345 marked the issue as duplicate of #299
#1 - c4-judge
2023-08-24T05:37:15Z
alcueca changed the severity to QA (Quality Assurance)
#2 - c4-judge
2023-08-25T22:48:20Z
alcueca marked the issue as grade-b