Livepeer contest - WatchPug's results

Decentralized live streaming platform built on the blockchain.

General Information

Platform: Code4rena

Start Date: 13/01/2022

Pot Size: $75,000 USDC

Total HM: 9

Participants: 27

Period: 7 days

Judge: leastwood

Total Solo HM: 5

Id: 73

League: ETH

Livepeer

Findings Distribution

Researcher Performance

Rank: 1/27

Findings: 14

Award: $23,665.13

🌟 Selected for report: 18

🚀 Solo Findings: 6

Findings Information

🌟 Selected for report: WatchPug

Also found by: Ruhum, gzeon, harleythedog

Labels

bug
3 (High Risk)
resolved
sponsor confirmed

Awards

20.7302 LPT - $768.68

2113.9229 USDC - $2,113.92

External Links

Handle

WatchPug

Vulnerability details

Per the arb-bridge-eth code:

all msg.value will deposited to callValueRefundAddress on L2

https://github.com/OffchainLabs/arbitrum/blob/78118ba205854374ed280a27415cb62c37847f72/packages/arb-bridge-eth/contracts/bridge/Inbox.sol#L313

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1ArbitrumMessenger.sol#L65-L74

uint256 seqNum = inbox.createRetryableTicket{value: _l1CallValue}(
    target,
    _l2CallValue,
    maxSubmissionCost,
    from,
    from,
    maxGas,
    gasPriceBid,
    data
);

At L308-L309, ETH held by BridgeMinter is withdrawn to L1Migrator:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L308-L309

        uint256 amount = IBridgeMinter(bridgeMinterAddr)
            .withdrawETHToL1Migrator();

However, when calling sendTxToL2() the parameter _l1CallValue is only the msg.value, therefore, the ETH transferred to L2 does not include any funds from bridgeMinter.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L318-L327

    sendTxToL2(
        l2MigratorAddr,
        address(this), // L2 alias of this contract will receive refunds
        msg.value,
        amount,
        _maxSubmissionCost,
        _maxGas,
        _gasPriceBid,
        ""
    )

As a result, due to lack of funds, call with value = amount to l2MigratorAddr will always fail on L2.

Since there is no other way to send ETH to L2, all the ETH from bridgeMinter is now frozen in the contract.

Recommendation

Change to:

    sendTxToL2(
        l2MigratorAddr,
        address(this), // L2 alias of this contract will receive refunds
        msg.value + amount, // the `amount` withdrawn from BridgeMinter should be added
        amount,
        _maxSubmissionCost,
        _maxGas,
        _gasPriceBid,
        ""
    )

#0 - yondonfu

2022-01-25T16:33:25Z

#1 - 0xleastwood

2022-01-29T23:46:05Z

Awesome find!

Findings Information

🌟 Selected for report: WatchPug

Also found by: gzeon

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed

Awards

15.3557 LPT - $569.39

1565.8688 USDC - $1,565.87

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L308-L310

uint256 amount = IBridgeMinter(bridgeMinterAddr)
            .withdrawETHToL1Migrator();

L1Migrator.sol#migrateETH() will call IBridgeMinter(bridgeMinterAddr).withdrawETHToL1Migrator() to withdraw ETH from BridgeMinter.

However, the current implementation of L1Migrator is unable to receive ETH.

https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/token/BridgeMinter.sol#L94-L94

(bool ok, ) = l1MigratorAddr.call.value(address(this).balance)("");

A contract receiving Ether must have at least one of the functions below:

  • receive() external payable
  • fallback() external payable

receive() is called if msg.data is empty, otherwise fallback() is called.

Because L1Migrator implement neither receive() or fallback(), the call at L94 will always revert.

Impact

All the ETH held by the BridgeMinter can get stuck in the contract.

Recommandation

Add receive() external payable {} in L1Migrator.

#0 - yondonfu

2022-01-24T02:47:47Z

Severity: 2 (Med)

We'll fix this, but noting that the funds are recoverable because the BridgeMinter can set a new L1Migrator that does have the receive() function which is why the suggested severity is 2 (Med).

#1 - yondonfu

2022-01-25T16:34:00Z

#2 - 0xleastwood

2022-01-29T23:49:47Z

Agree with sponsor, these funds are recoverable. However, the warden has identified a DOS attack, which is a valid medium severity issue.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

34.1238 LPT - $1,265.31

3479.7085 USDC - $3,479.71

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L23-L30

function mint(address _to, uint256 _amount)
    external
    override
    onlyRole(MINTER_ROLE)
{
    _mint(_to, _amount);
    emit Mint(_to, _amount);
}

Using the mint() function of L2LivepeerToken, an address with MINTER_ROLE can burn an arbitrary amount of tokens.

If the private key of the deployer or an address with the MINTER_ROLE is compromised, the attacker will be able to mint an unlimited amount of LPT tokens.

We believe this is unnecessary and poses a serious centralization risk.

Recommendation

Consider removing the MINTER_ROLE, make the L2LivepeerToken only mintable by the owner, and make the L2Minter contract to be the owner and therefore the only minter.

#0 - yondonfu

2022-01-24T02:38:21Z

Planning on keeping the role since the L2LPTGateway needs to be given minting rights as well in addition to the L2 Minter.

Findings Information

🌟 Selected for report: WatchPug

Also found by: cccz

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed

Awards

15.3557 LPT - $569.39

1565.8688 USDC - $1,565.87

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L36-L43

function burn(address _from, uint256 _amount)
    external
    override
    onlyRole(BURNER_ROLE)
{
    _burn(_from, _amount);
    emit Burn(_from, _amount);
}

Using the burn() function of L2LivepeerToken, an address with BURNER_ROLE can burn an arbitrary amount of tokens from any address.

We believe this is unnecessary and poses a serious centralization risk.

A malicious or compromised BURNER_ROLE address can take advantage of this, burn the balance of a Uniswap pool and effectively steal almost all the funds from the liquidity pool (eg, Uniswap LPT-WETH Pool).

Recommendation

Consider removing the BURNER_ROLE and change burn() function to:

function burn(uint256 _amount)
    external
    override
{
    _burn(msg.sender, _amount);
    emit Burn(msg.sender, _amount);
}

https://github.com/livepeer/arbitrum-lpt-bridge/blob/49cf5401b0514511675d781a1e29d6b0325cfe88/contracts/L2/gateway/L2LPTGateway.sol#L34-L45

Mintable(l2Lpt).burn(from, _amount); in L2LPTGateway.sol#outboundTransfer() should also be replaced with:

Mintable(l2Lpt).transferFrom(from, _amount);
Mintable(l2Lpt).burn(_amount);

#0 - yondonfu

2022-01-25T16:32:20Z

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
sponsor acknowledged

Awards

34.1238 LPT - $1,265.31

3479.7085 USDC - $3,479.71

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/escrow/L1Escrow.sol#L21-L28

function approve(
    address _token,
    address _spender,
    uint256 _value
) public onlyRole(DEFAULT_ADMIN_ROLE) {
    ApproveLike(_token).approve(_spender, _value);
    emit Approve(_token, _spender, _value);
}

L1Escrow.sol#approve() allows an address with DEFAULT_ADMIN_ROLE can approve an arbitrary amount of tokens to any address.

We believe this is unnecessary and poses a serious centralization risk.

A malicious or compromised DEFAULT_ADMIN_ROLE address can take advantage of this, and steal all the funds from the L1Escrow contract.

Recommendation

Consider removing approve() function and approve l1LPT to l1Gateway in the constructor.

#0 - yondonfu

2022-01-24T02:46:02Z

Likely won't change as we want to preserve the ability for protocol governance to move the LPT from the L1Escrow in the event of a L2 failure.

#1 - MrToph

2022-03-02T16:26:49Z

duplicate of #165 ?

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed

Awards

34.1238 LPT - $1,265.31

3479.7085 USDC - $3,479.71

External Links

Handle

WatchPug

Vulnerability details

Per the document: https://github.com/code-423n4/2022-01-livepeer#l2---l1-lpt-withdrawal

The following occurs when LPT is withdrawn from L2 to L1:

The user initiates a withdrawal for X LPT. This can be done in two ways: a. Call outboundTransfer() on L2GatewayRouter which will call outboundTransfer() on L2LPTGateway b. Call outboundTransfer() directly on L2LPTGateway

The method (a) described above won't work in the current implementation due to the missing interface on L2LPTGateway.

When initiate a withdraw from the Arbitrum Gateway Router, L2GatewayRouter will call outboundTransfer(address,address,uint256,uint256,uint256,bytes) on ITokenGateway(gateway):

function outboundTransfer(
    address _token,
    address _to,
    uint256 _amount,
    uint256 _maxGas,
    uint256 _gasPriceBid,
    bytes calldata _data
) external payable returns (bytes memory);

https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/arbitrum/gateway/L2GatewayRouter.sol#L57-L64

function outboundTransfer(
    address _l1Token,
    address _to,
    uint256 _amount,
    bytes calldata _data
) public payable returns (bytes memory) {
    return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data);
}

https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/libraries/gateway/GatewayRouter.sol#L78-L102

function outboundTransfer(
    address _token,
    address _to,
    uint256 _amount,
    uint256 _maxGas,
    uint256 _gasPriceBid,
    bytes calldata _data
) public payable virtual override returns (bytes memory) {
    address gateway = getGateway(_token);
    bytes memory gatewayData = GatewayMessageHandler.encodeFromRouterToGateway(
        msg.sender,
        _data
    );

    emit TransferRouted(_token, msg.sender, _to, gateway);
    return
        ITokenGateway(gateway).outboundTransfer{ value: msg.value }(
            _token,
            _to,
            _amount,
            _maxGas,
            _gasPriceBid,
            gatewayData
        );
}

However, L2LPTGateway dose not implement outboundTransfer(address,address,uint256,uint256,uint256,bytes) but only outboundTransfer(address,address,uint256,bytes):

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTGateway.sol#L65-L89

function outboundTransfer(
    address _l1Token,
    address _to,
    uint256 _amount,
    bytes calldata _data
) public override whenNotPaused returns (bytes memory res) {
    // ...
}

Therefore, the desired feature to withdraw LPT from L2 to L1 via Arbitrum Router will not be working properly.

Recommendation

Consider implementing the method used by Arbitrum Router.

See also the implementation of L2DaiGateway by arbitrum-dai-bridge: https://github.com/makerdao/arbitrum-dai-bridge/blob/master/contracts/l2/L2DaiGateway.sol#L88-L95

#0 - yondonfu

2022-01-31T21:21:37Z

Findings Information

🌟 Selected for report: WatchPug

Also found by: danb

Labels

bug
1 (Low Risk)
resolved
sponsor confirmed

Awards

5.1186 LPT - $189.80

521.9563 USDC - $521.96

External Links

Handle

WatchPug

Vulnerability details

In the current implementation of DelegatorPool.sol#claim(), it first requires claimedInitialStake < initialStake, or it throws an error of DelegatorPool#claim: FULLY_CLAIMED.

However, since it's an onlyMigrator function, the felicity of _delegator and _stake should be assured by the Migrator contract, otherwise, this require statement itself also can not prevent bad results caused by the wrong inputs.

Furthermore, even if the purpose of this require statement is to make sure that claimedInitialStake can never surpass the initialStake, the expression should be claimedInitialStake + _stake <= initialStake instead of claimedInitialStake < initialStake.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L58-L93

function claim(address _delegator, uint256 _stake) external onlyMigrator {
    require(
        claimedInitialStake < initialStake,
        "DelegatorPool#claim: FULLY_CLAIMED"
    );

    // Calculate stake owed to delegator
    uint256 currTotalStake = pendingStake();
    uint256 owedStake = (currTotalStake * _stake) /
        (initialStake - claimedInitialStake);

    // Calculate fees owed to delegator
    uint256 currTotalFees = pendingFees();
    uint256 owedFees = (currTotalFees * _stake) /
        (initialStake - claimedInitialStake);

    // update claimed balance
    claimedInitialStake += _stake;

    // Transfer owed stake to the delegator
    transferBond(_delegator, owedStake);

    // Transfer owed fees to the delegator
    IBondingManager(bondingManager).withdrawFees(
        payable(_delegator),
        owedFees
    );

    emit Claimed(_delegator, owedStake, owedFees);
}

Recommandation

Consider removing it or changing to:

require(
    claimedInitialStake + _stake <= initialStake,
    "DelegatorPool#claim: FULLY_CLAIMED"
);

Findings Information

🌟 Selected for report: WatchPug

Also found by: Ruhum

Labels

bug
1 (Low Risk)
resolved
sponsor confirmed

Awards

5.1186 LPT - $189.80

521.9563 USDC - $521.96

External Links

Handle

WatchPug

Vulnerability details

Per the document: https://github.com/livepeer/LIPs/blob/master/LIPs/LIP-73.md#upgrade-process

Phase 1

  • The L1 RoundsManager will be upgraded to disable round initialization at LIP_73_ROUND
  • During this phase, protocol transactions will be executed normally
  • During this phase, the following contracts will be deployed:
    • Protocol contracts on L2
    • Migrator contracts on L1 and L2
    • LPT bridge contracts on L1 and L2
    • All of these contracts will start off paused

However, the current implementation of L1LPTGateway, L2LPTGateway are not automatically paused on deployment.

We recommend adding _pause() to the end of the constructor() in L1LPTGateway, L2LPTGateway, like the constructor of L1Migrator.sol#L143, and unpause() when Phase 2 starts.

This will help avoid tx to happen in an intermediate state between Phase1 and Phase 2, which may cause certain txs to fail, for instance:

When in Phase 1, L1LPTGateway cant calls bridgeMint() on the BridgeMinter to mint LPT to the user, as L1 Minter have not migrateToNewMinter() to BridgeMinter yet. If a user in L2 tries to move LPT from L2 to L1, their tx may fail.

Findings Information

🌟 Selected for report: WatchPug

Also found by: Dravee

Labels

bug
G (Gas Optimization)
resolved

Awards

0.3637 LPT - $13.49

37.086 USDC - $37.09

External Links

Handle

WatchPug

Vulnerability details

For the storage variables that will be accessed multiple times, cache and read from the stack can save ~100 gas from each extra read (SLOAD after Berlin).

For example:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L58-L93

    function claim(address _delegator, uint256 _stake) external onlyMigrator {
        require(
            claimedInitialStake < initialStake,
            "DelegatorPool#claim: FULLY_CLAIMED"
        );

        uint256 currTotalStake = pendingStake();
        uint256 owedStake = (currTotalStake * _stake) /
            (initialStake - claimedInitialStake);

        // Calculate fees owed to delegator
        uint256 currTotalFees = pendingFees();
        uint256 owedFees = (currTotalFees * _stake) /
            (initialStake - claimedInitialStake);

        // update claimed balance
        claimedInitialStake += _stake;

        // Transfer owed stake to the delegator
        transferBond(_delegator, owedStake);

        // Transfer owed fees to the delegator
        IBondingManager(bondingManager).withdrawFees(
            payable(_delegator),
            owedFees
        );

        emit Claimed(_delegator, owedStake, owedFees);
    }

initialStake and claimedInitialStake can be cached.

Recommendation

Change to:

    function claim(address _delegator, uint256 _stake) external onlyMigrator {
        uint256 _intialStake = initialStake;
        uint256 _claimedInitialStake = claimedInitialStake;

        require(
            _claimedInitialStake < _initialStake,
            "DelegatorPool#claim: FULLY_CLAIMED"
        );

        uint256 currTotalStake = pendingStake();
        uint256 owedStake = (currTotalStake * _stake) /
            (_initialStake - _claimedInitialStake);

        // Calculate fees owed to delegator
        uint256 currTotalFees = pendingFees();
        uint256 owedFees = (currTotalFees * _stake) /
            (_initialStake - _claimedInitialStake);

        // update claimed balance
        claimedInitialStake += _stake;

        // Transfer owed stake to the delegator
        transferBond(_delegator, owedStake);

        // Transfer owed fees to the delegator
        IBondingManager(bondingManager).withdrawFees(
            payable(_delegator),
            owedFees
        );

        emit Claimed(_delegator, owedStake, owedFees);
    }

#1 - 0xleastwood

2022-01-30T05:29:24Z

Marking as primary issue.

Findings Information

🌟 Selected for report: WatchPug

Also found by: 0x0x0x, Dravee, Jujic, defsec, robee, ye0lde

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

Awards

0.0614 LPT - $2.28

6.2568 USDC - $6.26

External Links

Findings Information

🌟 Selected for report: defsec

Also found by: Dravee, WatchPug, byterocket, robee

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.1061 LPT - $3.93

10.8143 USDC - $10.81

External Links

Handle

WatchPug

Vulnerability details

Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.

Caching the array length in the stack saves around 3 gas per iteration.

Instances include:

#0 - yondonfu

2022-01-23T00:57:22Z

Findings Information

🌟 Selected for report: robee

Also found by: 0x1f8b, Dravee, Jujic, OriDabush, WatchPug, defsec

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.0614 LPT - $2.28

6.2568 USDC - $6.26

External Links

Handle

WatchPug

Vulnerability details

Using ++i is more gas efficient than i++, especially in a loop.

We suggest to use unchecked {++i} to even better gas performance. (for >= 0.8)

For example:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L472-L472

 for (uint256 i = 0; i < _unbondingLockIds.length; i++) 

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L197-L197

for (uint256 i = 0; i < _params.unbondingLockIds.length; i++) 

#0 - yondonfu

2022-01-23T14:23:59Z

Findings Information

🌟 Selected for report: WatchPug

Also found by: Dravee

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

Awards

0.3637 LPT - $13.49

37.086 USDC - $37.09

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L70-L78

        // Calculate stake owed to delegator
        uint256 currTotalStake = pendingStake();
        uint256 owedStake = (currTotalStake * _stake) /
            (initialStake - claimedInitialStake);

        // Calculate fees owed to delegator
        uint256 currTotalFees = pendingFees();
        uint256 owedFees = (currTotalFees * _stake) /
            (initialStake - claimedInitialStake);

The local variable currTotalStake, currTotalFees is used only once. Making the expression inline can save gas.

Similar issue exists in L2Migrator.sol#claimStake(), L1Migrator.sol#migrateETH(), L1Migrator.sol#migrateLPT(), L1ArbitrumMessenger.sol#onlyL2Counterpart().

Recommendation

Change to:

        // Calculate stake owed to delegator
        uint256 owedStake = (pendingStake() * _stake) /
            (initialStake - claimedInitialStake);

        // Calculate fees owed to delegator
        uint256 owedFees = (pendingFees() * _stake) /
            (initialStake - claimedInitialStake);

Findings Information

🌟 Selected for report: Dravee

Also found by: WatchPug

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.3637 LPT - $13.49

37.086 USDC - $37.09

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L91-L94

return
    l1TotalSupply >= l2SupplyFromL1
        ? l1TotalSupply - l2SupplyFromL1
        : 0;

When l1TotalSupply == l2SupplyFromL1, l1TotalSupply - l2SupplyFromL1 == 0. Therefore, replace >= with > can avoid the unnecessary arithmetic operations and storage reads (l1TotalSupply - l2SupplyFromL1).

Recommendation

Change to:

return
    l1TotalSupply > l2SupplyFromL1
        ? l1TotalSupply - l2SupplyFromL1
        : 0;

#0 - yondonfu

2022-01-23T20:20:05Z

Findings Information

🌟 Selected for report: sirhashalot

Also found by: Dravee, Jujic, WatchPug, aga7hokakological, defsec, gzeon, p4st13r4, robee

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.0387 LPT - $1.43

3.9418 USDC - $3.94

External Links

Handle

WatchPug

Vulnerability details

Every reason string takes at least 32 bytes.

Use short reason strings that fits in 32 bytes or it will become more expensive.

Instances include:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L506-L509

        require(
            _l2Addr != address(0),
            "L1Migrator#requireValidMigration: INVALID_L2_ADDR"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L510-L515

        require(
            msg.sender == _l1Addr ||
                recoverSigner(_structHash, _sig) == _l1Addr,
            "L1Migrator#requireValidMigration: FAIL_AUTH"
        );
    }

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L134-L137

        require(
            !migratedDelegators[_params.l1Addr],
            "L2Migrator#finalizeMigrateDelegator: ALREADY_MIGRATED"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L184-L184

        require(ok, "L2Migrator#finalizeMigrateDelegator: FAIL_FEE");

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L199-L202

        require(
            !migratedUnbondingLocks[_params.l1Addr][id],
            "L2Migrator#finalizeMigrateUnbondingLocks: ALREADY_MIGRATED"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L219-L222

        require(
            !migratedSenders[_params.l1Addr],
            "L2Migrator#finalizeMigrateSender: ALREADY_MIGRATED"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L255-L258

        require(
            claimStakeEnabled,
            "L2Migrator#claimStake: CLAIM_STAKE_DISABLED"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L267-L270

        require(
            merkleSnapshot.verify(keccak256("LIP-73"), _proof, leaf),
            "L2Migrator#claimStake: INVALID_PROOF"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L272-L275

        require(
            !migratedDelegators[delegator],
            "L2Migrator#claimStake: ALREADY_MIGRATED"
        );

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L39-L39

        require(msg.sender == migrator, "DelegatorPool#claim: NOT_MIGRATOR");

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L59-L62

        require(
            claimedInitialStake < initialStake,
            "DelegatorPool#claim: FULLY_CLAIMED"
        );

#0 - yondonfu

2022-01-21T16:17:38Z

Findings Information

🌟 Selected for report: robee

Also found by: Dravee, WatchPug, ye0lde

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.1473 LPT - $5.46

15.0198 USDC - $15.02

External Links

Handle

WatchPug

Vulnerability details

Unused named returns increase contract size and gas usage at deployment.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTGateway.sol#L65-L89

    function outboundTransfer(
        address _l1Token,
        address _to,
        uint256 _amount,
        bytes calldata _data
    ) public override whenNotPaused returns (bytes memory res) {
        require(_l1Token == l1Lpt, "TOKEN_NOT_LPT");

        (address from, bytes memory extraData) = parseOutboundData(_data);
        require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED");

        Mintable(l2Lpt).burn(from, _amount);
        IL2LPTDataCache(l2LPTDataCache).decreaseL2SupplyFromL1(_amount);

        uint256 id = sendTxToL1(
            from,
            l1Counterpart,
            getOutboundCalldata(_l1Token, from, _to, _amount, extraData)
        );

        // we don't need to track exitNums (b/c we have no fast exits) so we always use 0
        emit WithdrawalInitiated(_l1Token, from, _to, id, 0, _amount);

        return abi.encode(id);
    }

res is unused.

Recommendation

Change to:

    function outboundTransfer(
        address _l1Token,
        address _to,
        uint256 _amount,
        bytes calldata _data
    ) public override whenNotPaused returns (bytes memory) {
        require(_l1Token == l1Lpt, "TOKEN_NOT_LPT");

        (address from, bytes memory extraData) = parseOutboundData(_data);
        require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED");

        Mintable(l2Lpt).burn(from, _amount);
        IL2LPTDataCache(l2LPTDataCache).decreaseL2SupplyFromL1(_amount);

        uint256 id = sendTxToL1(
            from,
            l1Counterpart,
            getOutboundCalldata(_l1Token, from, _to, _amount, extraData)
        );

        // we don't need to track exitNums (b/c we have no fast exits) so we always use 0
        emit WithdrawalInitiated(_l1Token, from, _to, id, 0, _amount);

        return abi.encode(id);
    }

#0 - yondonfu

2022-01-23T14:27:59Z

Findings Information

🌟 Selected for report: ye0lde

Also found by: Dravee, Jujic, WatchPug, defsec, gzeon

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.0795 LPT - $2.95

8.1107 USDC - $8.11

External Links

Handle

WatchPug

Vulnerability details

For the arithmetic operations that will never over/underflow, using the unchecked directive (Solidity v0.8 has default overflow/underflow checks) can save some gas from the unnecessary internal over/underflow checks.

For example:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L62-L66

        if (_amount > l2SupplyFromL1) {
            l2SupplyFromL1 = 0;
        } else {
            l2SupplyFromL1 -= _amount;
        }

l2SupplyFromL1 -= _amount will never underflow.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1LPTGateway.sol#L149-L156

        if (amount <= escrowBalance) {
            TokenLike(l1Token).transferFrom(l1LPTEscrow, to, amount);
        } else {
            if (escrowBalance > 0) {
                TokenLike(l1Token).transferFrom(l1LPTEscrow, to, escrowBalance);
            }
            IMinter(minter).bridgeMint(to, amount - escrowBalance);
        }

amount - escrowBalance will never underflow.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/pool/DelegatorPool.sol#L59-L78

        require(
            claimedInitialStake < initialStake,
            "DelegatorPool#claim: FULLY_CLAIMED"
        );

        // _stake is the delegator's original stake
        // This contract started off with initalStake
        // We can calculate how much of the contract's current stake and fees
        // are owed to the delegator proportional to _stake / (initialStake - claimedInitialStake)
        // where claimedInitialStake is the stake of the contract that has already been claimed

        // Calculate stake owed to delegator
        uint256 currTotalStake = pendingStake();
        uint256 owedStake = (currTotalStake * _stake) /
            (initialStake - claimedInitialStake);

        // Calculate fees owed to delegator
        uint256 currTotalFees = pendingFees();
        uint256 owedFees = (currTotalFees * _stake) /
            (initialStake - claimedInitialStake);

initialStake - claimedInitialStake will never underflow.

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2LPTDataCache.sol#L91-L94

        return
            l1TotalSupply >= l2SupplyFromL1
                ? l1TotalSupply - l2SupplyFromL1
                : 0;

l1TotalSupply - l2SupplyFromL1 will never underflow.

#0 - yondonfu

2022-01-23T01:04:30Z

Findings Information

🌟 Selected for report: WatchPug

Also found by: Tomio, gzeon, hyh

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

Awards

0.1473 LPT - $5.46

15.0198 USDC - $15.02

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/token/BridgeMinter.sol#L90-L98

function withdrawETHToL1Migrator() external onlyL1Migrator returns (uint256) {
    uint256 balance = address(this).balance;

    // call() should be safe from re-entrancy here because the L1Migrator and l1MigratorAddr are trusted
    (bool ok, ) = l1MigratorAddr.call.value(address(this).balance)("");
    require(ok, "BridgeMinter#withdrawETHToL1Migrator: FAIL_CALL");

    return balance;
}

At L94, address(this).balance can be replaced with balance to avoid unnecessarily repeated read of account balance state to save some gas.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

Awards

0.8082 LPT - $29.97

82.4134 USDC - $82.41

External Links

Handle

WatchPug

Vulnerability details

When there are multiple checks, adjusting the sequence to allow the tx to fail earlier can save some gas.

Checks using less gas should be executed earlier than those with higher gas costs, to avoid unnecessary storage read, arithmetic operations, etc when it reverts.

For example:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L255-L275

        require(
            claimStakeEnabled,
            "L2Migrator#claimStake: CLAIM_STAKE_DISABLED"
        );

        IMerkleSnapshot merkleSnapshot = IMerkleSnapshot(merkleSnapshotAddr);

        address delegator = msg.sender;
        bytes32 leaf = keccak256(
            abi.encodePacked(delegator, _delegate, _stake, _fees)
        );

        require(
            merkleSnapshot.verify(keccak256("LIP-73"), _proof, leaf),
            "L2Migrator#claimStake: INVALID_PROOF"
        );

        require(
            !migratedDelegators[delegator],
            "L2Migrator#claimStake: ALREADY_MIGRATED"
        );

The check of !migratedDelegators[delegator] can be done earlier to avoid reading from storage when migratedDelegators[delegator] == true.

Recommendation

Change to:

        require(
            claimStakeEnabled,
            "L2Migrator#claimStake: CLAIM_STAKE_DISABLED"
        );

        address delegator = msg.sender;
        require(
            !migratedDelegators[delegator],
            "L2Migrator#claimStake: ALREADY_MIGRATED"
        );

        IMerkleSnapshot merkleSnapshot = IMerkleSnapshot(merkleSnapshotAddr);
        require(
            merkleSnapshot.verify(keccak256("LIP-73"), _proof, leaf),
            "L2Migrator#claimStake: INVALID_PROOF"
        );

        bytes32 leaf = keccak256(
            abi.encodePacked(delegator, _delegate, _stake, _fees)
        );

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

0.8082 LPT - $29.97

82.4134 USDC - $82.41

External Links

Handle

WatchPug

Vulnerability details

Booleans are more expensive than uint256 or any type that takes up a full word because each write operation emits an extra SLOAD to first read the slot's contents, replace the bits taken up by the boolean, and then write back. This is the compiler's defense against contract upgrades and pointer aliasing, and it cannot be disabled.

See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol#L23-L27

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L51-L51

    bool public claimStakeEnabled;

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/gateway/L2Migrator.sol#L53-L57

    mapping(address => bool) public migratedDelegators;
    // mapping(address => address) public delegatorPools;
    // mapping(address => uint256) public claimedDelegatedStake;
    mapping(address => mapping(uint256 => bool)) public migratedUnbondingLocks;
    mapping(address => bool) public migratedSenders;

#0 - yondonfu

2022-01-24T02:12:55Z

Likely won't change as we think keeping booleans is more readable.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

0.8082 LPT - $29.97

82.4134 USDC - $82.41

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/Manager.sol#L48-L50

function _onlyController() internal view {
    require(msg.sender == address(controller), "caller must be Controller");
}

https://github.com/livepeer/protocol/blob/20e7ebb86cdb4fe9285bf5fea02eb603e5d48805/contracts/Manager.sol#L11-L14

modifier onlyController() {
    _onlyController();
    _;
}

_onlyController() is unnecessary as it's being used only once. Therefore it can be inlined in modifier onlyController() to make the code simpler and save gas.

Recommendation

Change to:

modifier onlyController() {
    require(msg.sender == address(controller), "caller must be Controller");
    _;
}

#0 - yondonfu

2022-01-24T02:13:56Z

Likely won't change as the internal function reduces contract code size while inlining into the modifier will increase contract code size proportional to # of times modifier is used.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor disputed

Awards

0.8082 LPT - $29.97

82.4134 USDC - $82.41

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1LPTGateway.sol#L136-L159

function finalizeInboundTransfer(
    address l1Token,
    address from,
    address to,
    uint256 amount,
    bytes calldata data
) external override onlyL2Counterpart(l2Counterpart) {
    require(l1Token == l1Lpt, "TOKEN_NOT_LPT");
    (uint256 exitNum, ) = abi.decode(data, (uint256, bytes));

    uint256 escrowBalance = TokenLike(l1Token).balanceOf(l1LPTEscrow);

    // mint additional tokens if requested amount exceeds escrowed amount
    if (amount <= escrowBalance) {
        TokenLike(l1Token).transferFrom(l1LPTEscrow, to, amount);
    } else {
        if (escrowBalance > 0) {
            TokenLike(l1Token).transferFrom(l1LPTEscrow, to, escrowBalance);
        }
        IMinter(minter).bridgeMint(to, amount - escrowBalance);
    }

    emit WithdrawalFinalized(l1Token, from, to, exitNum, amount);
}

As L143 already assured that l1Token equals immutable variable l1Lpt, therefore l1Token can be replaced with immutable variable l1Lpt, to avoid local variable reads (MLOAD) to save some gas.

#0 - kautukkundan

2022-02-01T19:07:15Z

Immutable translates to a PUSH32 opcode costs 3 gas and CALLDATALOAD opcode also costs 3 gas. So, the cost for reading l1Token and reading l1Lpt should be the same.

Findings Information

🌟 Selected for report: WatchPug

Also found by: byterocket

Labels

bug
G (Gas Optimization)
resolved
sponsor confirmed

Awards

0.3637 LPT - $13.49

37.086 USDC - $37.09

External Links

Handle

WatchPug

Vulnerability details

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L2/token/LivepeerToken.sol#L12-L16

constructor() ERC20("Livepeer Token", "LPT") ERC20Permit("Livepeer Token") {
    _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
    _setRoleAdmin(MINTER_ROLE, DEFAULT_ADMIN_ROLE);
    _setRoleAdmin(BURNER_ROLE, DEFAULT_ADMIN_ROLE);
}

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/ControlledGateway.sol#L18-L24

constructor(address _l1Lpt, address _l2Lpt) {
    _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
    _setRoleAdmin(GOVERNOR_ROLE, DEFAULT_ADMIN_ROLE);

    l1Lpt = _l1Lpt;
    l2Lpt = _l2Lpt;
}

constant DEFAULT_ADMIN_ROLE = 0x00

By default, the admin role for all roles is DEFAULT_ADMIN_ROLE.

Therefore, _setRoleAdmin(***_ROLE, DEFAULT_ADMIN_ROLE); is redundant.

Removing it will make the code simpler and save some gas.

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/783ac759a902a7b4a218c2d026a77e6a26b6c42d/contracts/access/AccessControl.sol#L40-L43

 * By default, the admin role for all roles is `DEFAULT_ADMIN_ROLE`, which means
 * that only accounts with this role will be able to grant or revoke other
 * roles. More complex role relationships can be created by using
 * {_setRoleAdmin}.

https://docs.openzeppelin.com/contracts/3.x/access-control#granting-and-revoking

AccessControl includes a special role, called DEFAULT_ADMIN_ROLE, which acts as the default admin role for all roles. An account with this role will be able to manage any other role, unless _setRoleAdmin is used to select a new admin role.

Recommendation

Remove the redundant code.

Findings Information

🌟 Selected for report: WatchPug

Labels

bug
G (Gas Optimization)
sponsor acknowledged

Awards

0.8082 LPT - $29.97

82.4134 USDC - $82.41

External Links

Handle

WatchPug

Vulnerability details

There is no risk of overflow caused by increamenting the iteration index in for loops (the i++ in for for (uint256 i = 0; i < _unbondingLockIds.length; i++)).

Increments perform overflow checks that are not necessary in this case.

Recommendation

Surround the increment expressions with an unchecked { ... } block to avoid the default overflow checks. For example, change the for loop:

https://github.com/livepeer/arbitrum-lpt-bridge/blob/ebf68d11879c2798c5ec0735411b08d0bea4f287/contracts/L1/gateway/L1Migrator.sol#L472-L479

for (uint256 i = 0; i < _unbondingLockIds.length; i++) {
    (uint256 amount, ) = bondingManager.getDelegatorUnbondingLock(
        _l1Addr,
        _unbondingLockIds[i]
    );

    total += amount;
}

to:

for (uint256 i = 0; i < _unbondingLockIds.length;) {
    (uint256 amount, ) = bondingManager.getDelegatorUnbondingLock(
        _l1Addr,
        _unbondingLockIds[i]
    );

    total += amount;

    unchecked { ++i; }
}

#0 - yondonfu

2022-01-24T02:53:13Z

Likely won't make this change as we think it hurts readability.

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter