Makes sense, I will try to stick with the OZ standard functionality mainly for interoperability and ease-of-understanding purposes. I think that method of maintaining a custom accountToPower
or something like it will work well.
More Brainstorming
I have an alternative idea which may even less overhead, but I am not sure if it would break any interoperability: instead of a new mapping, we just re-use mapping(address => uint256) _balances
but instead of the # of tokens, it represents the sum'd voting power.
In this way, something close to the default ERC721Votes behavior could be used: _getVotingUnits
returns the balanceOf
still.
_afterTokenTransfer
would be almost the same, instead replacing _transferVotingUnits(from, to, 1);
with _transferVotingUnits(from, to, tokenIdToVotingPower(tokenId));
(which still a separate mapping but only one mapping (uint256 => uint256) tokenIdToVotingPower;
.
In this way, it shifts the power calculation to _mint
, _transfer
, and _burn
to adjust the balance properly, so we will override those. For example, this is the old mint:
function _mint(address to, uint256 tokenId) internal virtual {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_beforeTokenTransfer(address(0), to, tokenId);
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
For our new mint to work, we will track the total number of tokens (tokenCount
) and increment it for a generated tokenId
. This allows our mint to take in a tokenPower
, and assign it to the next avalliable tokenCount
. This gives us:
function _mint(address to, uint256 tokenPower) internal virtual override {
require(to != address(0), "ERC721: mint to the zero address");
require(tokenPower != 0, "ERC721: power must be > 0");
tokenCount++;
uint256 tokenId = tokenCount;
_beforeTokenTransfer(address(0), to, tokenId);
tokenIdToVotingPower[tokenId] = tokenPower;
_balances[to] += tokenPower;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
_afterTokenTransfer(address(0), to, tokenId);
}
(I realize I cannot override the function directly due to the difference in naming tokenId
=> tokenPower
, in a real example I will pass in tokenId
to internally overriden mint that will have some of the storage removed, it will be equivalent to the above though).
The same kind of method should work fine for _transfer
and _burn
, referencing tokenIdToVotingPower
when we add or subtract from the balances map.
I believe this is minimal overhead (only new storage is mapping (uint256 => uint256) tokenIdToVotingPower
and uint256 tokenCount
). But there may be some fundamental flaws that I am not seeing. I'm also not sure how bad it would be to shift how balanceOf
is interpreted. Would the break the kinds of Web3 UIs that try to display total tokens (e.g. Tally)?, or would this actually help to make that instantly interoperable since balanceOf
would normally show voting power in these UIs?
Would absolutely love to get your thoughts @frangio and @JulissaDantes!