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
Rank: 1/27
Findings: 14
Award: $23,665.13
🌟 Selected for report: 18
🚀 Solo Findings: 6
🌟 Selected for report: WatchPug
Also found by: Ruhum, gzeon, harleythedog
20.7302 LPT - $768.68
2113.9229 USDC - $2,113.92
WatchPug
Per the arb-bridge-eth
code:
all msg.value will deposited to callValueRefundAddress on L2
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:
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
.
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.
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!
15.3557 LPT - $569.39
1565.8688 USDC - $1,565.87
WatchPug
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.
(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.
All the ETH held by the BridgeMinter
can get stuck in the contract.
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.
🌟 Selected for report: WatchPug
34.1238 LPT - $1,265.31
3479.7085 USDC - $3,479.71
WatchPug
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.
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.
15.3557 LPT - $569.39
1565.8688 USDC - $1,565.87
WatchPug
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).
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); }
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
🌟 Selected for report: WatchPug
34.1238 LPT - $1,265.31
3479.7085 USDC - $3,479.71
WatchPug
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.
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 ?
🌟 Selected for report: WatchPug
34.1238 LPT - $1,265.31
3479.7085 USDC - $3,479.71
WatchPug
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);
function outboundTransfer( address _l1Token, address _to, uint256 _amount, bytes calldata _data ) public payable returns (bytes memory) { return outboundTransfer(_l1Token, _to, _amount, 0, 0, _data); }
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)
:
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.
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
5.1186 LPT - $189.80
521.9563 USDC - $521.96
WatchPug
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
.
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); }
Consider removing it or changing to:
require( claimedInitialStake + _stake <= initialStake, "DelegatorPool#claim: FULLY_CLAIMED" );
#0 - yondonfu
2022-01-31T21:20:04Z
5.1186 LPT - $189.80
521.9563 USDC - $521.96
WatchPug
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.
#0 - yondonfu
2022-01-31T21:17:38Z
0.3637 LPT - $13.49
37.086 USDC - $37.09
WatchPug
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:
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.
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); }
#0 - yondonfu
2022-01-23T19:56:50Z
#1 - 0xleastwood
2022-01-30T05:29:24Z
Marking as primary issue.
#2 - yondonfu
2022-02-02T17:01:16Z
0.0614 LPT - $2.28
6.2568 USDC - $6.26
WatchPug
uint256 total = 0;
for (uint256 i = 0; i < _unbondingLockIds.length; i++)
Setting uint256
variables to 0
is redundant as they default to 0
.
#0 - kautukkundan
2022-02-01T19:10:37Z
🌟 Selected for report: defsec
Also found by: Dravee, WatchPug, byterocket, robee
0.1061 LPT - $3.93
10.8143 USDC - $10.81
WatchPug
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:
L1Migrator.sol#getMigrateUnbondingLocksParams()
L2Migrator.sol#finalizeMigrateUnbondingLocks()
#0 - yondonfu
2022-01-23T00:57:22Z
0.0614 LPT - $2.28
6.2568 USDC - $6.26
WatchPug
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:
for (uint256 i = 0; i < _unbondingLockIds.length; i++)
for (uint256 i = 0; i < _params.unbondingLockIds.length; i++)
#0 - yondonfu
2022-01-23T14:23:59Z
0.3637 LPT - $13.49
37.086 USDC - $37.09
WatchPug
// 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()
.
Change to:
// Calculate stake owed to delegator uint256 owedStake = (pendingStake() * _stake) / (initialStake - claimedInitialStake); // Calculate fees owed to delegator uint256 owedFees = (pendingFees() * _stake) / (initialStake - claimedInitialStake);
#0 - yondonfu
2022-02-01T17:30:46Z
0.3637 LPT - $13.49
37.086 USDC - $37.09
WatchPug
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
).
Change to:
return l1TotalSupply > l2SupplyFromL1 ? l1TotalSupply - l2SupplyFromL1 : 0;
#0 - yondonfu
2022-01-23T20:20:05Z
🌟 Selected for report: sirhashalot
Also found by: Dravee, Jujic, WatchPug, aga7hokakological, defsec, gzeon, p4st13r4, robee
0.0387 LPT - $1.43
3.9418 USDC - $3.94
WatchPug
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:
require( _l2Addr != address(0), "L1Migrator#requireValidMigration: INVALID_L2_ADDR" );
require( msg.sender == _l1Addr || recoverSigner(_structHash, _sig) == _l1Addr, "L1Migrator#requireValidMigration: FAIL_AUTH" ); }
require( !migratedDelegators[_params.l1Addr], "L2Migrator#finalizeMigrateDelegator: ALREADY_MIGRATED" );
require(ok, "L2Migrator#finalizeMigrateDelegator: FAIL_FEE");
require( !migratedUnbondingLocks[_params.l1Addr][id], "L2Migrator#finalizeMigrateUnbondingLocks: ALREADY_MIGRATED" );
require( !migratedSenders[_params.l1Addr], "L2Migrator#finalizeMigrateSender: ALREADY_MIGRATED" );
require( claimStakeEnabled, "L2Migrator#claimStake: CLAIM_STAKE_DISABLED" );
require( merkleSnapshot.verify(keccak256("LIP-73"), _proof, leaf), "L2Migrator#claimStake: INVALID_PROOF" );
require( !migratedDelegators[delegator], "L2Migrator#claimStake: ALREADY_MIGRATED" );
require(msg.sender == migrator, "DelegatorPool#claim: NOT_MIGRATOR");
require( claimedInitialStake < initialStake, "DelegatorPool#claim: FULLY_CLAIMED" );
#0 - yondonfu
2022-01-21T16:17:38Z
0.1473 LPT - $5.46
15.0198 USDC - $15.02
WatchPug
Unused named returns increase contract size and gas usage at deployment.
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.
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
0.0795 LPT - $2.95
8.1107 USDC - $8.11
WatchPug
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:
if (_amount > l2SupplyFromL1) { l2SupplyFromL1 = 0; } else { l2SupplyFromL1 -= _amount; }
l2SupplyFromL1 -= _amount
will never underflow.
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.
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.
return l1TotalSupply >= l2SupplyFromL1 ? l1TotalSupply - l2SupplyFromL1 : 0;
l1TotalSupply - l2SupplyFromL1
will never underflow.
#0 - yondonfu
2022-01-23T01:04:30Z
0.1473 LPT - $5.46
15.0198 USDC - $15.02
WatchPug
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.
#0 - yondonfu
2022-02-01T16:26:17Z
🌟 Selected for report: WatchPug
0.8082 LPT - $29.97
82.4134 USDC - $82.41
WatchPug
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:
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
.
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) );
#0 - yondonfu
2022-02-01T17:31:11Z
🌟 Selected for report: WatchPug
0.8082 LPT - $29.97
82.4134 USDC - $82.41
WatchPug
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.
bool public claimStakeEnabled;
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.
🌟 Selected for report: WatchPug
0.8082 LPT - $29.97
82.4134 USDC - $82.41
WatchPug
function _onlyController() internal view { require(msg.sender == address(controller), "caller must be Controller"); }
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.
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.
🌟 Selected for report: WatchPug
0.8082 LPT - $29.97
82.4134 USDC - $82.41
WatchPug
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.
🌟 Selected for report: WatchPug
Also found by: byterocket
0.3637 LPT - $13.49
37.086 USDC - $37.09
WatchPug
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); }
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.
* 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.
Remove the redundant code.
#0 - yondonfu
2022-01-31T21:17:04Z
🌟 Selected for report: WatchPug
0.8082 LPT - $29.97
82.4134 USDC - $82.41
WatchPug
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.
Surround the increment expressions with an unchecked { ... }
block to avoid the default overflow checks. For example, change the for loop:
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.