-
Notifications
You must be signed in to change notification settings - Fork 9
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
Force royalties on ERC721 #55
base: main
Are you sure you want to change the base?
Conversation
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.
Give this a shot!
lastBalance: uint256 | ||
|
||
# @dev we check this value to make sure royalties have been paid | ||
royaltyAmount: uint256 |
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.
Would suggest minRoyaltyAmount
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.
Also, add a note here to explore means of setting minRoyaltyAmount
based on floor price oracle
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.
Also also, need to set this in __init__
to a nonzero value
""" | ||
|
||
royalty: uint256 = convert(convert(_salePrice, decimal) * ROYALTY_TO_APPLY_TO_PRICE, uint256) # Percentage that accepts decimals | ||
return self.owner, royalty |
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.
and then here do:
return self.owner, royalty | |
# NOTE: We are returning `self` as the receiver of the royalty to enforce payment to `self.owner` later | |
return self, max(self.minRoyaltyAmount, royalty) | |
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.
@fubuloubu this max
is a genius solution for a future implementation of a minimum sell price looking at minRoyaltyAmount
. Hats off. 🙏
@@ -378,6 +437,10 @@ def transferFrom(owner: address, receiver: address, tokenId: uint256): | |||
@param receiver The new owner. | |||
@param tokenId The NFT to transfer. | |||
""" | |||
{%- if cookiecutter.force_royalties == 'y' %} | |||
# check if royalties have been paid | |||
self.royaltyChecker(tokenId) |
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.
We can simplify this a bunch!
First, gonna change this function
self.royaltyChecker(tokenId) | |
self._enforceRoyalties(msg.sender, tokenId) |
@@ -403,6 +466,10 @@ def safeTransferFrom( | |||
@param tokenId The NFT to transfer. | |||
@param data Additional data with no specified format, sent in call to `receiver`. | |||
""" | |||
{%- if cookiecutter.force_royalties == 'y' %} | |||
# check if royalties have been paid | |||
self.royaltyChecker(tokenId) |
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.
And here
self.royaltyChecker(tokenId) | |
self._enforceRoyalties(msg.sender, tokenId) |
return self.owner, royalty | ||
|
||
@internal | ||
def royaltyChecker(tokenId: uint256): |
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.
def royaltyChecker(tokenId: uint256): | |
def _enforceRoyalties(caller: address, tokenId: uint256): |
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.
@fubuloubu why include the caller
if you don't use it on the function?
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.
In a future scenario you might want to use it to detect which marketplace is triggering the call to update the minimum royalty amount
For now you can probably skip the check if msg.sender
is not a contract
# @dev the last balance of the smart contract that stores the royalties of the contract creator | ||
# this balance is reset to 0 the moment the creator withdraws royalties | ||
lastBalance: uint256 | ||
|
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.
Don't actually need this!
# @dev the last balance of the smart contract that stores the royalties of the contract creator | |
# this balance is reset to 0 the moment the creator withdraws royalties | |
lastBalance: uint256 |
if self.balance < self.lastBalance: | ||
self._deductRoyalties(tokenId) | ||
# equal the contract balance to the lastBalance for future checks | ||
self.lastBalance = self.balance |
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.
if self.balance < self.lastBalance: | |
self._deductRoyalties(tokenId) | |
# equal the contract balance to the lastBalance for future checks | |
self.lastBalance = self.balance | |
assert self.balance >= self.minRoyaltyAmount | |
# Send all balance to the owner (clears `self.balance`) | |
send(self.owner, self.balance) | |
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.
@fubuloubu for assert self.balance >= self.minRoyaltyAmount
to make sense, we need to set it back to 0 with the send(self.owner, self.balance)
→ OK. Would you allow the creator to modify the minRoyaltyAmount
? I think that could be a nice feature is case is set too hight/low at genesis
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.
Also withdrawRoyalties()
might be very beneficial to keep from a fiscal perspective so that the artist can control when he gets paid, therefore pay his/her taxes... checking this with a lawyer expert on the token matter.
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.
@fubuloubu for
assert self.balance >= self.minRoyaltyAmount
to make sense, we need to set it back to 0 with thesend(self.owner, self.balance)
→ OK. Would you allow the creator to modify theminRoyaltyAmount
? I think that could be a nice feature is case is set too hight/low at genesis
That's the trick! It'll automatically be 0 because the balance will be physically transferred via send
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.
Also
withdrawRoyalties()
might be very beneficial to keep from a fiscal perspective so that the artist can control when he gets paid, therefore pay his/her taxes... checking this with a lawyer expert on the token matter.
From a tax perspective, income is income the moment it's earned (made available to you under your full control), so I don't think there's any real tax optimization tricks here
also would have to update any tests where there's a transfer occuring to have |
# @dev this can also be used to explore a based floor price oracle for a minimum royalty payment to the creator | ||
# @ dev current implementation sends to the smart contract the apply % in royalties to the creator |
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.
Don't need @dev
tag three times in a row
Missing |
|
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.
Finally! We got it done!
Were you able to validate on OpenSea testnet?
cookiecutter.json
Outdated
"force_royalties": "y", | ||
"royalty_percentage_in_decimals": 10.0, |
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.
Can you flip these two? Royalty percentage is just for regular royalties, while min royalty amount is for forced royalties
return self, max(self.minRoyaltyAmount, royalty) | ||
|
||
@internal | ||
def _enforceRoyalties(tokenId: uint256): |
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.
Add a note on how this function might be expanding to allow updating the floor price oracle
@@ -294,13 +303,40 @@ def royaltyInfo(_tokenId: uint256, _salePrice: uint256) -> (address, uint256): | |||
/// @param _tokenId - the NFT asset queried for royalty information | |||
/// @param _salePrice - the sale price of the NFT asset specified by _tokenId | |||
/// @return receiver - address of who should be sent the royalty payment | |||
/// @return royaltyAmount - the royalty payment amount for _salePrice | |||
/// @return minRoyaltyAmount - the minimum royalty payment amount for _salePrice | |||
""" | |||
|
|||
royalty: uint256 = convert(convert(_salePrice, decimal) * ROYALTY_TO_APPLY_TO_PRICE, uint256) # Percentage that accepts decimals | |||
return self.owner, royalty |
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.
This actually needs to be update to integrate well
return self.owner, royalty | |
return self, max(self.minRoyaltyAmount, royalty) |
# Helper functions in case market place does not support royalties to execute by this contract with _enforceRoyalties() | ||
@internal | ||
@view | ||
def _royaltyInfo(_tokenId: uint256, _salePrice: uint256) -> (address, uint256): | ||
""" | ||
/// @notice Called with the sale price to determine how much royalty | ||
// is owed and to whom. Important; Not all marketplaces respect this, e.g. OpenSea | ||
/// @param _tokenId - the NFT asset queried for royalty information | ||
/// @param _salePrice - the sale price of the NFT asset specified by _tokenId | ||
/// @return receiver - address of who should be sent the royalty payment | ||
/// @return owner address and minRoyaltyAmount - the minimum royalty payment amount for _salePrice | ||
""" | ||
|
||
royalty: uint256 = convert(convert(_salePrice, decimal) * ROYALTY_TO_APPLY_TO_PRICE, uint256) # Percentage that accepts decimals | ||
# NOTE: We are returning `self` as the receiver of the royalty to enforce payment to `self.owner` later | ||
return self, max(self.minRoyaltyAmount, royalty) | ||
|
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.
Then this function actually isn't necessary
# Helper functions in case market place does not support royalties to execute by this contract with _enforceRoyalties() | |
@internal | |
@view | |
def _royaltyInfo(_tokenId: uint256, _salePrice: uint256) -> (address, uint256): | |
""" | |
/// @notice Called with the sale price to determine how much royalty | |
// is owed and to whom. Important; Not all marketplaces respect this, e.g. OpenSea | |
/// @param _tokenId - the NFT asset queried for royalty information | |
/// @param _salePrice - the sale price of the NFT asset specified by _tokenId | |
/// @return receiver - address of who should be sent the royalty payment | |
/// @return owner address and minRoyaltyAmount - the minimum royalty payment amount for _salePrice | |
""" | |
royalty: uint256 = convert(convert(_salePrice, decimal) * ROYALTY_TO_APPLY_TO_PRICE, uint256) # Percentage that accepts decimals | |
# NOTE: We are returning `self` as the receiver of the royalty to enforce payment to `self.owner` later | |
return self, max(self.minRoyaltyAmount, royalty) |
@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.
I usually like to switch these
.build/__local__.json
Outdated
@@ -0,0 +1 @@ | |||
{"contractTypes":{},"manifest":"ethpm/3","sources":{}} |
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.
Delete this file, and add .build/
to gitignore
tests/Mintable.json
Outdated
"force_royalties": "y", | ||
"minRoyaltyAmount": 1000000000000000 |
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.
Can this be n
and 0?
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.
can be 0
, cannot be n
[string] but any int
. your thinking creating a function that allows the creator to modify this at any stage.
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.
No I meant force royalties is n
for this case
Can you add |
Head branch was pushed to by a user without write access
.coverage | ||
.coverage.* | ||
.cache | ||
.DS_Store |
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.
Usually files like this are best in your global gitignore
https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
{%- if cookiecutter.force_royalties == 'y' %} | ||
@external | ||
@payable | ||
def __default__(): |
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.
Typically, when smart contracts built by compilers like solidity or vyper send ether to a contract, it only gives a limited amount of gas to that contract to execute. So, basically I think this won't work since there's too much logic going on inside it
Force royalties on ERC721 in case the market place does not support/distribute royalties to creators.