Platform: Code4rena
Start Date: 23/05/2022
Pot Size: $75,000 USDC
Total HM: 23
Participants: 75
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 13
Id: 130
League: ETH
Rank: 2/75
Findings: 4
Award: $9,014.68
π Selected for report: 2
π Solo Findings: 2
π Selected for report: Ruhum
6473.16 USDC - $6,473.16
According to the LayerZero docs, the default behavior is that when a transaction on the destination application fails, the channel between the src and dst app is blocked. Before any new transactions can be executed, the failed transaction has to be retried until it succeeds.
See https://layerzero.gitbook.io/docs/faq/messaging-properties#message-ordering & https://layerzero.gitbook.io/docs/guides/advanced/nonblockinglzapp
So an attacker is able to initiate a transaction they know will fail to block the channel between FTM and Optimism. The RedemptionSender & Receiver won't be usable anymore.
The RedemptionReceiver contract doesn't implement the non-blocking approach as seen here: https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/redeem/RedemptionReceiver.sol#L72-L105
An example implementation of the non-blocking approach by LayerZero: https://github.com/LayerZero-Labs/solidity-examples/blob/main/contracts/lzApp/NonblockingLzApp.sol
none
Use the non-blocking approach as described here
#0 - pooltypes
2022-06-13T18:01:35Z
Duplicate of #87
#1 - GalloDaSballo
2022-06-28T23:07:49Z
@pooltypes Can anyone send a message or would they need to be whitelisted?
#2 - GalloDaSballo
2022-07-01T00:54:57Z
If anyone can call and deny, the contract is not suited to handle exceptions and doesn't implement the forceReceive
function, meaning the channel can be griefed and I don't believe there's a way to remedy.
The contract needs to implement forceResumeReceive
to allow to remove malicious messages that may be received
I still am unsure if anyone can send a malicious message or if they need to be approved
#3 - GalloDaSballo
2022-07-01T00:55:21Z
If only the admin can this is a Medium Severity, if anyone can, this is a High Severity finding
#4 - GalloDaSballo
2022-07-01T00:56:21Z
From the documentation it seems like anyone can call the function: https://layerzero.gitbook.io/docs/guides/master/how-to-send-a-message
#5 - GalloDaSballo
2022-07-01T23:53:48Z
With the information that I have it seems like anyone can grief the endpoint making claims revert indefinitely, have reached out to the sponsor as well as LayerZero but have yet to receive any reply
#6 - GalloDaSballo
2022-07-05T19:44:25Z
With the information I currently have, it seems like the channel can be setup to receive messages only by the specified contract, however for multiple reasons, the message sent can cause a revert, and in lack of a "nonblocking" architecture, the messages can get stuck indefinitely.
However, the implementation under scope has none of these defenses, it seems like the contact under scope can be denied functionality by any caller that builds their own LZApp
See example of how to prevent untrusted callers: https://github.com/LayerZero-Labs/solidity-examples/blob/e46a95ce93347aa65680bef288e206af0e5a8917/contracts/lzApp/LzApp.sol#L28
Because of that, the message queue can be filled with blocking messages that cannot be removed.
Because the contract under scope also has no way of re-setting the queue, I have reason to believe that any attack can permanently brick the receiver.
For these reasons, I believe High Severity to be more appropriate
#7 - ethzoomer
2022-07-07T21:31:25Z
At this point in time we've already completed all of the redemptions
Is it possible to send a message from the contract other than what sender sends? lz's msg queues are per src addr. https://layerzero.gitbook.io/docs/faq/messaging-properties "STORED message will block the delivery of any future message from srcUA to all dstUA on the same destination chain and can be retried until the message becomes SUCCESS" The only way that can get gummed up is if redemption's over right?
#8 - GalloDaSballo
2022-07-07T22:23:53Z
My understanding is any sender can block the queue as the receiver will revert
That said if redemption is over, there's no loss beside the risk of burning funds from the FTM side
π Selected for report: 0xf15ers
Also found by: 0x52, Ruhum, WatchPug, berndartmueller, cccz, horsefacts, hyh, minhquanym, pauliax
https://github.com/code-423n4/2022-05-velodrome/blob/main/contracts/contracts/Bribe.sol#L41-L60
An attacker can add worthless reward tokens to reach the max possible number of tokens at no cost to the Bribe contract. They deny any legit tokens from being added.
Attacker calls Bribe.notifyRewardAmount()
repeatedly a worthless custom token.
function notifyRewardAmount(address token, uint amount) external lock { require(amount > 0); if (!isReward[token]) { require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens"); } // bribes kick in at the start of next bribe period uint adjustedTstamp = getEpochStart(block.timestamp); uint epochRewards = tokenRewardsPerEpoch[token][adjustedTstamp]; _safeTransferFrom(token, msg.sender, address(this), amount); tokenRewardsPerEpoch[token][adjustedTstamp] = epochRewards + amount; if (!isReward[token]) { isReward[token] = true; rewards.push(token); IGauge(gauge).addBribeRewardToken(token); } emit NotifyReward(msg.sender, token, adjustedTstamp, amount); }
After calling the function 16 times, subsequent calls will fail because of the check on L44:
if (!isReward[token]) { require(rewards.length < MAX_REWARD_TOKENS, "too many rewards tokens"); }
If the user would use a legit token, that require
statement wouldn't be called.
none
There are multiple ways to fix this.
rewards
array. Then you don't have to limit the number of tokens.#0 - pooltypes
2022-06-13T15:52:31Z
Duplicate of #182
#1 - GalloDaSballo
2022-06-28T22:48:39Z
Dup of #182
π Selected for report: IllIllI
Also found by: Ruhum, rotcivegaf
After calling IERC721Receiver(_to).onERC721Received()
you have to verify that the returned value is bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))
.
Relevant code:
Whenever you make changes to a contract's config it should emit an event.
Relevant code:
./contracts/Voter.sol:82: function setGovernor(address _governor) public { ./contracts/Voter.sol:87: function setEmergencyCouncil(address _council) public { ./contracts/Velo.sol:26: function setMinter(address _minter) external { ./contracts/Velo.sol:31: function setRedemptionReceiver(address _receiver) external { ./contracts/Gauge.sol:188: function setVoteStatus(address account, bool voted) external { ./contracts/Bribe.sol:30: function setGauge(address _gauge) external { ./contracts/RewardsDistributor.sol:318: function setDepositor(address _depositor) external { ./contracts/VeloGovernor.sol:39: function setTeam(address newTeam) external { ./contracts/VeloGovernor.sol:44: function setProposalNumerator(uint256 numerator) external { ./contracts/Minter.sol:64: function setTeam(address _team) external { ./contracts/Minter.sol:75: function setTeamRate(uint _teamRate) external { ./contracts/VotingEscrow.sol:1060: function setVoter(address _voter) external {
#0 - GalloDaSballo
2022-07-04T22:08:54Z
Marking this as Dup of #185 raising to Med and deleting this from QA
π Selected for report: Ruhum
1941.948 USDC - $1,941.95
When sending a msg to the layer zero endpoint you include enough gas for the transaction. If you don't include enough tokens for the gas, the transaction will fail. The RedemptionSender contract allows the user to pass any value they want which might result in them sending not enough. Their transaction will fail.
To know how much you have to send there's the estimateFees()
function as described here
function redeemWEVE( uint256 amount, address zroPaymentAddress, bytes memory zroTransactionParams ) public payable { require(amount != 0, "AMOUNT_ZERO"); require( IERC20(weve).transferFrom( msg.sender, 0x000000000000000000000000000000000000dEaD, amount ), "WEVE: TRANSFER_FAILED" ); ILayerZeroEndpoint(endpoint).send{value: msg.value}( optimismChainId, abi.encodePacked(optimismReceiver), abi.encode(msg.sender, amount), payable(msg.sender), zroPaymentAddress, zroTransactionParams ); }
none
Use the estimateFees()
endpoint
#0 - GalloDaSballo
2022-06-28T23:01:09Z
The transaction shown has a case in which it could succeed but the message wouldn't be forwarded.
That is because any estimate_gas will make the tx work but the gas payment may not be sufficient to fund the message fees.
Because this can cause burned tokens, I believe a check should be added and since the tx must originate from the fantomSender
there would be no way of forwarding the message on the behalf of the sender.
For those reasons I believe the finding is of Medium Severity