-
Notifications
You must be signed in to change notification settings - Fork 0
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
support new buy artworks function for John Gerrard show #42
Changes from 9 commits
f2e970b
712f255
fff85ef
fc62a43
1b8902e
68109c5
6df789a
46673c9
03f1e4e
b4f3da0
dbebe04
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import {Nonces} from "./Nonces.sol"; | ||
import {FeralfileExhibitionV4_1} from "./FeralfileArtworkV4_1.sol"; | ||
import {IFeralfileVaultV2} from "./IFeralfileVaultV2.sol"; | ||
import {FeralfileSaleDataV2} from "./FeralfileSaleDataV2.sol"; | ||
|
||
contract FeralfileExhibitionV4_2 is | ||
FeralfileExhibitionV4_1, | ||
FeralfileSaleDataV2, | ||
Nonces | ||
{ | ||
error NotEnoughToken(); | ||
error TokenIDNotFound(); | ||
error FunctionNotSupported(); | ||
error SaleNotStarted(); | ||
error InvalidPaymentAmount(); | ||
error TotalBpsOver(); | ||
error InvalidAddress(); | ||
|
||
// vault contract instance | ||
IFeralfileVaultV2 public vaultV2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
|
||
mapping(uint256 => uint256) private seriesNextPurchasableTokenIds; // seriesID -> tokenID | ||
|
||
constructor( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
string memory name_, | ||
string memory symbol_, | ||
bool burnable_, | ||
bool bridgeable_, | ||
address signer_, | ||
address vault_, | ||
address costReceiver_, | ||
string memory contractURI_, | ||
uint256[] memory seriesIds_, | ||
uint256[] memory seriesMaxSupplies_, | ||
uint256[] memory seriesNextPurchasableTokenIds_ | ||
) | ||
FeralfileExhibitionV4_1( | ||
name_, | ||
symbol_, | ||
burnable_, | ||
bridgeable_, | ||
signer_, | ||
vault_, | ||
costReceiver_, | ||
contractURI_, | ||
seriesIds_, | ||
seriesMaxSupplies_ | ||
) | ||
{ | ||
vaultV2 = IFeralfileVaultV2(payable(vault_)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: check equal length of seriesIds_ and seriesNextPurchasableTokenIds_ |
||
for (uint256 i = 0; i < seriesIds_.length; i++) { | ||
seriesNextPurchasableTokenIds[seriesIds_[i]] = seriesNextPurchasableTokenIds_[i]; | ||
} | ||
} | ||
|
||
/// @notice Set vaultV2 contract | ||
/// @dev don't allow to set vaultV2 as zero address | ||
function setVaultV2(address vault_) external onlyOwner { | ||
if (vault_ == address(0)) { | ||
revert InvalidAddress(); | ||
} | ||
|
||
vaultV2 = IFeralfileVaultV2(payable(vault_)); | ||
} | ||
|
||
/// @notice override revert setVault | ||
function setVault(address) external pure override { | ||
revert FunctionNotSupported(); | ||
} | ||
|
||
/// @notice pay to get artworks to a destination address. The pricing, costs and other details is included in the saleData | ||
/// @param r_ - part of signature for validating parameters integrity | ||
/// @param s_ - part of signature for validating parameters integrity | ||
/// @param v_ - part of signature for validating parameters integrity | ||
/// @param saleData_ - the sale data | ||
function buyBulkArtworks( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
bytes32 r_, | ||
bytes32 s_, | ||
uint8 v_, | ||
SaleDataV2 calldata saleData_ | ||
) external payable { | ||
if (!_selling) { | ||
revert SaleNotStarted(); | ||
} | ||
|
||
uint256 balance = balanceOf(address(this)); | ||
if (balance < saleData_.quantity) { | ||
revert NotEnoughToken(); | ||
} | ||
|
||
validateSaleDataV2(saleData_); | ||
|
||
bytes32 message = keccak256( | ||
abi.encode(block.chainid, address(this), saleData_) | ||
); | ||
|
||
if (!isValidSignature(message, r_, s_, v_)) { | ||
revert InvalidSignature(); | ||
} | ||
|
||
//check nonce | ||
_useCheckedNonce(saleData_.destination, saleData_.nonce); | ||
hvthhien marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (saleData_.payByVaultContract) { | ||
vaultV2.payForSaleV2(r_, s_, v_, saleData_); | ||
} else { | ||
if (saleData_.price != msg.value) { | ||
revert InvalidPaymentAmount(); | ||
} | ||
} | ||
|
||
uint256 totalRevenue; | ||
if (saleData_.price > saleData_.cost) { | ||
totalRevenue = saleData_.price - saleData_.cost; | ||
} | ||
|
||
uint256 nextPurchasableTokenId = seriesNextPurchasableTokenIds[saleData_.seriesID]; | ||
uint256 i = 0; | ||
while (i < saleData_.quantity) { | ||
uint256 tokenIdForSale = nextPurchasableTokenId; | ||
|
||
if (!_exists(tokenIdForSale)) { | ||
revert TokenIDNotFound(); | ||
} | ||
|
||
nextPurchasableTokenId++; | ||
if (ownerOf(tokenIdForSale) != address(this)) { | ||
continue; | ||
} | ||
|
||
// send NFT | ||
_safeTransfer( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There would be a case that the remaining purchasable tokens is not enough compare to the requested quantity, the tx would be failed eventually but it will cost much gas since we transferred the token and emitted event already. So i'd suggest to accumulate the purchasable tokens into an array then loop over them to transfer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a rare case that only happens when out of tokens. but storing tokens in an array and performing an additional loop would cost some additional gas for each transaction. Since it's a rare case, so I think it would be more efficient to keep the transfer inside this loop. |
||
address(this), | ||
saleData_.destination, | ||
tokenIdForSale, | ||
"" | ||
); | ||
|
||
emit BuyArtworkV2( | ||
saleData_.destination, | ||
tokenIdForSale, | ||
saleData_.nonce | ||
); | ||
i++; | ||
} | ||
|
||
// save next sale token id for seriesID | ||
seriesNextPurchasableTokenIds[saleData_.seriesID] = nextPurchasableTokenId; | ||
|
||
// distribute royalty | ||
uint256 distributedRevenue; | ||
uint256 platformRevenue; | ||
|
||
RevenueShare[] memory revenueShares = saleData_.revenueShares; | ||
uint256 remainingRev = totalRevenue; | ||
|
||
// deduct advances payment from revenue | ||
for (uint256 j = 0; j < revenueShares.length && remainingRev > 0; j++) { | ||
uint256 remainingAdvanceAmount = advances[ | ||
revenueShares[j].recipient | ||
]; | ||
|
||
if (remainingAdvanceAmount == 0) { | ||
continue; | ||
} | ||
|
||
uint256 prePaidRev = remainingAdvanceAmount >= remainingRev | ||
? remainingRev | ||
: remainingAdvanceAmount; | ||
platformRevenue += prePaidRev; | ||
advances[revenueShares[j].recipient] -= prePaidRev; | ||
remainingRev -= prePaidRev; | ||
} | ||
|
||
// distribute revenue | ||
if (remainingRev > 0) { | ||
for (uint256 j = 0; j < revenueShares.length; j++) { | ||
address recipient = revenueShares[j].recipient; | ||
uint256 rev = (remainingRev * revenueShares[j].bps) / 10000; | ||
if (recipient == costReceiver) { | ||
platformRevenue += rev; | ||
continue; | ||
} | ||
distributedRevenue += rev; | ||
payable(recipient).transfer(rev); | ||
} | ||
} | ||
|
||
if ( | ||
saleData_.price - saleData_.cost < | ||
distributedRevenue + platformRevenue | ||
) { | ||
revert TotalBpsOver(); | ||
} | ||
Comment on lines
+197
to
+202
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a miss from long time ago that we should add a check for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or we could discuss should we allow to submit a buy tx that cost larger than price. |
||
|
||
// Transfer cost, platform revenue and remaining funds | ||
uint256 leftOver = saleData_.price - distributedRevenue; | ||
if (leftOver > 0) { | ||
payable(costReceiver).transfer(leftOver); | ||
} | ||
} | ||
|
||
/// @notice override revert buyArtworks | ||
function buyArtworks( | ||
bytes32, | ||
bytes32, | ||
uint8, | ||
SaleData calldata | ||
) external payable override { | ||
revert FunctionNotSupported(); | ||
} | ||
|
||
/// @notice Event emitted when Artwork has been sold with the additional nonce | ||
event BuyArtworkV2( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
address indexed buyer, | ||
uint256 indexed tokenId, | ||
uint256 nonce | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import {IFeralfileSaleDataV2} from "./IFeralfileSaleDataV2.sol"; | ||
|
||
contract FeralfileSaleDataV2 is IFeralfileSaleDataV2 { | ||
function validateSaleDataV2(SaleDataV2 calldata saleData_) internal view { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be |
||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
saleData_.expiryTime > block.timestamp, | ||
"FeralfileSaleData: sale is expired" | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
import {FeralfileSaleDataV2} from "./FeralfileSaleDataV2.sol"; | ||
import {ECDSASigner} from "./ECDSASigner.sol"; | ||
|
||
contract FeralfileVaultV2 is Ownable, FeralfileSaleDataV2, ECDSASigner { | ||
mapping(bytes32 => bool) private _paidSale; | ||
|
||
constructor(address signer_) ECDSASigner(signer_) {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
/// @notice pay for buyArtwork to a FFV4 contract destination. | ||
/// @param r_ - part of signature for validating parameters integrity | ||
/// @param s_ - part of signature for validating parameters integrity | ||
/// @param v_ - part of signature for validating parameters integrity | ||
/// @param saleData_ - the sale data | ||
function payForSaleV2( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the payload is different from the old Vault, so I changed the name to reduce the mistake when calling. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless it makes us confused, otherwise i dont see any reason to name it with the version suffix. |
||
bytes32 r_, | ||
bytes32 s_, | ||
uint8 v_, | ||
SaleDataV2 calldata saleData_ | ||
) external { | ||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
saleData_.payByVaultContract, | ||
"FeralfileVault: not pay by vault" | ||
); | ||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
address(this).balance >= saleData_.price, | ||
"FeralfileVault: insufficient balance" | ||
); | ||
|
||
validateSaleDataV2(saleData_); | ||
|
||
bytes32 message = keccak256( | ||
abi.encode(block.chainid, msg.sender, saleData_) | ||
); | ||
require(!_paidSale[message], "FeralfileVault: paid sale"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
isValidSignature(message, r_, s_, v_), | ||
"FeralfileVault: invalid signature" | ||
); | ||
_paidSale[message] = true; | ||
payable(msg.sender).transfer(saleData_.price); | ||
} | ||
|
||
function withdrawFund(uint256 weiAmount) external onlyOwner { | ||
require( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
address(this).balance >= weiAmount, | ||
"FeralfileVault: insufficient balance" | ||
); | ||
payable(msg.sender).transfer(weiAmount); | ||
} | ||
|
||
receive() external payable {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import {IFeralfileSaleData} from "./IFeralfileSaleData.sol"; | ||
|
||
interface IFeralfileSaleDataV2 is IFeralfileSaleData { | ||
struct SaleDataV2 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. I thought you could use separate interface but it seems like we have to reuse the |
||
uint256 price; // in wei | ||
uint256 cost; // in wei | ||
uint256 expiryTime; | ||
address destination; | ||
uint256 nonce; | ||
uint256 seriesID; | ||
uint16 quantity; | ||
RevenueShare[] revenueShares; // address and royalty bps (500 means 5%) | ||
bool payByVaultContract; // get eth from vault contract, used by credit card pay that proxy by ITX | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import {IFeralfileSaleDataV2} from "./IFeralfileSaleDataV2.sol"; | ||
|
||
interface IFeralfileVaultV2 is IFeralfileSaleDataV2 { | ||
function payForSaleV2( | ||
bytes32 r_, | ||
bytes32 s_, | ||
uint8 v_, | ||
SaleDataV2 calldata saleData_ | ||
) external; | ||
|
||
function withdrawFund(uint256 weiAmount) external; | ||
|
||
receive() external payable; | ||
} |
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.
Contract, Structs and Enums should be in CamelCase