diff --git a/gasbenchmark10mil b/gasbenchmark10mil index 1d0516f..33d1c81 100644 --- a/gasbenchmark10mil +++ b/gasbenchmark10mil @@ -1,24 +1,24 @@ No files changed, compilation skipped Running 1 test for test/GasBenchmark.t.sol:GasBenchmark -[PASS] testGas(address,bytes32) (runs: 256, μ: 14540532, ~: 14540813) -Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 289.94ms +[PASS] testGas(address,bytes32) (runs: 256, μ: 12592434, ~: 12592620) +Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 405.76ms | src/DelegateRegistry.sol:DelegateRegistry contract | | | | | | |----------------------------------------------------|-----------------|--------|--------|--------|---------| | Deployment Cost | Deployment Size | | | | | -| 2124658 | 10644 | | | | | +| 1800698 | 9026 | | | | | | Function Name | min | avg | median | max | # calls | -| checkDelegateForAll | 2980 | 3183 | 3183 | 3386 | 2 | -| checkDelegateForContract | 5473 | 5895 | 5895 | 6317 | 2 | -| checkDelegateForERC1155 | 7990 | 8685 | 8685 | 9380 | 2 | -| checkDelegateForERC20 | 7927 | 8610 | 8610 | 9293 | 2 | -| checkDelegateForERC721 | 7991 | 8644 | 8644 | 9297 | 2 | -| delegateAll | 135861 | 135861 | 135861 | 135861 | 2 | -| delegateContract | 114469 | 125419 | 125419 | 136369 | 2 | -| delegateERC1155 | 159393 | 170343 | 170343 | 181293 | 2 | -| delegateERC20 | 136942 | 147892 | 147892 | 158842 | 2 | -| delegateERC721 | 136936 | 147886 | 147886 | 158836 | 2 | -| multicall | 689696 | 689696 | 689696 | 689696 | 1 | +| checkDelegateForAll | 2847 | 3016 | 3016 | 3186 | 2 | +| checkDelegateForContract | 5276 | 5627 | 5627 | 5979 | 2 | +| checkDelegateForERC1155 | 7707 | 8280 | 8280 | 8854 | 2 | +| checkDelegateForERC20 | 7646 | 8211 | 8211 | 8777 | 2 | +| checkDelegateForERC721 | 7736 | 8283 | 8283 | 8830 | 2 | +| delegateAll | 135880 | 135880 | 135880 | 135880 | 2 | +| delegateContract | 114464 | 125414 | 125414 | 136364 | 2 | +| delegateERC1155 | 159363 | 170313 | 170313 | 181263 | 2 | +| delegateERC20 | 136903 | 147853 | 147853 | 158803 | 2 | +| delegateERC721 | 136903 | 147853 | 147853 | 158803 | 2 | +| multicall | 689608 | 689608 | 689608 | 689608 | 1 | diff --git a/hashbenchmark10mil b/hashbenchmark10mil index da6c65b..4f9a17b 100644 --- a/hashbenchmark10mil +++ b/hashbenchmark10mil @@ -1,24 +1,24 @@ No files changed, compilation skipped Running 1 test for test/HashBenchmark.t.sol:HashBenchmark -[PASS] testHashGas(address,bytes32,address,uint256,address,bytes32) (runs: 256, μ: 20187, ~: 20187) -Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 26.78ms +[PASS] testHashGas(address,bytes32,address,uint256,address,bytes32) (runs: 256, μ: 19737, ~: 19737) +Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 44.46ms | test/HashBenchmark.t.sol:HashHarness contract | | | | | | |-----------------------------------------------|-----------------|-----|--------|-----|---------| | Deployment Cost | Deployment Size | | | | | -| 396030 | 2010 | | | | | +| 275918 | 1410 | | | | | | Function Name | min | avg | median | max | # calls | -| allHash | 679 | 679 | 679 | 679 | 1 | -| allLocation | 724 | 724 | 724 | 724 | 1 | -| contractHash | 813 | 813 | 813 | 813 | 1 | -| contractLocation | 851 | 851 | 851 | 851 | 1 | +| allHash | 666 | 666 | 666 | 666 | 1 | +| allLocation | 694 | 694 | 694 | 694 | 1 | +| contractHash | 759 | 759 | 759 | 759 | 1 | +| contractLocation | 797 | 797 | 797 | 797 | 1 | | decodeType | 371 | 371 | 371 | 371 | 1 | -| erc1155Hash | 821 | 821 | 821 | 821 | 1 | -| erc1155Location | 920 | 920 | 920 | 920 | 1 | -| erc20Hash | 792 | 792 | 792 | 792 | 1 | -| erc20Location | 831 | 831 | 831 | 831 | 1 | -| erc721Hash | 866 | 866 | 866 | 866 | 1 | -| erc721Location | 888 | 888 | 888 | 888 | 1 | +| erc1155Hash | 776 | 776 | 776 | 776 | 1 | +| erc1155Location | 875 | 875 | 875 | 875 | 1 | +| erc20Hash | 738 | 738 | 738 | 738 | 1 | +| erc20Location | 766 | 766 | 766 | 766 | 1 | +| erc721Hash | 821 | 821 | 821 | 821 | 1 | +| erc721Location | 843 | 843 | 843 | 843 | 1 | | location | 384 | 384 | 384 | 384 | 1 | diff --git a/src/DelegateRegistry.sol b/src/DelegateRegistry.sol index 95f1564..a9bcc11 100644 --- a/src/DelegateRegistry.sol +++ b/src/DelegateRegistry.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.21; import {IDelegateRegistry as IDelegateRegistry} from "./IDelegateRegistry.sol"; import {RegistryHashes as Hashes} from "./libraries/RegistryHashes.sol"; import {RegistryStorage as Storage} from "./libraries/RegistryStorage.sol"; +import {RegistryOps as Ops} from "./libraries/RegistryOps.sol"; /** * @title DelegateRegistry @@ -133,25 +134,37 @@ contract DelegateRegistry is IDelegateRegistry { /// @inheritdoc IDelegateRegistry function checkDelegateForAll(address to, address from, bytes32 rights) external view override returns (bool valid) { valid = _validateDelegation(Hashes.allLocation(from, "", to), from); - if (rights != "" && !valid) valid = _validateDelegation(Hashes.allLocation(from, rights, to), from); + if (!Ops.or(rights == "", valid)) valid = _validateDelegation(Hashes.allLocation(from, rights, to), from); + assembly ("memory-safe") { + mstore(0x00, iszero(iszero(valid))) + return(0x00, 0x20) // Direct return. Skips Solidity's redundant copying to save gas. + } } /// @inheritdoc IDelegateRegistry function checkDelegateForContract(address to, address from, address contract_, bytes32 rights) external view override returns (bool valid) { valid = _validateDelegation(Hashes.allLocation(from, "", to), from) || _validateDelegation(Hashes.contractLocation(from, "", to, contract_), from); - if (rights != "" && !valid) { + if (!Ops.or(rights == "", valid)) { valid = _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from); } + assembly ("memory-safe") { + mstore(0x00, iszero(iszero(valid))) + return(0x00, 0x20) // Direct return. Skips Solidity's redundant copying to save gas. + } } /// @inheritdoc IDelegateRegistry function checkDelegateForERC721(address to, address from, address contract_, uint256 tokenId, bytes32 rights) external view override returns (bool valid) { valid = _validateDelegation(Hashes.allLocation(from, "", to), from) || _validateDelegation(Hashes.contractLocation(from, "", to, contract_), from) || _validateDelegation(Hashes.erc721Location(from, "", to, tokenId, contract_), from); - if (rights != "" && !valid) { + if (!Ops.or(rights == "", valid)) { valid = _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from) || _validateDelegation(Hashes.erc721Location(from, rights, to, tokenId, contract_), from); } + assembly ("memory-safe") { + mstore(0x00, iszero(iszero(valid))) + return(0x00, 0x20) // Direct return. Skips Solidity's redundant copying to save gas. + } } /// @inheritdoc IDelegateRegistry @@ -160,12 +173,16 @@ contract DelegateRegistry is IDelegateRegistry { amount = (_validateDelegation(Hashes.allLocation(from, "", to), from) || _validateDelegation(Hashes.contractLocation(from, "", to, contract_), from)) ? type(uint256).max : (_validateDelegation(location, from) ? _loadDelegationUint(location, Storage.Positions.amount) : 0); - if (rights != "" && amount != type(uint256).max) { + if (!Ops.or(rights == "", amount == type(uint256).max)) { location = Hashes.erc20Location(from, rights, to, contract_); uint256 rightsBalance = ( _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from) ) ? type(uint256).max : (_validateDelegation(location, from) ? _loadDelegationUint(location, Storage.Positions.amount) : 0); - amount = rightsBalance > amount ? rightsBalance : amount; + amount = Ops.max(rightsBalance, amount); + } + assembly ("memory-safe") { + mstore(0x00, amount) + return(0x00, 0x20) // Direct return. Skips Solidity's redundant copying to save gas. } } @@ -175,12 +192,16 @@ contract DelegateRegistry is IDelegateRegistry { amount = (_validateDelegation(Hashes.allLocation(from, "", to), from) || _validateDelegation(Hashes.contractLocation(from, "", to, contract_), from)) ? type(uint256).max : (_validateDelegation(location, from) ? _loadDelegationUint(location, Storage.Positions.amount) : 0); - if (rights != "" && amount != type(uint256).max) { + if (!Ops.or(rights == "", amount == type(uint256).max)) { location = Hashes.erc1155Location(from, rights, to, tokenId, contract_); uint256 rightsBalance = ( _validateDelegation(Hashes.allLocation(from, rights, to), from) || _validateDelegation(Hashes.contractLocation(from, rights, to, contract_), from) ) ? type(uint256).max : (_validateDelegation(location, from) ? _loadDelegationUint(location, Storage.Positions.amount) : 0); - amount = rightsBalance > amount ? rightsBalance : amount; + amount = Ops.max(rightsBalance, amount); + } + assembly ("memory-safe") { + mstore(0x00, amount) + return(0x00, 0x20) // Direct return. Skips Solidity's redundant copying to save gas. } } @@ -215,7 +236,7 @@ contract DelegateRegistry is IDelegateRegistry { for (uint256 i = 0; i < hashes.length; ++i) { bytes32 location = Hashes.location(hashes[i]); address from = _loadFrom(location, Storage.Positions.firstPacked); - if (from == DELEGATION_EMPTY || from == DELEGATION_REVOKED) { + if (Ops.or(from == DELEGATION_EMPTY, from == DELEGATION_REVOKED)) { delegations_[i] = Delegation({type_: DelegationType.NONE, to: address(0), from: address(0), rights: "", amount: 0, contract_: address(0), tokenId: 0}); } else { (, address to, address contract_) = _loadDelegationAddresses(location, Storage.Positions.firstPacked, Storage.Positions.secondPacked); @@ -259,7 +280,7 @@ contract DelegateRegistry is IDelegateRegistry { /// @param interfaceId The interface identifier /// @return valid Whether the queried interface is supported function supportsInterface(bytes4 interfaceId) external pure returns (bool) { - return interfaceId == type(IDelegateRegistry).interfaceId || interfaceId == 0x01ffc9a7; + return Ops.or(interfaceId == type(IDelegateRegistry).interfaceId, interfaceId == 0x01ffc9a7); } /** @@ -384,7 +405,12 @@ contract DelegateRegistry is IDelegateRegistry { } /// @dev Helper function to establish whether a delegation is enabled - function _validateDelegation(bytes32 location, address from) internal view returns (bool) { - return (_loadFrom(location, Storage.Positions.firstPacked) == from && from > DELEGATION_REVOKED); + function _validateDelegation(bytes32 location, address from) internal view returns (bool result) { + uint256 loaded = uint256(uint160(_loadFrom(location, Storage.Positions.firstPacked))); + uint256 revoked = uint256(uint160(DELEGATION_REVOKED)); + uint256 fromCasted = uint256(uint160(from)); + assembly { + result := and(eq(fromCasted, loaded), gt(fromCasted, revoked)) + } } } diff --git a/src/libraries/RegistryHashes.sol b/src/libraries/RegistryHashes.sol index 382da34..a16fb27 100644 --- a/src/libraries/RegistryHashes.sol +++ b/src/libraries/RegistryHashes.sol @@ -8,22 +8,18 @@ import {IDelegateRegistry} from "../IDelegateRegistry.sol"; * * The encoding for the 5 types of delegate registry hashes should be as follows * - * ALL: keccak256(abi.encode(from, rights, to)) - * CONTRACT: keccak256(abi.encode(from, rights, to, contract_)) - * ERC721: keccak256(abi.encode(from, rights, to, tokenId, contract_)) - * ERC20: keccak256(abi.encode(from, rights, to, contract_)) - * ERC1155: keccak256(abi.encode(from, rights, to, tokenId, contract_)) + * ALL: keccak256(abi.encodePacked(rights, from, to)) + * CONTRACT: keccak256(abi.encodePacked(rights, from, to, contract_)) + * ERC721: keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)) + * ERC20: keccak256(abi.encodePacked(rights, from, to, contract_)) + * ERC1155: keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)) * * To avoid collisions between the hashes with respect to type, the last byte of the hash is encoded with a unique number representing the type of delegation. * */ library RegistryHashes { - /// @dev Used to delete the last byte of a 32 byte word with and(word, deleteLastByte) - uint256 internal constant DELETE_LAST_BYTE = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00; - /// @dev Used to clean address types of dirty bits with and(address, CLEAN_ADDRESS) - uint256 internal constant CLEAN_ADDRESS = 0x00ffffffffffffffffffffffffffffffffffffffff; /// @dev Used to delete everything but the last byte of a 32 byte word with and(word, EXTRACT_LAST_BYTE) - uint256 internal constant EXTRACT_LAST_BYTE = 0xFF; + uint256 internal constant EXTRACT_LAST_BYTE = 0xff; /// @dev uint256 constant for the delegate registry delegation type enumeration, related unit test should fail if these mismatch uint256 internal constant ALL_TYPE = 1; uint256 internal constant CONTRACT_TYPE = 2; @@ -68,17 +64,19 @@ library RegistryHashes { * @param rights it the rights specified by the delegation * @param to is the address receiving the delegation * @return hash of the delegation parameters encoded with ALL_TYPE - * @dev returned hash should be equivalent to keccak256(abi.encode(from, rights, to)) with the last byte overwritten with ALL_TYPE + * @dev returned hash should be equivalent to keccak256(abi.encodePacked(rights, from, to)) with the last byte overwritten with ALL_TYPE * @dev will not revert if from or to are > uint160, any input larger than uint160 for from and to will be cleaned to their last 20 bytes */ function allHash(address from, bytes32 rights, address to) internal pure returns (bytes32 hash) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - hash := or(and(keccak256(ptr, 96), DELETE_LAST_BYTE), ALL_TYPE) // Runs keccak256 on the 96 bytes at ptr, and then encodes the last byte of that hash with - // ALL_TYPE + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(40, to) + mstore(20, from) + mstore(0, rights) + hash := or(shl(8, keccak256(0, 72)), ALL_TYPE) + // Restore the upper bits of the free memory pointer, which is zero. + mstore(40, 0) } } @@ -88,18 +86,22 @@ library RegistryHashes { * @param rights is the rights specified by the delegation * @param to is the address receiving the delegation * @return computedLocation is the storage location of the all delegation with those parameters in the delegations mapping - * @dev gives the same location hash as location(allHash(from, rights, to)) would + * @dev gives the same location hash as location(allHash(rights, from, to)) would * @dev will not revert if from or to are > uint160, any input larger than uint160 for from and to will be cleaned to their last 20 bytes */ function allLocation(address from, bytes32 rights, address to) internal pure returns (bytes32 computedLocation) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(ptr, or(and(keccak256(ptr, 96), DELETE_LAST_BYTE), ALL_TYPE)) // Store allHash at ptr location - mstore(add(ptr, 32), DELEGATION_SLOT) // Store delegationSlot after allHash at ptr location - computedLocation := keccak256(ptr, 64) // Runs keccak256 on the 64 bytes at ptr to obtain the location + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(40, to) + mstore(20, from) + mstore(0, rights) + mstore(8, or(shl(8, keccak256(0, 72)), ALL_TYPE)) + mstore(40, DELEGATION_SLOT) + computedLocation := keccak256(8, 64) + // Restore the upper bits of the free memory pointer, which is zero. + // Should be optimized away if `DELEGATION_SLOT` is zero. + mstore(40, 0) } } @@ -110,18 +112,20 @@ library RegistryHashes { * @param to is the address receiving the delegation * @param contract_ is the address of the contract specified by the delegation * @return hash of the delegation parameters encoded with CONTRACT_TYPE - * @dev returned hash should be equivalent to keccak256(abi.encode(from, rights, to, contract_)) with the last byte overwritten with CONTRACT_TYPE + * @dev returned hash should be equivalent to keccak256(abi.encodePacked(rights, from, to, contract_)) with the last byte overwritten with CONTRACT_TYPE * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function contractHash(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32 hash) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - hash := or(and(keccak256(ptr, 128), DELETE_LAST_BYTE), CONTRACT_TYPE) // Run keccak256 on the 128 bytes at ptr, and then encodes the last byte of that hash - // with CONTRACT_TYPE + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + hash := or(shl(8, keccak256(0, 92)), CONTRACT_TYPE) + // Restore the upper bits of the free memory pointer, which is zero. + mstore(60, 0) } } @@ -132,19 +136,23 @@ library RegistryHashes { * @param to is the address receiving the delegation * @param contract_ is the address of the contract specified by the delegation * @return computedLocation is the storage location of the contract delegation with those parameters in the delegations mapping - * @dev gives the same location hash as location(contractHash(from, rights, to, contract_)) would + * @dev gives the same location hash as location(contractHash(rights, from, to, contract_)) would * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function contractLocation(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32 computedLocation) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - mstore(ptr, or(and(keccak256(ptr, 128), DELETE_LAST_BYTE), CONTRACT_TYPE)) // Store contractHash - mstore(add(ptr, 32), DELEGATION_SLOT) // Store delegationSlot after hash - computedLocation := keccak256(ptr, 64) // Run keccak256 on the 64 bytes at ptr to obtain the location + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + mstore(28, or(shl(8, keccak256(0, 92)), CONTRACT_TYPE)) + mstore(60, DELEGATION_SLOT) + computedLocation := keccak256(28, 64) + // Restore the upper bits of the free memory pointer, which is zero. + // Should be optimized away if `DELEGATION_SLOT` is zero. + mstore(60, 0) } } @@ -156,19 +164,22 @@ library RegistryHashes { * @param tokenId is the id of the token specified by the delegation * @param contract_ is the address of the contract specified by the delegation * @return hash of the parameters encoded with ERC721_TYPE - * @dev returned hash should be equivalent to keccak256(abi.encode(from, rights, to, tokenId, contract_)) with the last byte overwritten with ERC721_TYPE + * @dev returned hash should be equivalent to keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)) with the last byte overwritten with ERC721_TYPE * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc721Hash(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32 hash) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), tokenId) // Stores tokenId - mstore(add(ptr, 128), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - hash := or(and(keccak256(ptr, 160), DELETE_LAST_BYTE), ERC721_TYPE) // Run keccak256 on the 160 bytes at ptr, and then encodes the last byte of that hash with - // ERC721_TYPE + assembly { + let m := mload(64) // Cache the free memory pointer. + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(92, tokenId) + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + hash := or(shl(8, keccak256(0, 124)), ERC721_TYPE) + mstore(64, m) // Restore the free memory pointer. + mstore(96, 0) // Restore the zero pointer. } } @@ -180,20 +191,24 @@ library RegistryHashes { * @param tokenId is the id of the erc721 token * @param contract_ is the address of the erc721 token contract * @return computedLocation is the storage location of the erc721 delegation with those parameters in the delegations mapping - * @dev gives the same location hash as location(erc721Hash(from, rights, to, tokenId, contract_)) would + * @dev gives the same location hash as location(erc721Hash(rights, from, to, contract_, tokenId)) would * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc721Location(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32 computedLocation) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), tokenId) // Stores tokenId - mstore(add(ptr, 128), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - mstore(ptr, or(and(keccak256(ptr, 160), DELETE_LAST_BYTE), ERC721_TYPE)) // Store erc721Hash - mstore(add(ptr, 32), DELEGATION_SLOT) // Stores delegationSlot - computedLocation := keccak256(ptr, 64) + assembly { + let m := mload(64) // Cache the free memory pointer. + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(92, tokenId) + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + mstore(64, or(shl(8, keccak256(0, 124)), ERC721_TYPE)) + mstore(96, DELEGATION_SLOT) + computedLocation := keccak256(64, 64) + mstore(64, m) // Restore the free memory pointer. + mstore(96, 0) // Restore the zero pointer. Should be optimized away if `DELEGATION_SLOT` is zero. } } @@ -204,17 +219,20 @@ library RegistryHashes { * @param to is the address receiving the delegation * @param contract_ is the address of the erc20 token contract * @return hash of the parameters encoded with ERC20_TYPE - * @dev returned hash should be equivalent to keccak256(abi.encode(from, rights, to, contract_)) with the last byte overwritten with ERC20_TYPE + * @dev returned hash should be equivalent to keccak256(abi.encodePacked(rights, from, to, contract_)) with the last byte overwritten with ERC20_TYPE * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc20Hash(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32 hash) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Stores rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), and(contract_, CLEAN_ADDRESS)) // Cleans and stores contract_ - hash := or(and(keccak256(ptr, 128), DELETE_LAST_BYTE), ERC20_TYPE) // Runs keccak256 on 128 bytes at ptr and encodes that hash with ERC20_TYPE + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + hash := or(shl(8, keccak256(0, 92)), ERC20_TYPE) + // Restore the upper bits of the free memory pointer, which is zero. + mstore(60, 0) } } @@ -225,19 +243,23 @@ library RegistryHashes { * @param to is the address receiving the delegation * @param contract_ is the address of the erc20 token contract * @return computedLocation is the storage location of the erc20 delegation with those parameters in the delegations mapping - * @dev gives the same location hash as location(erc20Hash(from, rights, to, contract_)) would + * @dev gives the same location hash as location(erc20Hash(rights, from, to, contract_)) would * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc20Location(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32 computedLocation) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - mstore(ptr, or(and(keccak256(ptr, 128), DELETE_LAST_BYTE), ERC20_TYPE)) // Store erc20Hash - mstore(add(ptr, 32), DELEGATION_SLOT) // Store delegationSlot - computedLocation := keccak256(ptr, 64) // Runs keccak256 on the 64 bytes at ptr to get the location + assembly { + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + mstore(28, or(shl(8, keccak256(0, 92)), ERC20_TYPE)) + mstore(60, DELEGATION_SLOT) + computedLocation := keccak256(28, 64) + // Restore the upper bits of the free memory pointer, which is zero. + // Should be optimized away if `DELEGATION_SLOT` is zero. + mstore(60, 0) } } @@ -249,19 +271,22 @@ library RegistryHashes { * @param tokenId is the id of the erc1155 token * @param contract_ is the address of the erc1155 token contract * @return hash of the parameters encoded with ERC1155_TYPE - * @dev returned hash should be equivalent to keccak256(abi.encode(from, rights, to, tokenId, contract_)) with the last byte overwritten with ERC1155_TYPE + * @dev returned hash should be equivalent to keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)) with the last byte overwritten with ERC1155_TYPE * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc1155Hash(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32 hash) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), tokenId) // Stores tokenId - mstore(add(ptr, 128), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - hash := or(and(keccak256(ptr, 160), DELETE_LAST_BYTE), ERC1155_TYPE) // Runs keccak256 on 160 bytes at ptr and encodes the last byte of that hash with - // ERC1155_TYPE + assembly { + let m := mload(64) // Cache the free memory pointer. + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(92, tokenId) + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + hash := or(shl(8, keccak256(0, 124)), ERC1155_TYPE) + mstore(64, m) // Restore the free memory pointer. + mstore(96, 0) // Restore the zero pointer. Should be optimized away if `DELEGATION_SLOT` is zero. } } @@ -273,20 +298,24 @@ library RegistryHashes { * @param tokenId is the id of the erc1155 token * @param contract_ is the address of the erc1155 token contract * @return computedLocation is the storage location of the erc1155 delegation with those parameters in the delegations mapping - * @dev gives the same location hash as location(erc1155Hash(from, rights, to, tokenId, contract_)) would + * @dev gives the same location hash as location(erc1155Hash(rights, from, to, contract_, tokenId)) would * @dev will not revert if from, to, or contract_ are > uint160, any input larger than uint160 for from, to, or contract_ will be cleaned to their last 20 bytes */ function erc1155Location(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32 computedLocation) { - assembly ("memory-safe") { - let ptr := mload(0x40) // Set ptr to the free memory location - mstore(ptr, and(from, CLEAN_ADDRESS)) // Cleans and stores from - mstore(add(ptr, 32), rights) // Store rights - mstore(add(ptr, 64), and(to, CLEAN_ADDRESS)) // Cleans and stores to - mstore(add(ptr, 96), tokenId) // Stores tokenId - mstore(add(ptr, 128), and(contract_, CLEAN_ADDRESS)) // Cleans and store contract_ - mstore(ptr, or(and(keccak256(ptr, 160), DELETE_LAST_BYTE), ERC1155_TYPE)) // Stores erc1155Hash - mstore(add(ptr, 32), DELEGATION_SLOT) // Store delegationSlot - computedLocation := keccak256(ptr, 64) // Runs keccak256 on 64 bytes at ptr to obtain the location + assembly { + let m := mload(64) // Cache the free memory pointer. + // Layout the variables from last to first, + // agnostic to upper 96 bits of address words. + mstore(92, tokenId) + mstore(60, contract_) + mstore(40, to) + mstore(20, from) + mstore(0, rights) + mstore(64, or(shl(8, keccak256(0, 124)), ERC1155_TYPE)) + mstore(96, DELEGATION_SLOT) + computedLocation := keccak256(64, 64) + mstore(64, m) // Restore the free memory pointer. + mstore(96, 0) // Restore the zero pointer. Should be optimized away if `DELEGATION_SLOT` is zero. } } } diff --git a/src/libraries/RegistryOps.sol b/src/libraries/RegistryOps.sol new file mode 100644 index 0000000..c522ab4 --- /dev/null +++ b/src/libraries/RegistryOps.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +library RegistryOps { + /// @dev `x > y ? x : y`. + function max(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + // `gt(y, x)` will evaluate to 1 if `y > x`, else 0. + // + // If `y > x`: + // `x ^ ((x ^ y) * 1) = x ^ (x ^ y) = (x ^ x) ^ y = 0 ^ y = y`. + // otherwise: + // `x ^ ((x ^ y) * 0) = x ^ 0 = x`. + z := xor(x, mul(xor(x, y), gt(y, x))) + } + } + + /// @dev `x & y`. + function and(bool x, bool y) internal pure returns (bool z) { + assembly { + // Any non-zero 256 bit word is true, else false. + z := and(iszero(iszero(x)), iszero(iszero(y))) + } + } + + /// @dev `x | y`. + function or(bool x, bool y) internal pure returns (bool z) { + assembly { + // Any non-zero 256 bit word is true, else false. + z := or(iszero(iszero(x)), iszero(iszero(y))) + } + } +} diff --git a/src/libraries/RegistryStorage.sol b/src/libraries/RegistryStorage.sol index aabd38e..3508b41 100644 --- a/src/libraries/RegistryStorage.sol +++ b/src/libraries/RegistryStorage.sol @@ -35,7 +35,7 @@ library RegistryStorage { function packAddresses(address from, address to, address contract_) internal pure returns (bytes32 firstPacked, bytes32 secondPacked) { assembly { firstPacked := or(shl(64, and(contract_, CLEAN_FIRST8_BYTES_ADDRESS)), and(from, CLEAN_ADDRESS)) - secondPacked := or(shl(160, and(contract_, CLEAN_LAST12_BYTES_ADDRESS)), and(to, CLEAN_ADDRESS)) + secondPacked := or(shl(160, contract_), and(to, CLEAN_ADDRESS)) } } diff --git a/test/RegistryHashTests.t.sol b/test/RegistryHashTests.t.sol index 4f83f42..baa0d6e 100644 --- a/test/RegistryHashTests.t.sol +++ b/test/RegistryHashTests.t.sol @@ -9,8 +9,6 @@ import {RegistryHashes as Hashes} from "src/libraries/RegistryHashes.sol"; contract RegistryHashTests is Test { /// @dev used to cross check internal constant in registry hashes with intended values function testRegistryHashConstant() public { - assertEq(Hashes.DELETE_LAST_BYTE, type(uint256).max << 8); - assertEq(Hashes.CLEAN_ADDRESS, uint256(type(uint160).max)); assertEq(Hashes.EXTRACT_LAST_BYTE, type(uint8).max); assertEq(Hashes.ALL_TYPE, uint256(IRegistry.DelegationType.ALL)); assertEq(Hashes.CONTRACT_TYPE, uint256(IRegistry.DelegationType.CONTRACT)); @@ -162,27 +160,27 @@ contract RegistryHashTests is Test { /// @dev internal functions of the original registry hash specification to test optimized methods work as intended function _computeAll(address from, bytes32 rights, address to) internal pure returns (bytes32) { - return _encodeLastByteWithType(keccak256(abi.encode(from, rights, to)), IRegistry.DelegationType.ALL); + return _encodeLastByteWithType(keccak256(abi.encodePacked(rights, from, to)), IRegistry.DelegationType.ALL); } function _computeContract(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32) { - return _encodeLastByteWithType(keccak256(abi.encode(from, rights, to, contract_)), IRegistry.DelegationType.CONTRACT); + return _encodeLastByteWithType(keccak256(abi.encodePacked(rights, from, to, contract_)), IRegistry.DelegationType.CONTRACT); } function _computeERC721(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32) { - return _encodeLastByteWithType(keccak256(abi.encode(from, rights, to, tokenId, contract_)), IRegistry.DelegationType.ERC721); + return _encodeLastByteWithType(keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)), IRegistry.DelegationType.ERC721); } function _computeERC20(address from, bytes32 rights, address to, address contract_) internal pure returns (bytes32) { - return _encodeLastByteWithType(keccak256(abi.encode(from, rights, to, contract_)), IRegistry.DelegationType.ERC20); + return _encodeLastByteWithType(keccak256(abi.encodePacked(rights, from, to, contract_)), IRegistry.DelegationType.ERC20); } function _computeERC1155(address from, bytes32 rights, address to, uint256 tokenId, address contract_) internal pure returns (bytes32) { - return _encodeLastByteWithType(keccak256(abi.encode(from, rights, to, tokenId, contract_)), IRegistry.DelegationType.ERC1155); + return _encodeLastByteWithType(keccak256(abi.encodePacked(rights, from, to, contract_, tokenId)), IRegistry.DelegationType.ERC1155); } function _encodeLastByteWithType(bytes32 _input, IRegistry.DelegationType _type) internal pure returns (bytes32) { - return (_input & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00) | bytes32(uint256(_type)); + return bytes32((uint256(_input) << 8) | uint256(_type)); } function _decodeLastByteToType(bytes32 _input) internal pure returns (IRegistry.DelegationType) { diff --git a/test/RegistryOpsTests.t.sol b/test/RegistryOpsTests.t.sol new file mode 100644 index 0000000..682b4ac --- /dev/null +++ b/test/RegistryOpsTests.t.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; +import {IDelegateRegistry as IRegistry} from "src/IDelegateRegistry.sol"; +import {RegistryOps as Ops} from "src/libraries/RegistryOps.sol"; + +contract RegistryOpsTests is Test { + function _brutalizeBool(bool x) internal view returns (bool result) { + assembly { + mstore(0x00, gas()) + result := mul(iszero(iszero(x)), shl(128, keccak256(0x00, 0x20))) + } + } + + function _brutalizeUint32(uint32 x) internal view returns (uint32 result) { + assembly { + mstore(0x00, gas()) + result := or(x, shl(32, keccak256(0x00, 0x20))) + } + } + + function testMaxDifferential(uint256 x, uint256 y) public { + assertEq(Ops.max(x, y), x > y ? x : y); + } + + function testMaxDifferential(uint32 x, uint32 y) public { + assertEq(Ops.max(_brutalizeUint32(x), _brutalizeUint32(y)), x > y ? x : y); + } + + function testAndDifferential(bool x, bool y) public { + assertEq(Ops.and(_brutalizeBool(x), _brutalizeBool(y)), x && y); + } + + function testOrDifferential(bool x, bool y) public { + assertEq(Ops.or(_brutalizeBool(x), _brutalizeBool(y)), x || y); + } + + function testTruthyness(uint256 x, uint256 y) public { + bool xCasted; + bool yCasted; + assembly { + xCasted := x + yCasted := y + } + assertEq(xCasted, x != 0); + assertTrue(xCasted == (x != 0)); + assertEq(Ops.or(xCasted, yCasted), x != 0 || y != 0); + assertTrue(Ops.or(xCasted, yCasted) == (x != 0 || y != 0)); + if (Ops.or(xCasted, yCasted)) if (!(x != 0 || y != 0)) revert(); + if (x != 0 || y != 0) if (!Ops.or(xCasted, yCasted)) revert(); + assertEq(Ops.and(xCasted, yCasted), x != 0 && y != 0); + assertTrue(Ops.and(xCasted, yCasted) == (x != 0 && y != 0)); + if (Ops.and(xCasted, yCasted)) if (!(x != 0 && y != 0)) revert(); + if (x != 0 && y != 0) if (!Ops.and(xCasted, yCasted)) revert(); + } + + function testTruthyness(bool x, bool y) public { + bool xCasted; + bool yCasted; + assembly { + mstore(0x00, gas()) + xCasted := mul(iszero(iszero(x)), shl(128, keccak256(0x00, 0x20))) + mstore(0x00, gas()) + yCasted := mul(iszero(iszero(y)), shl(128, keccak256(0x00, 0x20))) + } + assertEq(x, xCasted); + assertEq(y, yCasted); + assembly { + if and(0xff, xCasted) { revert(0x00, 0x00) } + if and(0xff, yCasted) { revert(0x00, 0x00) } + } + assertEq(Ops.or(xCasted, yCasted), x || y); + assertTrue(Ops.or(xCasted, yCasted) == (x || y)); + if (Ops.or(xCasted, yCasted)) if (!(x || y)) revert(); + if (x || y) if (!Ops.or(xCasted, yCasted)) revert(); + assertEq(Ops.and(xCasted, yCasted), x && y); + assertTrue(Ops.and(xCasted, yCasted) == (x && y)); + if (Ops.and(xCasted, yCasted)) if (!(x && y)) revert(); + if (x && y) if (!Ops.and(xCasted, yCasted)) revert(); + } + + function testTruthyness(bool x) public { + bool casted; + if (casted) revert(); + assertEq(casted, false); + assertTrue(casted == false); + assembly { + if x { + mstore(0x00, gas()) + casted := mul(iszero(iszero(x)), shl(128, keccak256(0x00, 0x20))) + } + } + assertEq(x, casted); + assertTrue(x == casted); + if (x) if (!casted) revert(); + if (casted) if (!x) revert(); + } +}