Lucene search

K
code423n4Code4renaCODE423N4:2023-07-ARCADE-FINDINGS-ISSUES-452
HistoryJul 28, 2023 - 12:00 a.m.

loss of user funds in ARCDVestingVault.sol

2023-07-2800:00:00
Code4rena
github.com
6
arcdvestingvault.sol
user funds
voting power delegation
zeroaddress error

Lines of code

Vulnerability details

Impact

In the contract ARCDVestingVault.sol the function delegate is used to delegate user votes to desired address but it fails to maintain the sanity check if the provided address is a zeroaddress or not

function delegate(address to) external {
        ARCDVestingVaultStorage.Grant storage grant = _grants()[msg.sender];
        if (to == grant.delegatee) revert AVV_AlreadyDelegated();

        ...

        emit VoteChange(to, msg.sender, int256(uint256(grant.latestVotingPower)));
    }

letting users delegate their votes to address(0) (accidentally). One might argue that they can always undo this step by delegating it to an other address but as the previous delegate of this grant now becomes addresszero, this causes the function to revert throwing an error as below

Error: call revert exception; VM Exception while processing transaction: reverted with reason string "Search Failure" [ See: https://links.ethers.org/v5-errors-CALL_EXCEPTION ] (method="queryVotePowerView(address,uint256)", data="0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e536561726368204661696c757265000000000000000000000000000000000000", errorArgs=["Search Failure"], errorName="Error", errorSignature="Error(string)", reason="Search Failure", code=CALL_EXCEPTION, version=abi/5.7.0)

Proof of Concept

copy paste in test\VestingVotingVault.ts and run npx hardhat test --grep β€œvoting power delegation to zeroaddress”

describe("voting power delegation to zeroaddress", function(){
        it("User changes vote delegation to 0x0", async function () {
            const { signers, vestingVotingVault } = ctxGovernance;
            const { arcdToken, bootstrapVestingManager } = ctxToken;
            const MANAGER = signers[1];
            const MANAGER_ADDRESS = signers[1].address;
            const OTHER_ADDRESS = signers[0].address;
            const OTHER = signers[0];

            await bootstrapVestingManager();

            // manager deposits tokens
            await arcdToken.connect(MANAGER).approve(vestingVotingVault.address, ethers.utils.parseEther("100"));
            await vestingVotingVault.connect(MANAGER).deposit(ethers.utils.parseEther("100"));
            expect(await arcdToken.balanceOf(vestingVotingVault.address)).to.equal(ethers.utils.parseEther("100"));

            // add grant
            const currentTime = await ethers.provider.getBlock("latest");
            const currentBlock = currentTime.number;
            const grantCreatedBlock = currentBlock + 1; // 1 block in the future
            const cliff = grantCreatedBlock + 100; // 100 blocks in the future
            const expiration = grantCreatedBlock + 200; // 200 blocks in the future
            await vestingVotingVault.connect(MANAGER).addGrantAndDelegate(
                OTHER_ADDRESS, // recipient
                ethers.utils.parseEther("100"), // grant amount
                ethers.utils.parseEther("50"), // cliff unlock amount
                0, // start time is current block
                expiration,
                cliff,
                OTHER_ADDRESS, // voting power delegate
            );

            // get grant
            const grant = await vestingVotingVault.getGrant(OTHER_ADDRESS);
            expect(grant.allocation).to.equal(ethers.utils.parseEther("100"));
            expect(grant.cliffAmount).to.equal(ethers.utils.parseEther("50"));
            expect(grant.withdrawn).to.equal(0);
            expect(grant.created).to.equal(grantCreatedBlock);
            expect(grant.expiration).to.equal(expiration);
            expect(grant.cliff).to.equal(cliff);
            expect(grant.latestVotingPower).to.equal(ethers.utils.parseEther("100"));
            expect(grant.delegatee).to.equal(OTHER_ADDRESS);

            // check voting power
            const checkBlock = await ethers.provider.getBlock("latest");
            expect(await vestingVotingVault.queryVotePowerView(OTHER_ADDRESS, checkBlock.number)).to.equal(
                ethers.utils.parseEther("100"),
            );

 // @>      //user delegating his votes to zero address
            console.log(ethers.constants.AddressZero);
            await vestingVotingVault.connect(OTHER).delegate(ethers.constants.AddressZero); 
            const checkBlock2 = await ethers.provider.getBlock("latest");
            expect(await vestingVotingVault.queryVotePowerView(OTHER_ADDRESS, checkBlock2.number)).to.equal(
                ethers.utils.parseEther("0"),
            );
// @>       //user votes have been delegated to zero address
            expect(await vestingVotingVault.queryVotePowerView(ethers.constants.AddressZero, checkBlock2.number)).to.equal(
                ethers.utils.parseEther("100"),
            );
        });
    });

Tools Used

manual review

Recommended Mitigation Steps

consider adding an if check

206:       function delegate(address to) external {
                 ARCDVestingVaultStorage.Grant storage grant = _grants()[msg.sender];
                 if (to == grant.delegatee) revert AVV_AlreadyDelegated();
          @>     if (to == address(0)) revert zero_address();

Assessed type

Invalid Validation


The text was updated successfully, but these errors were encountered:

All reactions