This repository has been archived by the owner on Dec 23, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 70
Gasless Asks Extension #160
Open
tbtstl
wants to merge
5
commits into
main
Choose a base branch
from
t/gasless-sell-orders
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5bf9764
[feat] unoptimized gasless asks
tbtstl fd0dc95
refactor: move ModuleApprovalSig to IGaslessAsksCoreEth
d5825b7
refactor: restructure as independent module
eb4796d
refactor: decouple ask & module approval
23f29cd
chore: update interface
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,13 @@ pragma solidity 0.8.10; | |
|
||
import {ZoraProtocolFeeSettings} from "./auxiliary/ZoraProtocolFeeSettings/ZoraProtocolFeeSettings.sol"; | ||
|
||
struct ModuleApprovalSig { | ||
uint8 v; // The 129th byte and chain ID of the signature | ||
bytes32 r; // The first 64 bytes of the signature | ||
bytes32 s; // Bytes 64-128 of the signature | ||
uint256 deadline; // The deadline at which point the approval expires | ||
} | ||
|
||
/// @title ZoraModuleManager | ||
/// @author tbtstl <[email protected]> | ||
/// @notice This contract allows users to approve registered modules on ZORA V3 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity 0.8.10; | ||
|
||
import {ReentrancyGuard} from "@rari-capital/solmate/src/utils/ReentrancyGuard.sol"; | ||
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; | ||
|
||
import {ZoraModuleManager} from "../../../../ZoraModuleManager.sol"; | ||
import {FeePayoutSupportV1} from "../../../../common/FeePayoutSupport/FeePayoutSupportV1.sol"; | ||
import {ModuleNamingSupportV1} from "../../../../common/ModuleNamingSupport/ModuleNamingSupportV1.sol"; | ||
import {IGaslessAsksCoreEth} from "./IGaslessAsksCoreEth.sol"; | ||
import {AsksCoreEth} from "./AsksCoreEth.sol"; | ||
|
||
/// @title Gasless Asks Core ETH | ||
/// @author tbtstl | ||
/// @notice Extension to minimal ETH asks module, providing off-chain order support | ||
contract GaslessAsksCoreEth is IGaslessAsksCoreEth, ReentrancyGuard, FeePayoutSupportV1, ModuleNamingSupportV1 { | ||
/// /// | ||
/// IMMUTABLES /// | ||
/// /// | ||
|
||
ZoraModuleManager private immutable zmm; | ||
AsksCoreEth private immutable asksModule; | ||
|
||
/// @notice The EIP-712 domain separator | ||
bytes32 private immutable EIP_712_DOMAIN_SEPARATOR = | ||
keccak256( | ||
abi.encode( | ||
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | ||
keccak256(bytes("ZORA:GaslessAsksCoreEth")), | ||
keccak256(bytes("1")), | ||
_chainID(), | ||
address(this) | ||
) | ||
); | ||
|
||
/// @notice The EIP-712 type for a signed ask order | ||
/// @dev keccak256("SignedAsk(address tokenAddress,uint256 tokenId,uint256 expiry,uint256 nonce, uint256 amount,uint8 _v,bytes32 _r,bytes32 _s,uint256 deadline)") | ||
bytes32 private constant SIGNED_ASK_TYPEHASH = 0x324d0f7b7aa4e0f218259028fc60b98a32657c974f8cb44eb3ceadbec042ddc4; | ||
|
||
/// /// | ||
/// ASK STORAGE /// | ||
/// /// | ||
|
||
/// @notice The spent (canceled or executed) asks | ||
/// @dev ERC-721 token contract => ERC-721 token id => Ask signer => spent boolean | ||
mapping(address => mapping(uint256 => mapping(address => bool))) public spentAsks; | ||
|
||
/// @param _zmm The ZORA Module Manager address | ||
/// @param _asksModule The ZORA Asks Core ETH Module address | ||
/// @param _royaltyEngine The Manifold Royalty Engine address | ||
/// @param _protocolFeeSettings The ZORA Protocol Fee Settings address | ||
/// @param _weth The WETH token address | ||
// TODO we don't even need this to be a "module" since no transfer helpers are used here | ||
constructor( | ||
address _zmm, | ||
address _asksModule, | ||
address _royaltyEngine, | ||
address _protocolFeeSettings, | ||
address _weth | ||
) | ||
FeePayoutSupportV1(_royaltyEngine, _protocolFeeSettings, _weth, ZoraModuleManager(_zmm).registrar()) | ||
ModuleNamingSupportV1("Gasless Asks Core ETH") | ||
{ | ||
zmm = ZoraModuleManager(_zmm); | ||
asksModule = AsksCoreEth(_asksModule); | ||
} | ||
|
||
/// @notice Implements EIP-165 for standard interface detection | ||
/// @dev `0x01ffc9a7` is the IERC165 interface id | ||
/// @param _interfaceId The identifier of a given interface | ||
/// @return If the given interface is supported | ||
function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { | ||
return _interfaceId == type(IGaslessAsksCoreEth).interfaceId || _interfaceId == 0x01ffc9a7; | ||
} | ||
|
||
/// @notice Executes a signed order on the Asks module | ||
/// @param _ask The signed ask parameters to execute | ||
/// @param _v The 129th byte and chain ID of the signature | ||
/// @param _r The first 64 bytes of the signature | ||
/// @param _s Bytes 64-128 of the signature | ||
function executeAsk( | ||
IGaslessAsksCoreEth.GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external payable nonReentrant { | ||
require(_ask.expiry == 0 || _ask.expiry >= block.timestamp, "EXPIRED_ASK"); | ||
address recoveredAddress = _recoverAddress(_ask, _v, _r, _s); | ||
require(recoveredAddress != address(0) && recoveredAddress == _ask.from, "INVALID_SIG"); | ||
require(!spentAsks[_ask.tokenAddress][_ask.tokenId][_ask.from], "SPENT_ASK"); | ||
|
||
if (!zmm.isModuleApproved(_ask.from, address(asksModule))) { | ||
zmm.setApprovalForModuleBySig( | ||
address(asksModule), | ||
_ask.from, | ||
true, | ||
_ask.approvalSig.deadline, | ||
_ask.approvalSig.v, | ||
_ask.approvalSig.r, | ||
_ask.approvalSig.s | ||
); | ||
} | ||
|
||
asksModule.createAsk(_ask.tokenAddress, _ask.tokenId, _ask.amount); | ||
asksModule.fillAsk{value: msg.value}(_ask.tokenAddress, _ask.tokenId); | ||
spentAsks[_ask.tokenAddress][_ask.tokenId][_ask.from] = true; | ||
|
||
IERC721(_ask.tokenAddress).transferFrom(address(this), msg.sender, _ask.tokenId); | ||
} | ||
|
||
/// @notice Creates an on-chain order on the Asks module | ||
/// @param _ask The signed ask parameters to store | ||
/// @param _v The 129th byte and chain ID of the signature | ||
/// @param _r The first 64 bytes of the signature | ||
/// @param _s Bytes 64-128 of the signature | ||
function storeAsk( | ||
IGaslessAsksCoreEth.GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external nonReentrant { | ||
require(_ask.expiry == 0 || _ask.expiry >= block.timestamp, "EXPIRED_ASK"); | ||
address recoveredAddress = _recoverAddress(_ask, _v, _r, _s); | ||
require(recoveredAddress != address(0) && recoveredAddress == _ask.from, "INVALID_SIG"); | ||
|
||
asksModule.createAsk(_ask.tokenAddress, _ask.tokenId, _ask.amount); | ||
} | ||
|
||
/// @notice Broadcasts an on-chain order to indexers | ||
/// @dev Intentionally a no-op, this can be picked up via EVM traces :) | ||
/// @param _ask The signed ask parameters to broadcast | ||
/// @param _v The 129th byte and chain ID of the signature | ||
/// @param _r The first 64 bytes of the signature | ||
/// @param _s Bytes 64-128 of the signature | ||
function broadcastAsk( | ||
IGaslessAsksCoreEth.GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external { | ||
// noop :) | ||
} | ||
|
||
/// @notice Invalidates an off-chain order | ||
/// @param _ask The signed ask parameters to invalidate | ||
function cancelAsk(IGaslessAsksCoreEth.GaslessAsk calldata _ask) external nonReentrant { | ||
require(msg.sender == _ask.from, "ONLY_SIGNER"); | ||
|
||
spentAsks[_ask.tokenAddress][_ask.tokenId][msg.sender] = true; | ||
} | ||
|
||
/// @notice Validates an on-chain order | ||
/// @param _ask The signed ask parameters to validate | ||
/// @param _v The 129th byte and chain ID of the signature | ||
/// @param _r The first 64 bytes of the signature | ||
/// @param _s Bytes 64-128 of the signature | ||
function validateAskSig( | ||
IGaslessAsksCoreEth.GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external view returns (bool) { | ||
return _recoverAddress(_ask, _v, _r, _s) == _ask.from; | ||
} | ||
|
||
function _recoverAddress( | ||
IGaslessAsksCoreEth.GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) private view returns (address) { | ||
bytes32 digest = keccak256( | ||
abi.encodePacked( | ||
"\x19\x01", | ||
EIP_712_DOMAIN_SEPARATOR, | ||
keccak256( | ||
abi.encode( | ||
SIGNED_ASK_TYPEHASH, | ||
_ask.tokenAddress, | ||
_ask.tokenId, | ||
_ask.expiry, | ||
_ask.nonce, | ||
_ask.amount, | ||
_ask.approvalSig.v, | ||
_ask.approvalSig.r, | ||
_ask.approvalSig.s, | ||
_ask.approvalSig.deadline | ||
) | ||
) | ||
) | ||
); | ||
|
||
return ecrecover(digest, _v, _r, _s); | ||
} | ||
|
||
/// @notice The EIP-155 chain id | ||
function _chainID() private view returns (uint256 id) { | ||
assembly { | ||
id := chainid() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
pragma solidity 0.8.10; | ||
|
||
import {ModuleApprovalSig} from "../../../../ZoraModuleManager.sol"; | ||
|
||
interface IGaslessAsksCoreEth { | ||
struct GaslessAsk { | ||
address from; // The address of the seller | ||
address tokenAddress; // The address of the NFT being sold | ||
uint256 tokenId; // The ID of the NFT being sold | ||
uint256 expiry; // The Unix timestamp that this order expires at | ||
uint256 nonce; // Nonce to represent this order (for cancellations) | ||
uint256 amount; // The amount of ETH to sell the NFT for | ||
ModuleApprovalSig approvalSig; // The user's approval to use this module (optional, empty if already set) | ||
} | ||
|
||
function executeAsk( | ||
GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external payable; | ||
|
||
function storeAsk( | ||
GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external; | ||
|
||
function broadcastAsk( | ||
GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external; | ||
|
||
function cancelAsk(GaslessAsk calldata _ask) external; | ||
|
||
function validateAskSig( | ||
GaslessAsk calldata _ask, | ||
uint8 _v, | ||
bytes32 _r, | ||
bytes32 _s | ||
) external view returns (bool); | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Should these comments instead say 65th byte, first 32 bytes, and bytes 32-64, respectively? Or am I missing another part of the signature?
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.
Lmao I honestly didn't even expect to have review permission on 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.
I think it's correct – from the EIP-712 spec:
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.
Ohh yea the ERC is very unclear with the terminology there. Seems it confuses bytes with hex characters in that paragraph (the appendix it references also says there are 65 bytes in a sig). Thanks for linking!
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.
Lol it also said to expect a 129 byte array as a signature but shows a 65 byte array in the example just below?? I’m gonna try and open a PR over in ethereum/EIPs