-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Codearena-145,147: PermitERC721 EIP-4494 compliance #818
Conversation
@@ -2,124 +2,264 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the license need to be switched from BUSL-1.1 since it uses some logic from a MIT licensed codebase?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the MIT license doesn't even require derivative works to be open-sourced. If we have used a "substantial portion" of that implementation, however, we should include a copyright notice in this file. However, the original work by dievardump
does not include a LICENSE file or copyright notice.
// this also allows the verifier to recover signatures made via contracts | ||
bool isValidSignature = | ||
SignatureChecker.isValidSignatureNow( | ||
ownerOf(tokenId_), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this does not cover the case where the signature is made by a contract that is not the owner of the tokenId but instead is approved by the owner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check also this one Fixed-Point-Solutions/prototech-ajna-audit#27
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR addresses the prototech issue as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I believe that this specific case is checked on line 1367 of PositionManager.t.sol @prateek105
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw the tests and the scenario I mentioned is different from the test, my scenario will fail for contracts but pass for EOA. Scenario is that an EOA account which is approved can give permit to spender address on behalf of owner but the same thing will fail for a contract which is approved trying to give permit to a spender. The same limitation applies to this reference implementation https://github.com/dievardump/erc721-with-permits/blob/main/contracts/ERC721WithPermit.sol as well.
I think that this is the limitation of ERC1271, so I am approving the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the usage of OpenZeppelin's SignatureChecker
library should fix the previous issues with isValidSignature
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, above issues are fixed now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good to me. added a comment here.
src/base/PermitERC721.sol
Outdated
|
||
// get chainId for the domain | ||
uint256 chainId; | ||
//solhint-disable-next-line no-inline-assembly | ||
assembly { | ||
chainId := chainid() | ||
} | ||
|
||
// save gas by storing the chainId and DomainSeparator in the state on deployment | ||
_domainChainId = chainId; | ||
_domainSeparator = _calculateDomainSeparator(chainId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See here for related issues (gas optimization and domain separator internal discussion):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in PR #850
// verify if the recovered address is owner or approved on tokenId | ||
// and make sure recoveredAddress is not address(0), else getApproved(tokenId) might match | ||
bool isOwnerOrApproved = | ||
(recoveredAddress_ != address(0) && _isApprovedOrOwner(recoveredAddress_, tokenId_)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, right, the zero address check is redundant I can add a commit for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zero address check is required as getApproved(tokenId)
can return zero address
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getApproved(tokenId)
uses the _exists
check, inside _requireMinted
, to ensure the tokenId
exists and checks also to ensure owner is not address zero (in oz v4.8.2
):
But I see that for the approved address
there doesn't seem to be any check and it could return indeed a zero address and considering the refactoring with the extra logic it makes sense now to have an explicit check for zero address due to the extra conditions, isApprovedForAll(owner, spender)
and getApproved(tokenId) == spender
(the above issue was related to the previous implementation specifically).
(address recoveredAddress, ) = ECDSA.tryRecover(digest, signature_); | ||
if (!_checkSignature(digest, signature_, recoveredAddress, tokenId_)) revert NotAuthorized(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why these extra steps are introduced instead of using directly isValidSignatureNow
check from SignatureChecker.sol
?
In this issue I've broken down the recommendation with related issues for consideration:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Primarily to offload as much of the logic as possible to well audited and tested external libraries. I recognize the gas and storage size benefits to just implementing that logic directly, I had just avoided it to minimize the amount of logic that is self-rolled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another reason we can not use isValidSignatureNow
is that this check returns just the bool. we want to get the recovered address because the recovered address can wither be owner or the approved account. if we use isValidSignatureNow
permit will fail when the approved account(not the owner) has given the permit. This check here, _isApprovedOrOwner(recoveredAddress_, tokenId_)
@naszam
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noticed now that the refactoring is following the implementation linked in the description, I'll check that in depth and adjust the certora specs accordingly (the related issues linked were referring to the previous implementation, so they might need to be adjusted following the linked implementation where the _isApprovedOrOwner
function seems to be required, thanks for flagging that out).
…ssembly with block.chainid (#850)
@@ -2,124 +2,264 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the MIT license doesn't even require derivative works to be open-sourced. If we have used a "substantial portion" of that implementation, however, we should include a copyright notice in this file. However, the original work by dievardump
does not include a LICENSE file or copyright notice.
src/base/PermitERC721.sol
Outdated
"\x19\x01", | ||
DOMAIN_SEPARATOR(), | ||
// increment the permit nonce of the given tokenId | ||
_incrementNonce(tokenId_); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_incrementeNonce
seems to be done inside the overrided _transfer
in the reference linked implementation (see here), in accordance with the standard specifications:
In general, test suites should assert at least the following about any implementation of this EIP:
- the nonce is incremented after each transfer
- permit approves the spender on the correct tokenId
- the permit cannot be used after the NFT is transferred
- an expired permit cannot be used
Alternatively, usually is before the approval in the erc20 permit.
What is the rationale behind this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was a mistake on my part, no solid rationale in light of @prateek105 proposed exploit. Updated to _incrementNonce
in an override _transfer
similar to the reference implementation.
@naszam could you please re review this one, should be ready to merge |
* @param spender_ The address of the third party who will execute the transaction involving an owners `NFT`. | ||
* @param tokenId_ The id of the `NFT` being interacted with. | ||
* @param deadline_ The unix timestamp by which the permit must be called. | ||
* @param signature_ The owner's permit signature to verify. | ||
*/ | ||
function permit( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just checking if you've considered to include the overloaded permit
as well, as follow:
function permit(
address spender,
uint256 tokenId,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
permit(spender, tokenId, deadline, abi.encodePacked(r, s, v));
}
It doesn't seem to be mentioned in the standard implementation but I guess would be good to have for integrators.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think it's a small task for integrators to generate signature at their end, i m in favour of not adding this method in contract as it adds one more step for the permit increasing gas cost as well as increases contract size.
function _calculateDomainSeparator(uint256 chainId_) internal view returns (bytes32) { | ||
return | ||
keccak256( | ||
abi.encode( | ||
// keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)') | ||
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f, | ||
_nameHash, | ||
_versionHash, | ||
chainId_, | ||
address(this) | ||
) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to diverge with the standard implementation, where the chainId_
is used in the keccak256
as follows:
function _calculateDomainSeparator(uint256 chainId)
internal
view
returns (bytes32)
{
return
keccak256(
abi.encode(
keccak256(
'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'
),
keccak256(bytes(name())),
keccak256(bytes('1')),
block.chainid,
address(this)
)
);
}
where indeed the chainId_
passed as argument can be used directly instead of block.chainid
, but the hash I guess it should be computed each time as block.chainid
might diverge from deployment, due to forks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per my understanding of this comment, u mean that we should calculate keccak256( 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' )
each time rather than hardcoding it as 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f
But this is hash of a string which is constant across forks, so we can hardcode this instead of calculating it each time.
If u meant something else please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! Right, got confused with the argument, good to resolve, I was just ensuring block.chainid
is passed each time the internal function is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
checked the hash via chisel
, it matches:
➜ bytes32 hash3= keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
➜ hash3
Type: bytes32
└ Data: 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Diffed against Issue 27, related issues and standard reference implementation
LGTM
Description of change
High level
Description of bug or vulnerability and solution
PositionManager
&PermitERC721
Failure to comply with the EIP-4494 code-423n4/2023-05-ajna-findings#141PositionManager.sol#permit()
fail to validate signatures from counterfactual wallets code-423n4/2023-05-ajna-findings#156