Unlock Protocol contest - 0x0x0x's results

Protocol for memberships built on a blockchain, where fans can support their favourite creators by purchasing their NFTs!

General Information

Platform: Code4rena

Start Date: 18/11/2021

Pot Size: $50,000 USDC

Total HM: 18

Participants: 26

Period: 7 days

Judge: leastwood

Total Solo HM: 12

Id: 54

League: ETH

Unlock Protocol

Findings Distribution

Researcher Performance

Rank: 7/26

Findings: 5

Award: $1,467.49

🌟 Selected for report: 7

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: pauliax

Also found by: 0x0x0x, GiveMeTestEther, Reigada, Ruhum, WatchPug, cmichel, kenzo

Labels

bug
duplicate
1 (Low Risk)

Awards

71.4681 USDC - $71.47

External Links

Handle

0x0x0x

Vulnerability details

Impact

Funds of locks using tokens with not reverting transferFrom can be stolen.

Proof of Concept

Not all ERC20 tokens revert, when a transfer fail. Some tokens return false in that case. Currently return values are not checked at 2 spots. Let's assume there is a lock using a such ERC20 token like USDT. A malicious user with an empty wallet can buy keys for free by calling MixinPurchase.sol#purchase. Moreover, if a refund is possible, the user can refund the key to steal funds from the Lock. The attacker can repeat those steps to steal all tokens from the Lock (At lest iteration, the attacker send some token to the contract to allow the transfer).

Tools Used

Manual analysis

Use safeTransferFrom of OZ.

#0 - 0xleastwood

2022-01-16T08:21:25Z

Duplicate of #162

Findings Information

🌟 Selected for report: kenzo

Also found by: 0x0x0x, WatchPug

Labels

bug
duplicate
2 (Med Risk)
sponsor acknowledged

Awards

322.7515 USDC - $322.75

External Links

Handle

0x0x0x

Vulnerability details

Impact

A lock manager changes keyPrice. If the price is increased, then possible refunds are also increased and it can result in theft of funds. If the price is reduced, by doing so lock manager can reduce the refund payments and scam the users.

Tools Used

Manual analysis

Save keyPrice for each NFT

#0 - julien51

2022-01-03T15:30:04Z

This is correct, but also actually the intended behavior. When changing pricing the lock managers need to e careful about refund and for example make sure they apply a cancellation fee that covers the difference in the case of price increases.

#1 - 0xleastwood

2022-01-16T05:12:26Z

As the sponsor has stated in this issue and many other issues. The lock manager is a permissioned role with the highest level of access to the underlying lock. Therefore, they are able to make whatever changes they like. It is important to note that the lock manager can also be a contract owned by a community DAO. So it is expected that the lock manager makes enforces the necessary protections that prevent abuse.

Interestingly, the lock manager would be more inclined to abuse off-chain behaviour by not granting its members any benefits. This would be a mostly invisible way to attack the lock as on-chain behaviour is easily monitored. Hence, I've come to the decision of marking all issues related to lock manager abuse as invalid.

#2 - 0xleastwood

2022-01-16T09:48:24Z

Duplicate of #53

Findings Information

🌟 Selected for report: cmichel

Also found by: 0x0x0x

Labels

bug
duplicate
2 (Med Risk)
sponsor acknowledged

Awards

537.9192 USDC - $537.92

External Links

Handle

0x0x0x

Vulnerability details

Impact

Users can lose extra time, when they shareKey

Proof of Concept

When timePlusFee < timeRemaining does not hold, the fee is calculated as follow:

fee = getTransferFee(keyOwner, timeRemaining); time = timeRemaining - fee;

By doing so fee is also charged for the fee and more fee is charged than usual. So when timePlusFee = timeRemaining - 1, you transfer more time compared to when timePlusFee = timeRemaining. This also penalizes transferring your all of remaining time directly.

Tools Used

Manual analysis

Simplest approach is to directly take as input the time a user wants to remove and calculate fee of it. With current approach, mathematically you should divide timeRemaining with 1 + fee percentage to obtain how much time is transferred.

#0 - 0xleastwood

2022-01-16T11:44:01Z

Duplicate of #165

Findings Information

🌟 Selected for report: elprofesor

Also found by: 0x0x0x, harleythedog, loop, pauliax

Labels

bug
duplicate
1 (Low Risk)
sponsor acknowledged

Awards

52.2858 USDC - $52.29

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

Since this function doesn't check whether there is an implementation for the given version number. Implementations can be updated by calling this function, which is different from the description. Moreover, updating can create an attack vector and i don't recommend this behaviour. Also it would be nice, if it checks whether publicLockLatestVersion is incremented by one or not.

Tools Used

Manual analysis

Rename or add checks to make sure implementations doesn't get updated

#0 - julien51

2022-01-03T15:21:27Z

This addLockTemplate can only be called by the Unlock owner, who is the DAO. We should expect them to not be malicious (otherwise they could just upgrade the whole contract). Adding verifications would cost gas for no real benefit because the owner could still resubmit a new transaction to fix the previous one.

#1 - 0xleastwood

2022-01-16T11:42:24Z

Duplicate of #39

Findings Information

🌟 Selected for report: cmichel

Also found by: 0x0x0x, Reigada

Labels

bug
duplicate
1 (Low Risk)
sponsor acknowledged

Awards

107.5838 USDC - $107.58

External Links

Handle

0x0x0x

Vulnerability details

Impact

All ERC20 token does not support approving directly and require first approving for an amount of 0. By using safeApprove or approving 0 first, the function will have better compatability with different tokens.

Tools Used

Manual analysis

#0 - julien51

2022-01-03T15:14:24Z

Why is that a bug?

#1 - 0xleastwood

2022-01-16T11:45:48Z

Duplicate of #161

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

setVotingDelay, setVotingPeriod and setQuorum have a similar function structure. All can emit first and then change to avoid caching. Current implementation of setVotingDelay is as follows:

function setVotingDelay(uint256 newVotingDelay) public onlyGovernance { uint256 oldVotingDelay = _votingDelay; _votingDelay = newVotingDelay; emit VotingDelayUpdated(oldVotingDelay, newVotingDelay); }

This can be replaced with

function setVotingDelay(uint256 newVotingDelay) public onlyGovernance { emit VotingDelayUpdated(_votingDelay, newVotingDelay); _votingDelay = newVotingDelay; }

to save gas and have a shorter code.

Tools Used

Manual

#0 - 0xleastwood

2022-01-17T07:57:21Z

Nice find!

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

Example:

for (uint i = 0; i < arr.length; i++) { //Operations not effecting the length of the array. }

Loading length of array costs gas. Therefore, the length should be cached, if the length of the array doesn't change inside the loop. Recommended implementation:

uint length = arr.length; for (uint i = 0; i < length; i++) { //Operations not effecting the length of the array. }

By doing so the length is only loaded once rather than loading it as many times as iterations (Therefore, less gas is spent).

Occurences

./contracts/mixins/MixinGrantKeys.sol:29: for(uint i = 0; i < _recipients.length; i++) {

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

It is better to put require statements as early as possible to avoid wasting gas.

https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/mixins/MixinGrantKeys.sol#L30-L37

can be replaced with following code block

address recipient = _recipients[i]; require(recipient != address(0), 'INVALID_ADDRESS'); uint expirationTimestamp = _expirationTimestamps[i]; Key storage toKey = keyByOwner[recipient]; require(expirationTimestamp > toKey.expirationTimestamp, 'ALREADY_OWNS_KEY'); address keyManager = _keyManagers[i];

Tools Used

Manual

#0 - 0xleastwood

2022-01-17T07:57:54Z

I think this makes logical sense.

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

As mentioned in the comment this line is safe to execute. by using unchecked gas can be saved. https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/Unlock.sol#L313

Tools Used

manual

#0 - 0xleastwood

2022-01-17T07:57:40Z

Nice optimisation!

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

lock, impl, currentVersion and proxy are used only once, but all of them are cached. To save gas, all of them can be directly passed.

#0 - 0xleastwood

2022-01-17T07:57:30Z

This sounds right!

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

Current implementation converts addresses the bytes32, does skip empty bytes and read where the address is. To do so + 12 is used to loop over the address. By directly converting addresses to bytes20, gas can be saved and more gas will be saved by avoiding + 12. By doing so the function consumes around %10 less gas.

function address2Str( address _addr ) internal pure returns(string memory) { bytes32 value = bytes20(_addr); bytes memory alphabet = '0123456789abcdef'; bytes memory str = new bytes(42); str[0] = '0'; str[1] = 'x'; for (uint i = 0; i < 20; i++) { str[2+i*2] = alphabet[uint8(value[i] >> 4)]; str[3+i*2] = alphabet[uint8(value[i] & 0x0f)]; } return string(str); }

Tools Used

Manual analysis

#0 - 0xleastwood

2022-01-17T07:55:04Z

Warden is correct here. Nice optimisation! Tested this in remix.

Findings Information

🌟 Selected for report: 0x0x0x

Labels

bug
G (Gas Optimization)

Awards

53.6422 USDC - $53.64

External Links

Handle

0x0x0x

Vulnerability details

Proof of Concept

https://github.com/code-423n4/2021-11-unlock/blob/ec41eada1dd116bcccc5603ce342257584bec783/smart-contracts/contracts/Unlock.sol#L329-L362 can be implemented cheaper, with several upgrades as follows:

if (chainId > 1) { // non mainnet: we distribute tokens using asymptotic curve between 0 and 0.5 // maxTokens = IMintableERC20(udt).balanceOf(address(this)).mul((valueInETH / grossNetworkProduct) / (2 + 2 * valueInETH / grossNetworkProduct)); uint balance = IMintableERC20(udt).balanceOf(address(this)); maxTokens = balance * valueInETH / (2 + 2 * valueInETH / grossNetworkProduct) / grossNetworkProduct; if(maxTokens > 0){ if(tokensToDistribute > maxTokens){ tokensToDistribute = maxTokens; } } if (balance > tokensToDistribute) { uint devReward = tokensToDistribute * 20 / 100; // Only distribute if there are enough tokens IMintableERC20(udt).transfer(_referrer, tokensToDistribute - devReward); IMintableERC20(udt).transfer(owner(), devReward); } } else { // Mainnet: we mint new token using log curve maxTokens = IMintableERC20(udt).totalSupply() * valueInETH / 2 / grossNetworkProduct; if(maxTokens > 0){ if(tokensToDistribute > maxTokens){ tokensToDistribute = maxTokens; } } uint devReward = tokensToDistribute * 20 / 100; IMintableERC20(udt).mint(_referrer, tokensToDistribute - devReward); IMintableERC20(udt).mint(owner(), devReward); }

This code block makes less computations, when maxTokens == 0. Moreover balance is cached and shared.

Tools Used

Manual

#0 - CloudEllie

2021-11-23T21:57:51Z

Warden noted the above code block has some mistakes, and requested it be replaced with the following:

if (chainId > 1) { // non mainnet: we distribute tokens using asymptotic curve between 0 and 0.5 // maxTokens = IMintableERC20(udt).balanceOf(address(this)).mul((valueInETH / grossNetworkProduct) / (2 + 2 * valueInETH / grossNetworkProduct)); uint balance = IMintableERC20(udt).balanceOf(address(this)); maxTokens = balance * valueInETH / (2 + 2 * valueInETH / grossNetworkProduct) / grossNetworkProduct; if(tokensToDistribute > maxTokens){ tokensToDistribute = maxTokens; } if (tokensToDistribute > 0 && balance > tokensToDistribute) { uint devReward = tokensToDistribute * 20 / 100; // Only distribute if there are enough tokens IMintableERC20(udt).transfer(_referrer, tokensToDistribute - devReward); IMintableERC20(udt).transfer(owner(), devReward); } } else { // Mainnet: we mint new token using log curve maxTokens = IMintableERC20(udt).totalSupply() * valueInETH / 2 / grossNetworkProduct; if(tokensToDistribute > maxTokens){ tokensToDistribute = maxTokens; } if (tokensToDistribute > 0){ uint devReward = tokensToDistribute * 20 / 100; IMintableERC20(udt).mint(_referrer, tokensToDistribute - devReward); IMintableERC20(udt).mint(owner(), devReward); } }
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