I have a preemptive concern on the convenience of nesting NFT-accounts. If I’m not mistaken, right now to move an NFT 3 layers deep my EOA calls my parent wallet (layer 1) which auth’s my EOA owns the NFT, then calls wallet of one of its NFTs (layer 2) which auths that layer 1 wallet owns the inner NFT, which then calls layer 3 wallet, etc.
This seems like a problem on two main fronts:
- encoding the parent call for my EOA goes through N layers of executeCall(to, value, data) which is a pain for the frontend dev building with a nested feature
- this adds meaningful gas overhead for (a) additional calldata for the transaction, (b) many internal transactions hopping down each layer in the ownership tree, (c) storage updates from nonce increments in each layer.
My proposed amendment is to add an additional function specifically designed for executing calls on nested NFTs.
function executeCallNested(address to, uint256 value, bytes data, uint256 depth) external payable returns (bytes memory result) {
address rootOwner = owner();
address tokenContract;
uint256 tokenId;
for (int i = 0; i < depth; i++) {
(_, tokenContract, tokenId) = IERC6551Account(rootOwner).token();
rootOwner = IERC721(tokenContract).ownerOf(tokenId);
}
require(msg.sender == rootOwner, "Not token owner");
bool success;
(success, result) = to.call{value: value}(data);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
This function takes in an additional depth
parameter for how many parents up the contract must loop over to find the root owner and finally checked against msg.sender
. This does validation work upfront that would be performed anyways in absence of this function, but with cheaper view calls, less net calldata, no cascading nonce incrementing, and an obvious interface for developers.
On a similar note, I think it would be more convenient to build gating logic on nested NFT with a view function that returns if an address is an eventual parent of this nested NFT. I don’t think this one should be on the Account spec, maybe as a part of the global registry?
function isEventualParent(address account, address tokenContract, uint256 tokenId) external view (returns bool) {
address owner = IERC721(tokenContract).ownerOf(tokenId);
while (owner != account) {
// no guarantee owner of token is IERC6551Account
// if not, catch and return false
try IERC6551Account(owner).token() returns (uint256 _chainId, address _tokenContract, uint256 _tokenId) {
owner = IERC721(_tokenContract).ownerOf(_tokenId);
} catch {
return false;
}
}
return true;
}
This function will keep going up the ownership tree until it finds the account of interest or hits an address that does not implement the ERC (e.g. EOA, other SCW, address(0)
), therefore erroring out which is caught and triggers a false return.