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
Rank: 4/26
Findings: 8
Award: $4,980.49
🌟 Selected for report: 6
🚀 Solo Findings: 2
kenzo
Not all ERC20 tokens revert on failed transfer.
This is why using SafeERC20 is recommended.
purchase
does not use it.
If a key is using as payment an ERC20 token that does not throw on failed transfer, a user will be able to buy a key without paying, and even later claiming a refund, thereby stealing other users funds.
purchase
method uses regular transferFrom
:
token.transferFrom(msg.sender, address(this), pricePaid);
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinPurchase.sol#L97
(Although IERC20Upgradeable
is already using SafeERC20Upgradeable
)
Change the line to:
token.safeTransferFrom(msg.sender, address(this), pricePaid);
#0 - julien51
2021-11-23T03:36:18Z
I think this is a duplicate of https://github.com/code-423n4/2021-11-unlock-findings/issues/33 ?
#1 - 0xleastwood
2022-01-16T05:26:26Z
Duplicate of #162
🌟 Selected for report: kenzo
kenzo
Unlock contains a feature in which a key buyer can ask for a refund. The refund is sent from the lock - where the purchase funds were sent. The lock manager can withdraw all funds from the lock. Therefore, if the lock manager withdraws enough profits from the lock, the user would not be able to cancel his key and request refund. Even if a lock manager is not malicious, if he would want to enable users to cancel their key, he would have to keep track of how much tokens need to be kept in the contract in order to enable this - not a trivial calculation. A naive lock manager might accidently disable refunds for his clients.
Refunds are not guaranteed. A user might buy a key expecting to cancel it within some time, only to discover he can not cancel it. (This loss of user funds if why I consider this a high risk finding.) An unaware lock manager who just wants to withdraw all his profits might accidently discover that he removed his users' ability to cancel their key.
It seems the Unlock team is aware to some extent that withdrawing breaks refunds, as they state in the withdraw
function:
* TODO: consider allowing anybody to trigger this as long as it goes to owner anyway? * -- however be wary of draining funds as it breaks the `cancelAndRefund` and `expireAndRefundFor` * use cases.
However, even if just the owner is allowed to call it, he may break the refund functionality - on purpose or accidently. Looking on Unlock documentation I don't see a warning to creators about withdrawing their funds.
withdraw
function has no limit on the amount withdrawn, therefore the owner can withdraw all funds:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinLockCore.sol#L133:#L162
cancelAndRefund
transfers the funds from the same lock contract:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinRefunds.sol#L118
Therefore if there are not enough funds, the transfer will fail.
Perhaps a sort of MasterChef-like shares system can be implemented in order to make sure the owner leaves enough funds in the lock to process refunds.
#0 - julien51
2021-11-23T03:34:05Z
As noted, this is actually documented. You are right though that we should make this more obvious on the UI. I would not classify this as High Risk.
#1 - 0xleastwood
2022-01-16T08:27:03Z
Nice find! I think this can be downgraded to medium
as the availability of the protocol is impacted by this issue.
#2 - julien51
2022-03-16T06:45:31Z
This would not affect the whole protocol but only the "malicious" lock and it is impractice not only documented but also how these things work in the real world. If Netflix went out of business tomorrow, I could not get a refund on this month's membership fee...
#3 - 0xleastwood
2022-03-22T21:30:06Z
While I mostly agree with the sponsor, this may be intended behaviour as user's should not be entitled to a refund in this case. However, based on what was known at the time, it seemed like this broke the functionality of cancelAndRefund
and expireAndRefundFor
functions, hence why it was marked as medium
severity.
322.7515 USDC - $322.75
kenzo
Lock manager can change key pricing. The refund mechanism calculates refund according to current key price, not price actually paid.
A user refunding can get less (or more) funds than deserved.
Refund only takes the current price into account: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinRefunds.sol#L144:#L152 Lock manager can update key price at any point, and the old price is not saved anywhere: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinLockCore.sol#L183 So if for example a key price has gone down, a user who tried to refund will get less funds than deserved.
Consider saving the amount the user paid, and refund according to that. Or having a kind of a price snapshot/version mechanism.
#0 - julien51
2021-11-23T03:35:42Z
This is a known issue... but indeed we should show things in the UI to indicate things to users.
#1 - 0xleastwood
2022-01-16T08:29:21Z
Agree this sounds like an issue! However, I don't think this can be justified as a high
risk issue. But it does seem that the protocol could leak value and impact users, so marking this as medium
.
#2 - julien51
2022-03-16T06:46:24Z
We actually are adding a new mechanism to keep track of the last price paid by any user which means we could use it in the next version to solve this issue!
🌟 Selected for report: kenzo
Also found by: GiveMeTestEther, cmichel
322.7515 USDC - $322.75
kenzo
If calling transferFrom
with _from == _recipient
, the key will get destroyed (meaning the key will be set as expired and set the owner's key to be 0).
A key manager or approved might accidently destroy user's token.
Note: this requires user error and so I'm not sure if this is a valid finding. However, few things make me think that it is valid:
By following transferFrom
's execution:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L109:#L166
One can see that in the case where _from == _recipient
with a valid key:
Add a require statement in the beginning of transferFrom
:
require(_from != _recipient, 'TRANSFER_TO_SELF');
#0 - julien51
2022-03-16T06:46:36Z
Fixed since then :)
🌟 Selected for report: GiveMeTestEther
Also found by: kenzo
537.9192 USDC - $537.92
kenzo
A malicious user can call shareKey repeatedly, transferring miniscule amounts of his key to different accounts, thereby minting new keys until maxNumberOfKeys is reached.
Malicious user can grief and make lock purchasing become disabled.
In shareKey
, any user can mint a new token without paying anything extra:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L83:#L84
And there's no minimum on _timeShared.
Once _assignNewTokenId
is called, _totalSupply
is incremented.
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L311:#L322
Therefore a user can call shareKey
repeatedely until a lock has been "sold out".
Note: maxNumberOfKeys
cannot be updated after lock deployment.
I think it depends design/ux wise how you want to mitigate this.
One option for example would be to not count these shared keys towards the maxNumberOfKeys
.
Note: a similar situation exists with grantKey
, where the lock manager can grant keys which are free but still counted towards maxNumberOfKeys
. But there I find it less of an issue as only the lock manager can issue these keys, so it's perhaps more acceptible.
#0 - julien51
2022-01-03T12:49:09Z
Thanks! We have actually added the ability to increase the supply by the lock manager recently. Additionnaly the lock manager can cancel the abuser's keys.
#1 - 0xleastwood
2022-01-16T11:30:02Z
Duplicate of #242
🌟 Selected for report: elprofesor
Also found by: kenzo
kenzo
Unlock can add a new lock version, which lock owners can upgrade to, even before the implementation has been initialized. So anybody could initialize it before Unlock does.
If somebody else has initialized the new lock version before Unlock has called setLockTemplate
-
Unlock couldn't set this version as the default template as it has already been initialized. (Unlock would have to deploy another implementation.)
If the new lock version has some mechanism of delegatecall or selfdestruct, the malicious actor could call it and brick the locks which have upgraded to that version.
When Unlock is adding a new lock version, there is no verification on whether it is initialized or not. https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L187:#L193 When upgrading a lock, there is no verification on whether it is initialized or not. https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L237:#L255 Only if the lock is being set as the default lock template, would it be initialized: https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/Unlock.sol#L423:#L438 But at that time it might have already been initialized by somebody else.
I might be missing something, but I see no reason to not move the initialization of the lock to addLockTemplate
. This will ensure that any lock implementation that is used by the protocol/users belongs to Unlock and not to another actor.
#0 - 0xleastwood
2022-01-16T09:22:21Z
Duplicate of #132
🌟 Selected for report: kenzo
1195.3761 USDC - $1,195.38
kenzo
More keys can be minted than maxNumberOfKeys since shareKey
and grantKey
do not check if the lock is sold out.
More keys can be minted than intended.
In both shareKey
and grantKey
, if minting a new token, a new token is simply minted (and _totalSupply
increased) without checking it against maxNumberOfKeys
.
This is unlike purchase
, which has the notSoldOut
modifier.
grantKey
:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinGrantKeys.sol#L41:#L42
shareKey
:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinTransfer.sol#L83:#L84
Both functions call _assignNewTokenId
which does not check maxNumberOfKeys.
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L311:#L322
So you can say that _assignNewTokenId
is actually the root of the error, and this is why I am submitting this as 1 finding and not 2 (for grantKey/shareKey).
Add a check to _assignNewTokenId
that will revert if we need to record a new key and maxNumberOfKeys
has been reached.
#0 - julien51
2022-01-03T12:42:26Z
This is actually intentional. We want the lock manager to be able to grant keys even if the lock is sold out. Note that the lock manager could also increase the supply if they needed anyway.
#1 - julien51
2022-01-03T12:43:58Z
However, we should take that into account in the shareKey
flow so I'll mark as confirmed for that flow.
🌟 Selected for report: kenzo
398.4587 USDC - $398.46
kenzo
A user calling setKeyManagerOf
with address 0 will succeed, thereby locking himself out of managing his key.
This can only be remedied by the lock manager himself updating the key manager. (But as I mentioned in a previous finding, I believe that capability is problematic in itself and should be considered for removal.)
The user will retrain ownership but lose control of his key.
There is no zero address check on setKeyManagerOf
:
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L207:#L231
Add a zero address check for _keyManager
.
#0 - julien51
2022-01-03T12:40:52Z
As you noted this can easily be recovered by the lock manager (and no removing the role of lock manager does not make sense!)
#1 - 0xleastwood
2022-01-16T11:27:36Z
As this does not directly impact protocol availability and rather affects how the function state is handled, I'll mark this as low
severity.
🌟 Selected for report: kenzo
398.4587 USDC - $398.46
kenzo
_setKeyManagerOf
always emits address(0)
as the new key manager.
Wrong event emitted.
The code is:
emit KeyManagerChanged(_tokenId, address(0));
https://github.com/code-423n4/2021-11-unlock/blob/main/smart-contracts/contracts/mixins/MixinKeys.sol#L229
Change line to
emit KeyManagerChanged(_tokenId, _keyManager);