Skip to content
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

Merged
merged 15 commits into from
May 31, 2023
Merged

Conversation

@MikeHathaway MikeHathaway changed the base branch from master to develop May 15, 2023 22:18
@MikeHathaway MikeHathaway marked this pull request as ready for review May 15, 2023 22:59
@@ -2,124 +2,264 @@

Copy link
Collaborator Author

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?

Copy link
Contributor

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_),
Copy link
Contributor

@prateek105 prateek105 May 16, 2023

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

Copy link
Contributor

@prateek105 prateek105 May 19, 2023

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

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.

Copy link
Contributor

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.

@grandizzy grandizzy changed the title Code Arena: PermitERC721 EIP-4494 compliance Codearena-145,147: PermitERC721 EIP-4494 compliance May 17, 2023
Copy link
Contributor

@prateek105 prateek105 left a 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 Show resolved Hide resolved
src/base/PermitERC721.sol Outdated Show resolved Hide resolved
@grandizzy grandizzy requested a review from naszam May 23, 2023 09:35
Comment on lines 98 to 108

// 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);
Copy link
Contributor

@naszam naszam May 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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_));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@prateek105 prateek105 May 26, 2023

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.

Copy link
Contributor

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@naszam naszam May 30, 2023

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).

Comment on lines +166 to +167
(address recoveredAddress, ) = ECDSA.tryRecover(digest, signature_);
if (!_checkSignature(digest, signature_, recoveredAddress, tokenId_)) revert NotAuthorized();
Copy link
Contributor

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:

Copy link
Collaborator Author

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.

Copy link
Contributor

@prateek105 prateek105 May 26, 2023

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

Copy link
Contributor

@naszam naszam May 29, 2023

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).

@@ -2,124 +2,264 @@

Copy link
Contributor

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.

"\x19\x01",
DOMAIN_SEPARATOR(),
// increment the permit nonce of the given tokenId
_incrementNonce(tokenId_);
Copy link
Contributor

@naszam naszam May 30, 2023

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?

Copy link
Collaborator Author

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.

@grandizzy grandizzy requested a review from naszam May 31, 2023 11:48
@grandizzy
Copy link
Contributor

@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(
Copy link
Contributor

@naszam naszam May 31, 2023

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.

Copy link
Contributor

@prateek105 prateek105 May 31, 2023

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.

Comment on lines +206 to +218
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)
)
);
}
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

@naszam naszam May 31, 2023

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.

Copy link
Contributor

@naszam naszam May 31, 2023

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

@MikeHathaway MikeHathaway merged commit b36bf64 into develop May 31, 2023
@MikeHathaway MikeHathaway deleted the ca-permit branch May 31, 2023 16:08
Copy link
Contributor

@naszam naszam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants