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!