Skip to content
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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

Conversation

victor-ego
Copy link
Contributor

Force royalties on ERC721 in case the market place does not support/distribute royalties to creators.

Copy link
Contributor

@fubuloubu fubuloubu left a 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would suggest minRoyaltyAmount

Copy link
Contributor

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

Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and then here do:

Suggested change
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)

Copy link
Contributor Author

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)
Copy link
Contributor

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

Suggested change
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here

Suggested change
self.royaltyChecker(tokenId)
self._enforceRoyalties(msg.sender, tokenId)

return self.owner, royalty

@internal
def royaltyChecker(tokenId: uint256):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def royaltyChecker(tokenId: uint256):
def _enforceRoyalties(caller: address, tokenId: uint256):

Copy link
Contributor Author

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?

Copy link
Contributor

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

Comment on lines 120 to 123
# @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

Copy link
Contributor

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!

Suggested change
# @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

Comment on lines 340 to 343
if self.balance < self.lastBalance:
self._deductRoyalties(tokenId)
# equal the contract balance to the lastBalance for future checks
self.lastBalance = self.balance
Copy link
Contributor

@fubuloubu fubuloubu Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)

Copy link
Contributor Author

@victor-ego victor-ego Apr 15, 2023

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.minRoyaltyAmountto 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

Copy link
Contributor Author

@victor-ego victor-ego Apr 15, 2023

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.

Copy link
Contributor

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.minRoyaltyAmountto 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

That's the trick! It'll automatically be 0 because the balance will be physically transferred via send

Copy link
Contributor

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

{{cookiecutter.project_name}}/contracts/NFT.vy Outdated Show resolved Hide resolved
@fubuloubu
Copy link
Contributor

also would have to update any tests where there's a transfer occuring to have value=nft.minRoyaltyAmount()

Comment on lines 114 to 115
# @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
Copy link
Contributor

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

@fubuloubu
Copy link
Contributor

Missing force_royalties in test case JSONs

@victor-ego
Copy link
Contributor Author

also would have to update any tests where there's a transfer occurring to have value=nft.minRoyaltyAmount()

  1. I made minRoyaltyAmount public so that it could be accessed by nft.minRoyaltyAmount()
  2. Passed the tests like such: nft.transferFrom(receiver, owner, 0, value=nft.minRoyaltyAmount(), sender=receiver) -> didn't pass the test
  3. Hardcoded the value on the tests as string "10000000000000000" or as "0.01 ether" without the quotes and the test still doesn't pass.

Copy link
Contributor

@fubuloubu fubuloubu left a 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?

Comment on lines 13 to 14
"force_royalties": "y",
"royalty_percentage_in_decimals": 10.0,
Copy link
Contributor

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

{{cookiecutter.project_name}}/contracts/NFT.vy Outdated Show resolved Hide resolved
return self, max(self.minRoyaltyAmount, royalty)

@internal
def _enforceRoyalties(tokenId: uint256):
Copy link
Contributor

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
Copy link
Contributor

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

Suggested change
return self.owner, royalty
return self, max(self.minRoyaltyAmount, royalty)

Comment on lines 314 to 330
# 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)

Copy link
Contributor

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

Suggested change
# 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)

Comment on lines 425 to 426
@external
@payable
Copy link
Contributor

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

@@ -0,0 +1 @@
{"contractTypes":{},"manifest":"ethpm/3","sources":{}}
Copy link
Contributor

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

Comment on lines 15 to 16
"force_royalties": "y",
"minRoyaltyAmount": 1000000000000000
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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

@fubuloubu
Copy link
Contributor

Can you add .hypothesis/ to gitignore too?

auto-merge was automatically disabled June 7, 2023 11:31

Head branch was pushed to by a user without write access

.coverage
.coverage.*
.cache
.DS_Store
Copy link
Contributor

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__():
Copy link
Contributor

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

.DS_Store Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants