Livepeer contest - gzeon'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: 2/27

Findings: 10

Award: $11,308.96

🌟 Selected for report: 1

🚀 Solo Findings: 2

Findings Information

🌟 Selected for report: WatchPug

Also found by: Ruhum, gzeon, harleythedog

Labels

bug
duplicate
3 (High Risk)

Awards

20.7302 LPT - $768.68

2113.9229 USDC - $2,113.92

External Links

Handle

gzeon

Vulnerability details

Impact

migrateETH does not send the withdrawn ETH to L2 causing fund to stuck in the L1Migrator contract.

Proof of Concept

When migrateETH is called, it would withdraw all ETH from bridgeMinter, and then use sendTxToL2 create a L2 retryable ticket to call the L2Migrator with _l1CallValue set to msg.value and _l2CallValue set to the ETH balance withdrew.

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

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; }

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

function migrateETH( uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost ) external payable whenNotPaused { uint256 amount = IBridgeMinter(bridgeMinterAddr) .withdrawETHToL1Migrator(); // Any ETH refunded to the L2 alias of this contract can be used for // other cross-chain txs sent by this contract. // The retryable ticket created will not be cancellable since this contract // currently does not support cross-chain txs to call ArbRetryableTx.cancel(). // Regarding the comment below on this contract receiving refunds: // msg.sender also cannot be the address to receive refunds as beneficiary because otherwise // msg.sender could cancel the ticket before it is executed on L2 to receive the L2 call value. sendTxToL2( l2MigratorAddr, address(this), // L2 alias of this contract will receive refunds msg.value, amount, _maxSubmissionCost, _maxGas, _gasPriceBid, "" ); }

The sendTxToL2 function create a L2 retryable ticket using _l1CallValue as call value. The ETH in _l2CallValue is not transfered to the inbox contract.

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

function sendTxToL2( address target, address from, uint256 _l1CallValue, uint256 _l2CallValue, uint256 maxSubmissionCost, uint256 maxGas, uint256 gasPriceBid, bytes memory data ) internal returns (uint256) { uint256 seqNum = inbox.createRetryableTicket{value: _l1CallValue}( target, _l2CallValue, maxSubmissionCost, from, from, maxGas, gasPriceBid, data ); emit TxToL2(from, target, seqNum, data); return seqNum; }

Since the _l2CallValue (i.e. the ETH withdrew from bridgeMinter) was never sent to the inbox, it would stuck in the L1 migrator contract and the L2 call would likely revert due to insufficient call value.

The call value should be _l1CallValue + _l2CallValue

#0 - yondonfu

2022-01-23T00:49:31Z

Findings Information

🌟 Selected for report: Ruhum

Also found by: gzeon, harleythedog

Labels

bug
duplicate
2 (Med Risk)

Awards

9.2134 LPT - $341.63

939.5213 USDC - $939.52

External Links

Handle

gzeon

Vulnerability details

Impact

Anyone can call migrateETH and migrateLPT in L1Migrator with arbitrary _maxSubmissionCost. For example when migrateLPT is called, it would withdraw all LPT from bridgeMinter, and then create a L2 retryable ticket to call the L2Migrator using _maxSubmissionCost supplied by the caller.

function migrateLPT( uint256 _maxGas, uint256 _gasPriceBid, uint256 _maxSubmissionCost ) external payable whenNotPaused { uint256 amount = IBridgeMinter(bridgeMinterAddr) .withdrawLPTToL1Migrator(); // Approve L1LPTGateway to pull tokens ApproveLike(tokenAddr).approve(l1LPTGatewayAddr, amount); // Trigger cross-chain transfer with L1LPTGateway which will pull and escrow tokens // Forward msg.value to outboundTransfer() to be used for cross-chain tx IL1LPTGateway(l1LPTGatewayAddr).outboundTransfer{value: msg.value}( tokenAddr, l2MigratorAddr, amount, _maxGas, _gasPriceBid, abi.encode(_maxSubmissionCost, "") ); }

If the caller supplied a very low (e.g. 0) _maxSubmissionCost, the call would succeed on L1, but would fail the retryable ticket creation on L2.

https://developer.offchainlabs.com/docs/l1_l2_messages#important-note-about-base-submission-fee

If an L1 transaction underpays for a retryable ticket's base submission free, the retryable ticket creation on L2 simply fails. Given that this potentially breaks the atomicity of the L1 / L2 transactions, applications should avoid this scenario.

Since both migrateETH and migrateLPT are permissionless, an attacker can freeze the fund by calling the functions with _maxSubmissionCost=0.

Use ArbRetryableTx.getSubmissionPrice provided by Arbitrum L1 contract instead of using user provided _maxSubmissionCost.

Findings Information

🌟 Selected for report: WatchPug

Also found by: gzeon

Labels

bug
duplicate
2 (Med Risk)

Awards

15.3557 LPT - $569.39

1565.8688 USDC - $1,565.87

External Links

Handle

gzeon

Vulnerability details

Impact

L1Migrator can call withdrawETHToL1Migrator to withdraw ETH from BridgeMinter, but L1Migrator does not have a payable receive function so the call will revert.

Proof of Concept

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

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; }

Add a payable receive function

#0 - yondonfu

2022-01-23T00:51:58Z

Findings Information

🌟 Selected for report: gzeon

Labels

bug
2 (Med Risk)
resolved
sponsor confirmed

Awards

34.1238 LPT - $1,265.31

3479.7085 USDC - $3,479.71

External Links

Handle

gzeon

Vulnerability details

Impact

Fund can be lost if the L1 call value provided is insufficient to cover _maxSubmissionCost, or stuck if insufficient to cover _maxSubmissionCost + (_maxGas * _gasPriceBid).

Proof of Concept

outboundTransfer in L1LPTGateway does not check if the call value is sufficient, if it is < _maxSubmissionCost the retryable ticket creation will fail and fund is lost; if it is <_maxSubmissionCost + (_maxGas * _gasPriceBid) the ticket would require manual execution.

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

function outboundTransfer( address _l1Token, address _to, uint256 _amount, uint256 _maxGas, uint256 _gasPriceBid, bytes calldata _data ) external payable override whenNotPaused returns (bytes memory) { require(_l1Token == l1Lpt, "TOKEN_NOT_LPT"); // nested scope to avoid stack too deep errors address from; uint256 seqNum; bytes memory extraData; { uint256 maxSubmissionCost; (from, maxSubmissionCost, extraData) = parseOutboundData(_data); require(extraData.length == 0, "CALL_HOOK_DATA_NOT_ALLOWED"); // transfer tokens to escrow TokenLike(_l1Token).transferFrom(from, l1LPTEscrow, _amount); bytes memory outboundCalldata = getOutboundCalldata( _l1Token, from, _to, _amount, extraData ); seqNum = sendTxToL2( l2Counterpart, from, maxSubmissionCost, _maxGas, _gasPriceBid, outboundCalldata ); } emit DepositInitiated(_l1Token, from, _to, seqNum, _amount); return abi.encode(seqNum); }

Add check similar to the one used in L1GatewayRouter provided by Arbitrum team

https://github.com/OffchainLabs/arbitrum/blob/b8366005a697000dda1f57a78a7bdb2313db8fe2/packages/arb-bridge-peripherals/contracts/tokenbridge/ethereum/gateway/L1GatewayRouter.sol#L236

uint256 expectedEth = _maxSubmissionCost + (_maxGas * _gasPriceBid); require(_maxSubmissionCost > 0, "NO_SUBMISSION_COST"); require(msg.value == expectedEth, "WRONG_ETH_VALUE");

#0 - yondonfu

2022-01-21T17:04:01Z

Labeled as disagree with severity because we think this is a 2 - Med finding. We think that the likelihood of this occurring is low because in almost all cases users should be interacting with this contract using an application that handles calculating the maxSubmissionCost properly which would prevent the reported issue. However, we do think that the impact if this occurs is high since LPT and ETH could be lost if the reported issue happens. Thus, we think 2 - Med is appropriate given that assets are not at direct risk, but there is a low probability path for assets to be lost.

#1 - 0xleastwood

2022-01-29T23:55:19Z

I agree, there is potential for unintentional loss of funds, however, the attack vector makes assumptions on how this might occur. Due to the unlikely nature, I agree that this should be a medium severity issue..

Findings Information

🌟 Selected for report: kemmio

Also found by: 0x1f8b, danb, gzeon, pauliax

Labels

bug
duplicate
1 (Low Risk)

Awards

1.4926 LPT - $55.34

152.2025 USDC - $152.20

External Links

Handle

gzeon

Vulnerability details

Impact

Anyone can migrate address(0) since there is no check to make sure _l1Addr != address(0) and recoverSigner return address(0) if _sig.length == 0, which allow anyone to submit migration tx with _l1Addr = address(0) and _sig = ""

Proof of Concept

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

function requireValidMigration( address _l1Addr, address _l2Addr, bytes32 _structHash, bytes memory _sig ) internal view { require( _l2Addr != address(0), "L1Migrator#requireValidMigration: INVALID_L2_ADDR" ); require( msg.sender == _l1Addr || recoverSigner(_structHash, _sig) == _l1Addr, "L1Migrator#requireValidMigration: FAIL_AUTH" ); } function recoverSigner(bytes32 _structHash, bytes memory _sig) internal view returns (address) { if (_sig.length == 0) { return address(0); } bytes32 hash = _hashTypedDataV4(_structHash); return ECDSA.recover(hash, _sig); }
require(_l1Addr != address(0))

#0 - yondonfu

2022-01-23T00:43:40Z

Findings Information

🌟 Selected for report: robee

Also found by: Dravee, defsec, gzeon

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.1473 LPT - $5.46

15.0198 USDC - $15.02

External Links

Handle

gzeon

Vulnerability details

Impact

Upgrade to Solidity ^0.8.4 bring gas saving and allow the use of custom errors to optimize gas usage. https://blog.soliditylang.org/2021/04/21/custom-errors/

#0 - yondonfu

2022-01-23T21:08:11Z

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

gzeon

Vulnerability details

Impact

Shortening revert strings to fit in 32 bytes will decrease deployment time gas and will decrease runtime gas when the revert condition has been met.

Revert strings that are longer than 32 bytes require at least one additional mstore, along with additional overhead for computing memory offset, etc.

Proof of Concept

e.g. ./contracts/L1/gateway/L1Migrator.sol:508 ./contracts/L1/gateway/L1Migrator.sol:513 ./contracts/L2/gateway/L2Migrator.sol:136 ./contracts/L2/gateway/L2Migrator.sol:184 ./contracts/L2/gateway/L2Migrator.sol:201 ./contracts/L2/gateway/L2Migrator.sol:221 ./contracts/L2/gateway/L2Migrator.sol:257 ./contracts/L2/gateway/L2Migrator.sol:269 ./contracts/L2/gateway/L2Migrator.sol:274 ./contracts/L2/pool/DelegatorPool.sol:39 ./contracts/L2/pool/DelegatorPool.sol:61

#0 - yondonfu

2022-01-23T21:07:23Z

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

gzeon

Vulnerability details

Impact

Some known to be safe math can be put in a unchecked block to save gas.

Proof of Concept

contracts/L2/gateway/L2LPTDataCache.sol:65

l2SupplyFromL1 -= _amount;

is safe since _amount <= l2SupplyFromL1

unchecked{ l2SupplyFromL1 -= _amount; }

#0 - yondonfu

2022-01-23T21:12:00Z

Findings Information

🌟 Selected for report: WatchPug

Also found by: Tomio, gzeon, hyh

Labels

bug
duplicate
G (Gas Optimization)

Awards

0.1473 LPT - $5.46

15.0198 USDC - $15.02

External Links

Handle

gzeon

Vulnerability details

Impact

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

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

to

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

#0 - yondonfu

2022-01-23T00:53:15Z

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