Created
November 6, 2021 00:01
-
-
Save jaredcohe/ffcf7028c88b63d8e79dcf1cec3cc33a to your computer and use it in GitHub Desktop.
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
// SPDX-License-Identifier: GPL-3.0-or-later | |
pragma solidity >=0.8.0; | |
/// @notice Modern and gas efficient ERC-721 + ERC-20/EIP-2612-like implementation. | |
contract LexNFT { | |
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); | |
event Approval(address indexed owner, address indexed spender, uint256 indexed tokenId); | |
event ApprovalForAll(address indexed owner, address indexed operator, bool approved); | |
string public name; | |
string public symbol; | |
uint256 public totalSupply; | |
mapping(address => uint256) public balanceOf; | |
mapping(uint256 => address) public ownerOf; | |
mapping(uint256 => string) public tokenURI; | |
mapping(uint256 => address) public getApproved; | |
mapping(address => mapping(address => bool)) public isApprovedForAll; | |
bytes32 public constant PERMIT_TYPEHASH = | |
keccak256("Permit(address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); | |
bytes32 public constant PERMIT_ALL_TYPEHASH = | |
keccak256("Permit(address owner,address spender,uint256 nonce,uint256 deadline)"); | |
uint256 internal immutable INITIAL_CHAIN_ID; | |
bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; | |
mapping(uint256 => uint256) public nonces; | |
mapping(address => uint256) public noncesForAll; | |
constructor( | |
string memory _name, | |
string memory _symbol | |
) { | |
name = _name; | |
symbol = _symbol; | |
INITIAL_CHAIN_ID = block.chainid; | |
INITIAL_DOMAIN_SEPARATOR = _calculateDomainSeparator(); | |
} | |
function _calculateDomainSeparator() internal view returns (bytes32 domainSeparator) { | |
domainSeparator = keccak256( | |
abi.encode( | |
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), | |
keccak256(bytes(name)), | |
keccak256(bytes("1")), | |
block.chainid, | |
address(this) | |
) | |
); | |
} | |
function DOMAIN_SEPARATOR() public view returns (bytes32 domainSeparator) { | |
domainSeparator = block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _calculateDomainSeparator(); | |
} | |
function supportsInterface(bytes4 interfaceId) public pure returns (bool supported) { | |
supported = interfaceId == 0x80ac58cd || interfaceId == 0x5b5e139f || interfaceId == 0x01ffc9a7; | |
} | |
function approve(address spender, uint256 tokenId) public virtual { | |
address owner = ownerOf[tokenId]; | |
require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_APPROVED"); | |
getApproved[tokenId] = spender; | |
emit Approval(owner, spender, tokenId); | |
} | |
function setApprovalForAll(address operator, bool approved) public virtual { | |
isApprovedForAll[msg.sender][operator] = approved; | |
emit ApprovalForAll(msg.sender, operator, approved); | |
} | |
function transfer(address to, uint256 tokenId) public virtual { | |
require(msg.sender == ownerOf[tokenId], "NOT_OWNER"); | |
// This is safe because ownership is checked | |
// against decrement, and sum of all user | |
// balances can't exceed 'type(uint256).max'. | |
unchecked { | |
balanceOf[msg.sender]--; | |
balanceOf[to]++; | |
} | |
delete getApproved[tokenId]; | |
ownerOf[tokenId] = to; | |
emit Transfer(msg.sender, to, tokenId); | |
} | |
function transferFrom(address, address to, uint256 tokenId) public virtual { | |
address owner = ownerOf[tokenId]; | |
require( | |
msg.sender == owner | |
|| msg.sender == getApproved[tokenId] | |
|| isApprovedForAll[owner][msg.sender], | |
"NOT_APPROVED" | |
); | |
// This is safe because ownership is checked | |
// against decrement, and sum of all user | |
// balances can't exceed 'type(uint256).max'. | |
unchecked { | |
balanceOf[owner]--; | |
balanceOf[to]++; | |
} | |
delete getApproved[tokenId]; | |
ownerOf[tokenId] = to; | |
emit Transfer(owner, to, tokenId); | |
} | |
function safeTransferFrom(address, address to, uint256 tokenId) public virtual { | |
safeTransferFrom(address(0), to, tokenId, ""); | |
} | |
function safeTransferFrom(address, address to, uint256 tokenId, bytes memory data) public virtual { | |
transferFrom(address(0), to, tokenId); | |
if (to.code.length != 0) { | |
// selector = `onERC721Received(address,address,uint,bytes)`. | |
(, bytes memory returned) = to.staticcall(abi.encodeWithSelector(0x150b7a02, | |
msg.sender, address(0), tokenId, data)); | |
bytes4 selector = abi.decode(returned, (bytes4)); | |
require(selector == 0x150b7a02, "NOT_ERC721_RECEIVER"); | |
} | |
} | |
function permit( | |
address spender, | |
uint256 tokenId, | |
uint256 deadline, | |
uint8 v, | |
bytes32 r, | |
bytes32 s | |
) public virtual { | |
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); | |
address owner = ownerOf[tokenId]; | |
// This is reasonably safe from overflow because incrementing `nonces` beyond | |
// 'type(uint256).max' is exceedingly unlikely compared to optimization benefits. | |
unchecked { | |
bytes32 digest = keccak256( | |
abi.encodePacked( | |
"\x19\x01", | |
DOMAIN_SEPARATOR(), | |
keccak256(abi.encode(PERMIT_TYPEHASH, spender, tokenId, nonces[tokenId]++, deadline)) | |
) | |
); | |
address recoveredAddress = ecrecover(digest, v, r, s); | |
require(recoveredAddress != address(0), "INVALID_PERMIT_SIGNATURE"); | |
require(recoveredAddress == owner || isApprovedForAll[owner][recoveredAddress], "INVALID_SIGNER"); | |
} | |
getApproved[tokenId] = spender; | |
emit Approval(owner, spender, tokenId); | |
} | |
function permitAll( | |
address owner, | |
address operator, | |
uint256 deadline, | |
uint8 v, | |
bytes32 r, | |
bytes32 s | |
) public virtual { | |
require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); | |
// This is reasonably safe from overflow because incrementing `nonces` beyond | |
// 'type(uint256).max' is exceedingly unlikely compared to optimization benefits. | |
unchecked { | |
bytes32 digest = keccak256( | |
abi.encodePacked( | |
"\x19\x01", | |
DOMAIN_SEPARATOR(), | |
keccak256(abi.encode(PERMIT_ALL_TYPEHASH, owner, operator, noncesForAll[owner]++, deadline)) | |
) | |
); | |
address recoveredAddress = ecrecover(digest, v, r, s); | |
require( | |
(recoveredAddress != address(0) && recoveredAddress == owner) || isApprovedForAll[owner][recoveredAddress], | |
"INVALID_PERMIT_SIGNATURE" | |
); | |
} | |
isApprovedForAll[owner][operator] = true; | |
emit ApprovalForAll(owner, operator, true); | |
} | |
function _mint(address to, uint256 tokenId, string memory _tokenURI) internal { | |
require(ownerOf[tokenId] == address(0), "ALREADY_MINTED"); | |
// This is reasonably safe from overflow because incrementing `totalSupply` beyond | |
// 'type(uint256).max' is exceedingly unlikely compared to optimization benefits, | |
// and because the sum of all user balances can't exceed 'type(uint256).max'. | |
unchecked { | |
totalSupply++; | |
balanceOf[to]++; | |
} | |
ownerOf[tokenId] = to; | |
tokenURI[tokenId] = _tokenURI; | |
emit Transfer(address(0), to, tokenId); | |
} | |
function _burn(uint256 tokenId) internal { | |
address owner = ownerOf[tokenId]; | |
require(ownerOf[tokenId] != address(0), "NOT_MINTED"); | |
// This is safe because a user won't ever | |
// have a balance larger than `totalSupply`. | |
unchecked { | |
totalSupply--; | |
balanceOf[owner]--; | |
} | |
delete ownerOf[tokenId]; | |
delete tokenURI[tokenId]; | |
emit Transfer(owner, address(0), tokenId); | |
} | |
} | |
interface IERC20 { | |
function transfer(address to, uint256 amount) external; | |
function transferFrom(address from, address to, uint256 amount) external; | |
} | |
contract NFTWrapper is LexNFT { | |
mapping(uint => Derivative) derivatives; | |
struct Derivative { | |
IERC20[] underlyingAssets; | |
uint[] amounts; | |
string details; | |
uint expiration; // unix | |
} | |
constructor(string memory name, string memory symbol) LexNFT(name, symbol) { | |
} | |
function makeDerivative(address address_, uint tokenId_, string memory tokenURI_, IERC20[] memory underlyingAssets_, string memory details_, uint[] memory amounts_, uint expiration_) external { | |
_mint(address_, tokenId_, tokenURI_); | |
Derivative memory _derivative = Derivative({ | |
underlyingAssets: underlyingAssets_, | |
amounts: amounts_, | |
details: details_, | |
expiration: expiration_ | |
}); | |
derivatives[tokenId_] = _derivative; | |
uint countUnderlyingAssets = underlyingAssets_.length; | |
for (uint i=0; i<countUnderlyingAssets; i++) { | |
underlyingAssets_[i].transferFrom(msg.sender, address(this), amounts_[i]); | |
} | |
} | |
function withdraw(uint tokenId_) external { | |
_burn(tokenId_); | |
Derivative memory derivative = derivatives[tokenId_]; | |
uint countUnderlyingAssets = derivative.underlyingAssets.length; | |
for (uint i=0; i<countUnderlyingAssets; i++) { | |
derivative.underlyingAssets[i].transfer(msg.sender, derivative.amounts[i]); | |
} | |
delete derivatives[tokenId_]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment