Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
33c4d65
Initial forge tests
drinkcoffee Jan 10, 2025
35ba4ac
Added initial test
drinkcoffee Jan 10, 2025
84bad32
Create initial performance tests
drinkcoffee Jan 13, 2025
5c9f3a9
Mint at 15K per block to max out the blocks
drinkcoffee Jan 13, 2025
80bce5d
Added more tests
drinkcoffee Jan 14, 2025
0f14037
Add more tests
drinkcoffee Jan 14, 2025
bf0837f
Revise import order
drinkcoffee Jan 15, 2025
f70f480
Initial compiling version of PsiV2
drinkcoffee Jan 16, 2025
011ccb4
Resolved issues with PsiV2
drinkcoffee Jan 17, 2025
47c0079
Fixed issue with determing existance of an NFT
drinkcoffee Jan 17, 2025
f72216a
Improve documentation. Prefill by 160K
drinkcoffee Jan 20, 2025
dedbc6b
Migrate ERC721 tests to forge
drinkcoffee Jan 29, 2025
5861530
Improve ERC 721 test coverage
drinkcoffee Jan 29, 2025
e48d49c
Added more tests
drinkcoffee Jan 31, 2025
a74fa1b
Add more tests and remove dead code
drinkcoffee Feb 3, 2025
f5c0f86
Added documentation for ERC721PsiV2
drinkcoffee Feb 4, 2025
3f56c1b
Added more tests
drinkcoffee Feb 5, 2025
540e221
Change solidity version
drinkcoffee Feb 6, 2025
c53427a
Merge branch 'main' into peter-solidity-version
drinkcoffee Feb 6, 2025
bea8ee7
Merge branch 'peter-solidity-version' into peter-erc721-perf
drinkcoffee Feb 6, 2025
ce033e3
Add more tests
drinkcoffee Feb 6, 2025
36c41e7
Added transferFrom tests
drinkcoffee Feb 6, 2025
59b6c1a
Added more tests
drinkcoffee Feb 7, 2025
6912c29
Rename files
drinkcoffee Feb 7, 2025
4045cac
Merge branch 'main' into peter-erc721-perf
drinkcoffee Feb 7, 2025
0f31fa1
Add documentation for ERC 721 contracts
drinkcoffee Feb 7, 2025
2be5039
Fix prettier issues
drinkcoffee Feb 9, 2025
c5f3ec5
Fix solidity version and remove dead code
drinkcoffee Feb 10, 2025
0cc0e1b
Add solhint support for PsiV2
drinkcoffee Feb 10, 2025
e7e359c
Fixed last Slither warning
drinkcoffee Feb 10, 2025
df57fa4
Remove temporary file
drinkcoffee Feb 10, 2025
a6f485c
Remove dead code
drinkcoffee Feb 10, 2025
78bf98b
Improve test coverage
drinkcoffee Feb 10, 2025
af2bca0
Resolve solhint issues
drinkcoffee Feb 10, 2025
45ea3cd
Merge from peter-erc721-perf
drinkcoffee Feb 25, 2025
b7684d7
Remove mention of invariant tests
drinkcoffee Feb 25, 2025
2606a37
Fixed copyright notices
drinkcoffee Feb 27, 2025
34965c8
Added Mermaid diagram source code
drinkcoffee Feb 28, 2025
3bcd1a1
Fixed up incorrect documentation
drinkcoffee Feb 28, 2025
e321e7a
Add audit report
drinkcoffee Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Revise import order
  • Loading branch information
drinkcoffee committed Jan 15, 2025
commit bf0837f7962678987e4d24e3e7041ddf15156a7d
30 changes: 30 additions & 0 deletions contracts/access/IMintingAccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

import {IAccessControlEnumerable} from "@openzeppelin/contracts/access/IAccessControlEnumerable.sol";


interface IMintingAccessControl is IAccessControlEnumerable {
/**
* @notice Role to mint tokens
*/
function MINTER_ROLE() external returns (bytes32);

/**
* @notice Allows admin grant `user` `MINTER` role
* @param user The address to grant the `MINTER` role to
*/
function grantMinterRole(address user) external;

/**
* @notice Allows admin to revoke `MINTER_ROLE` role from `user`
* @param user The address to revoke the `MINTER` role from
*/
function revokeMinterRole(address user) external;

/**
* @notice Returns the addresses which have DEFAULT_ADMIN_ROLE
*/
function getAdmins() external view returns (address[] memory);
}
173 changes: 173 additions & 0 deletions contracts/token/erc721/abstract/ERC721HybridPermitV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright Immutable Pty Ltd 2018 - 2025
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {IERC1271} from "@openzeppelin/contracts/interfaces/IERC1271.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {BytesLib} from "solidity-bytes-utils/contracts/BytesLib.sol";
import {IERC4494} from "./IERC4494.sol";
import {ERC721HybridV2} from "./ERC721HybridV2.sol";

/**
* @title ERC721HybridPermit: An extension of the ERC721Hybrid NFT standard that supports off-chain approval via permits.
* @dev This contract implements ERC-4494 as well, allowing tokens to be approved via off-chain signed messages.
*/
abstract contract ERC721HybridPermitV2 is ERC721HybridV2, IERC4494, EIP712 {
/**
* @notice mapping used to keep track of nonces of each token ID for validating
* signatures
*/
mapping(uint256 tokenId => uint256 nonce) private _nonces;

/**
* @dev the unique identifier for the permit struct to be EIP 712 compliant
*/
bytes32 private constant _PERMIT_TYPEHASH =
keccak256(
abi.encodePacked(
"Permit(",
"address spender,"
"uint256 tokenId,"
"uint256 nonce,"
"uint256 deadline"
")"
)
);

constructor(string memory name, string memory symbol) ERC721HybridV2(name, symbol) EIP712(name, "1") {}

/**
* @notice Function to approve by way of owner signature
* @param spender the address to approve
* @param tokenId the index of the NFT to approve the spender on
* @param deadline a timestamp expiry for the permit
* @param sig a traditional or EIP-2098 signature
*/
function permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) external override {
_permit(spender, tokenId, deadline, sig);
}

/**
* @notice Returns the current nonce of a given token ID.
* @param tokenId The ID of the token for which to retrieve the nonce.
* @return Current nonce of the given token.
*/
function nonces(uint256 tokenId) external view returns (uint256) {
return _nonces[tokenId];
}

/**
* @notice Returns the domain separator used in the encoding of the signature for permits, as defined by EIP-712
* @return the bytes32 domain separator
*/
// solhint-disable-next-line func-name-mixedcase
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
}

/**
* @notice Overrides supportsInterface from IERC165 and ERC721Hybrid to add support for IERC4494.
* @param interfaceId The interface identifier, which is a 4-byte selector.
* @return True if the contract implements `interfaceId` and the call doesn't revert, otherwise false.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC721HybridV2) returns (bool) {
return
interfaceId == type(IERC4494).interfaceId || // 0x5604e225
super.supportsInterface(interfaceId);
}

/**
* @notice Overrides the _transfer method from ERC721Hybrid to increment the nonce after a successful transfer.
* @param from The address from which the token is being transferred.
* @param to The address to which the token is being transferred.
* @param tokenId The ID of the token being transferred.
*/
function _transfer(address from, address to, uint256 tokenId) internal virtual override(ERC721HybridV2) {
_nonces[tokenId]++;
super._transfer(from, to, tokenId);
}

function _permit(address spender, uint256 tokenId, uint256 deadline, bytes memory sig) internal virtual {
// solhint-disable-next-line not-rely-on-time
if (deadline < block.timestamp) {
revert PermitExpired();
}

bytes32 digest = _buildPermitDigest(spender, tokenId, deadline);

// smart contract wallet signature validation
if (_isValidERC1271Signature(ownerOf(tokenId), digest, sig)) {
_approve(spender, tokenId);
return;
}

address recoveredSigner = address(0);

// EOA signature validation
if (sig.length == 64) {
// ERC2098 Sig
recoveredSigner = ECDSA.recover(
digest,
bytes32(BytesLib.slice(sig, 0, 32)),
bytes32(BytesLib.slice(sig, 32, 64))
);
} else if (sig.length == 65) {
// typical EDCSA Sig
recoveredSigner = ECDSA.recover(digest, sig);
} else {
revert InvalidSignature();
}

if (_isValidEOASignature(recoveredSigner, tokenId)) {
_approve(spender, tokenId);
} else {
revert InvalidSignature();
}
}

/**
* @notice Builds the EIP-712 compliant digest for the permit.
* @param spender The address which is approved to spend the token.
* @param tokenId The ID of the token for which the permit is being generated.
* @param deadline The deadline until which the permit is valid.
* @return A bytes32 digest, EIP-712 compliant, that serves as a unique identifier for the permit.
*/
function _buildPermitDigest(address spender, uint256 tokenId, uint256 deadline) internal view returns (bytes32) {
return _hashTypedDataV4(keccak256(abi.encode(_PERMIT_TYPEHASH, spender, tokenId, _nonces[tokenId], deadline)));
}

/**
* @notice Checks if a given signature is valid according to EIP-1271.
* @param recoveredSigner The address which purports to have signed the message.
* @param tokenId The token id.
* @return True if the signature is from an approved operator or owner, otherwise false.
*/
function _isValidEOASignature(address recoveredSigner, uint256 tokenId) private view returns (bool) {
return recoveredSigner != address(0) && _isApprovedOrOwner(recoveredSigner, tokenId);
}

/**
* @notice Checks if a given signature is valid according to EIP-1271.
* @param spender The address which purports to have signed the message.
* @param digest The EIP-712 compliant digest that was signed.
* @param sig The actual signature bytes.
* @return True if the signature is valid according to EIP-1271, otherwise false.
*/
function _isValidERC1271Signature(address spender, bytes32 digest, bytes memory sig) private view returns (bool) {
// slither-disable-next-line low-level-calls
(bool success, bytes memory res) = spender.staticcall(
abi.encodeWithSelector(IERC1271.isValidSignature.selector, digest, sig)
);

if (success && res.length == 32) {
bytes4 decodedRes = abi.decode(res, (bytes4));
if (decodedRes == IERC1271.isValidSignature.selector) {
return true;
}
}

return false;
}
}
Loading