Closed c4-submissions closed 1 year ago
GalloDaSballo marked the issue as primary issue
0xfoobar (sponsor) confirmed
The POC invalidates the finding
function testFuzzedERC721Withdraw(address from, uint256 underlyingTokenId) public {
address from = address(0xadadadadada);
uint256 underlyingTokenId = 98765432;
address delegationRecipient = address(0x4a5df);
bool expiryTypeRelative = true;
uint256 time = 123;
( /* ExpiryType */ , uint256 expiry, /* ExpiryValue */ ) = prepareValidExpiry(expiryTypeRelative, time);
mockERC721.mint(address(from), underlyingTokenId);
vm.startPrank(from);
mockERC721.setApprovalForAll(address(dt), true);
uint256 delegateId =
dt.create(DelegateTokenStructs.DelegateInfo(from, IDelegateRegistry.DelegationType.ERC721, delegationRecipient, 0, address(mockERC721), underlyingTokenId, "", expiry), SALT);
address principalOwner = principal.ownerOf(delegateId);
console2.log("principalOwner", principalOwner);
address dtRecipient = dt.ownerOf(delegateId);
console2.log("dtRecipient", dtRecipient);
// Wait for expiry
vm.warp(expiry + 1); // More than expiry
// Verify Expiry
console2.log("expiry", (dt.getDelegateInfo(delegateId)).expiry);
console2.log("now", block.timestamp);
// Call withdraw (without PT)
vm.stopPrank();
vm.prank(delegationRecipient);
dt.withdraw(delegateId); /// @audit Reverts here
address ownerOfUnderlying = mockERC721.ownerOf(underlyingTokenId);
console2.log("ownerOfUnderlying", ownerOfUnderlying);
// Verification
assertFalse(registry.checkDelegateForERC721(from, address(dt), address(mockERC721), underlyingTokenId, ""));
assertEq(ownerOfUnderlying, from, "Stolen");
}
The principalToken
is burned from the caller
Meaning only they can call withdraw and receive the token
@0xfoobar lmk if I missed anything
There's authentication in StorageHelpers.burnPrincipal()
which requires that msg.sender
be the owner or approved operator of the PT. So no risk here, invalid issue: https://github.com/code-423n4/2023-09-delegate/blob/main/src/libraries/DelegateTokenStorageHelpers.sol#L72
0xfoobar (sponsor) disputed
Per the above, closing as invalid
GalloDaSballo marked the issue as unsatisfactory: Invalid
Lines of code
https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L353
Vulnerability details
Proof of Concept
The
DelegateToken.withdraw
is used for the Principal Token Holder to burn his Principal Token and withdraw the underlying asset:In the code above, it's visible that there are two access controls:
First is
revertInvalidWithdrawalConditions
, which checks some withdraw conditions. If the expiry date has passed, then It doesn't revert. Then, the condition checks are verified using (OR) operator. So, ifdelegateTokenHolder == msg.sender
, this function will not revert, which allows the Delegate Token Holder to call it.The next access control is
RegistryHelpers.revokeERC721/20/1155
and it basically checks if a delegation exists, which is true since we are withdrawing with an active Delegation Token.Then, after
Delegation Token Holder
passes all these access controls, aERC721/1155/20.transferFrom
is used usingmsg.sender
asto
.The Delegate Token Holder gets the Underlying Asset without owning the Principal Token.
Impact
If rescind is not used after Expiry Date, delegation Token Holder can steal the Underlying Asset without having the Principal Token or permission for that.
Tools Used
Manual Review
Recommended Mitigation Steps
Check if msg.sender is the owner of Principal Token
Or use
IERC721(principalToken).ownerOf(delegateTokenId)
as the target ofERC20/1155/721.transferFrom
.Assessed type
Access Control