diff --git a/contracts/LSP10ReceivedVaults/LSP10Utils.sol b/contracts/LSP10ReceivedVaults/LSP10Utils.sol index 3a7546a99..4ac7ff3bc 100644 --- a/contracts/LSP10ReceivedVaults/LSP10Utils.sol +++ b/contracts/LSP10ReceivedVaults/LSP10Utils.sol @@ -14,37 +14,6 @@ import {LSP2Utils} from "../LSP2ERC725YJSONSchema/LSP2Utils.sol"; import "../LSP10ReceivedVaults/LSP10Constants.sol"; import "../LSP9Vault/LSP9Constants.sol"; -/** - * @dev Reverts when the value stored under the 'LSP10ReceivedVaults[]' Array data key is not valid. - * The value stored under this data key should be exactly 16 bytes long. - * - * Only possible valid values are: - * - any valid uint128 values - * _e.g: `0x00000000000000000000000000000000` (zero), meaning empty array, no vaults received._ - * _e.g: `0x00000000000000000000000000000005` (non-zero), meaning 5 array elements, 5 vaults received._ - * - * - `0x` (nothing stored under this data key, equivalent to empty array). - * - * @param invalidValueStored The invalid value stored under the `LSP10ReceivedVaults[]` Array data key. - * @param invalidValueLength The invalid number of bytes stored under the `LSP10ReceivedVaults[]` Array data key (MUST be 16 bytes long). - */ -error InvalidLSP10ReceivedVaultsArrayLength( - bytes invalidValueStored, - uint256 invalidValueLength -); - -/** - * @dev Reverts when the `LSP10Vaults[]` Array reaches its maximum limit (`max(uint128)`). - * @param notRegisteredVault The address of the LSP9Vault that could not be registered. - */ -error MaxLSP10VaultsCountReached(address notRegisteredVault); - -/** - * @dev Reverts when the vault index is superior to `max(uint128)`. - * @param index The vault index. - */ -error VaultIndexSuperiorToUint128(uint256 index); - /** * @title LSP10 Utility library. * @author Yamen Merhi , Jean Cavallera @@ -55,200 +24,158 @@ library LSP10Utils { /** * @dev Generate an array of data keys/values pairs to be set on the receiver address after receiving vaults. * + * @custom:warning Returns empty arrays when encountering errors. Otherwise the arrays must have 3 data keys and 3 data values. + * * @param receiver The address receiving the vault and where the LSP10 data keys should be added. - * @param vault The address of the vault being received. - * @param vaultMapKey The `LSP10VaultMap:` data key of the vault being received containing the interfaceId of the - * vault and its index in the `LSP10Vaults[]` Array. + * @param vaultAddress The address of the vault being received. * - * @return keys An array of 3 x data keys: `LSP10Vaults[]`, `LSP10Vaults[index]` and `LSP10VaultMap:`. - * @return values An array of 3 x data values: the new length of `LSP10Vaults[]`, the address of the asset under `LSP10Vaults[index]` - * and the interfaceId + index stored under `LSP10VaultsMap:`. + * @return lsp10DataKeys An array Data Keys used to update the [LSP-10-ReceivedAssets] data. + * @return lsp10DataValues An array Data Values used to update the [LSP-10-ReceivedAssets] data. */ function generateReceivedVaultKeys( address receiver, - address vault, - bytes32 vaultMapKey - ) internal view returns (bytes32[] memory keys, bytes[] memory values) { - keys = new bytes32[](3); - values = new bytes[](3); - - IERC725Y account = IERC725Y(receiver); - bytes memory encodedArrayLength = getLSP10ReceivedVaultsCount(account); - - // CHECK it's either the first vault received, - // or the storage is already set with a valid `uint128` value - if (encodedArrayLength.length != 0 && encodedArrayLength.length != 16) { - revert InvalidLSP10ReceivedVaultsArrayLength({ - invalidValueStored: encodedArrayLength, - invalidValueLength: encodedArrayLength.length - }); + bytes20 vaultAddress + ) + internal + view + returns (bytes32[] memory lsp10DataKeys, bytes[] memory lsp10DataValues) + { + IERC725Y ERC725YContract = IERC725Y(receiver); + + /// --- Array --- + + bytes memory currentArrayLengthBytes = LSP2Utils.getArrayLength( + ERC725YContract, + _LSP10_VAULTS_ARRAY_KEY + ); + + // invalid `currentArrayLengthBytes` + if (currentArrayLengthBytes.length == 0) { + return (lsp10DataKeys, lsp10DataValues); } - uint128 oldArrayLength = uint128(bytes16(encodedArrayLength)); + // overflow + if (uint128(bytes16(currentArrayLengthBytes)) == type(uint128).max) { + return (lsp10DataKeys, lsp10DataValues); + } + + uint128 currentArrayLength = uint128(bytes16(currentArrayLengthBytes)); + + /// --- Mapping --- - if (oldArrayLength == type(uint128).max) { - revert MaxLSP10VaultsCountReached({notRegisteredVault: vault}); + bytes32 mapKey = LSP2Utils.generateMappingKey( + _LSP10_VAULTS_MAP_KEY_PREFIX, + vaultAddress + ); + + // Query the ERC725Y storage of the LSP0-ERC725Account + bytes memory mapValue = ERC725YContract.getData(mapKey); + + if (mapValue.length != 0) { + return (lsp10DataKeys, lsp10DataValues); } - uint128 newArrayLength = oldArrayLength + 1; + /// --- Data Keys & Values --- - // store the number of received vaults incremented by 1 - keys[0] = _LSP10_VAULTS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(newArrayLength)); + lsp10DataKeys = new bytes32[](3); + lsp10DataValues = new bytes[](3); - // store the address of the vault under the element key in the array - keys[1] = LSP2Utils.generateArrayElementKeyAtIndex( + // Update array length + lsp10DataKeys[0] = _LSP10_VAULTS_ARRAY_KEY; + lsp10DataValues[0] = abi.encodePacked(currentArrayLength + 1); + + // Add element to array + lsp10DataKeys[1] = LSP2Utils.generateArrayElementKeyAtIndex( _LSP10_VAULTS_ARRAY_KEY, - oldArrayLength + currentArrayLength ); - values[1] = bytes.concat(bytes20(vault)); + lsp10DataValues[1] = bytes.concat(vaultAddress); - // store the interfaceId and the location in the array of the asset - // under the LSP5ReceivedAssetMap key - keys[2] = vaultMapKey; - values[2] = bytes.concat(_INTERFACEID_LSP9, bytes16(oldArrayLength)); + // Add value to the mapping + lsp10DataKeys[2] = mapKey; + lsp10DataValues[2] = bytes.concat( + _INTERFACEID_LSP9, + currentArrayLengthBytes + ); } /** * @dev Generate an array of data key/value pairs to be set on the sender address after sending vaults. * + * @custom:warning Returns empty arrays when encountering errors. Otherwise the arrays must have at least 3 data keys and 3 data values. + * * @param sender The address sending the vault and where the LSP10 data keys should be updated. - * @param vaultMapKey The `LSP10VaultMap:` data key of the vault being sent containing the interfaceId of the - * vault and the index in the `LSP10Vaults[]` Array. - * @param vaultIndex The index at which the vault address is stored under `LSP10Vaults[]` Array. + * @param vaultAddress The address of the vault that is being sent. * - * @return keys An array of 3 x data keys: `LSP10Vaults[]`, `LSP10Vaults[index]` and `LSP10VaultsMap:`. - * @return values An array of 3 x data values: the new length of `LSP10Vaults[]`, the address of the asset under `LSP10Vaults[index]` - * and the interfaceId + index stored under `LSP10VaultsMap:`. + * @return lsp10DataKeys An array Data Keys used to update the [LSP-10-ReceivedAssets] data. + * @return lsp10DataValues An array Data Values used to update the [LSP-10-ReceivedAssets] data. */ function generateSentVaultKeys( address sender, - bytes32 vaultMapKey, - uint128 vaultIndex - ) internal view returns (bytes32[] memory keys, bytes[] memory values) { - IERC725Y account = IERC725Y(sender); - bytes memory lsp10VaultsCountValue = getLSP10ReceivedVaultsCount( - account + bytes20 vaultAddress + ) + internal + view + returns (bytes32[] memory lsp10DataKeys, bytes[] memory lsp10DataValues) + { + IERC725Y ERC725YContract = IERC725Y(sender); + + /// --- Array --- + + bytes memory newArrayLengthBytes = LSP2Utils.getDecrementedArrayLength( + ERC725YContract, + _LSP10_VAULTS_ARRAY_KEY ); - if (lsp10VaultsCountValue.length != 16) { - revert InvalidLSP10ReceivedVaultsArrayLength({ - invalidValueStored: lsp10VaultsCountValue, - invalidValueLength: lsp10VaultsCountValue.length - }); + if (newArrayLengthBytes.length == 0) { + return (lsp10DataKeys, lsp10DataValues); } - // Updating the number of the received vaults - uint128 oldArrayLength = uint128(bytes16(lsp10VaultsCountValue)); - - if (oldArrayLength > type(uint128).max) { - revert VaultIndexSuperiorToUint128(oldArrayLength); - } + uint128 newArrayLength = uint128(bytes16(newArrayLengthBytes)); - // Updating the number of the received vaults (decrementing by 1 - uint128 newArrayLength = oldArrayLength - 1; + /// --- Mapping --- - // Generate the element key in the array of the vault - bytes32 vaultInArrayKey = LSP2Utils.generateArrayElementKeyAtIndex( - _LSP10_VAULTS_ARRAY_KEY, - vaultIndex + bytes32 removedElementMapKey = LSP2Utils.generateMappingKey( + _LSP10_VAULTS_MAP_KEY_PREFIX, + vaultAddress ); - // If the asset to remove is the last element in the array - if (vaultIndex == newArrayLength) { - /** - * We will be updating/removing 3 keys: - * - Keys[0]: [Update] The arrayLengthKey to contain the new number of the received vaults - * - Keys[1]: [Remove] The element in arrayKey (Remove the address of the vault sent) - * - Keys[2]: [Remove] The mapKey (Remove the interfaceId and the index of the vault sent) - */ - keys = new bytes32[](3); - values = new bytes[](3); - - // store the number of received vaults decremented by 1 - keys[0] = _LSP10_VAULTS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(newArrayLength)); - - // remove the address of the vault from the element key - keys[1] = vaultInArrayKey; - values[1] = ""; - - // remove the interfaceId and the location in the array of the vault - keys[2] = vaultMapKey; - values[2] = ""; - - // Swapping last element in ArrayKey with the element in ArrayKey to remove || {Swap and pop} method; - // check https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol#L80 - } else if (vaultIndex < newArrayLength) { - /** - * We will be updating/removing 5 keys: - * - Keys[0]: [Update] The arrayLengthKey to contain the new number of the received vaults - * - Keys[1]: [Remove] The mapKey of the vault to remove (Remove the interfaceId and the index of the vault sent) - * - Keys[2]: [Update] The element in arrayKey to remove (Swap with the address of the last element in Array) - * - Keys[3]: [Remove] The last element in arrayKey (Remove (pop) the address of the last element as it's already swapped) - * - Keys[4]: [Update] The mapKey of the last element in array (Update the new index and the interfaceID) - */ - keys = new bytes32[](5); - values = new bytes[](5); - - // store the number of received vaults decremented by 1 - keys[0] = _LSP10_VAULTS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(newArrayLength)); - - // remove the interfaceId and the location in the array of the vault - keys[1] = vaultMapKey; - values[1] = ""; - - // Generate all data Keys/values of the last element in Array to swap - // with data Keys/values of the vault to remove - - // Generate the element key of the last vault in the array - bytes32 lastVaultInArrayKey = LSP2Utils - .generateArrayElementKeyAtIndex( - _LSP10_VAULTS_ARRAY_KEY, - newArrayLength - ); - - // Get the address of the vault from the element key of the last vault in the array - bytes20 lastVaultInArrayAddress = bytes20( - account.getData(lastVaultInArrayKey) - ); + // Query the ERC725Y storage of the LSP0-ERC725Account + bytes memory mapValue = ERC725YContract.getData(removedElementMapKey); - // Generate the map key of the last vault in the array - bytes32 lastVaultInArrayMapKey = LSP2Utils.generateMappingKey( - _LSP10_VAULTS_MAP_KEY_PREFIX, - lastVaultInArrayAddress - ); + if (mapValue.length != 20) { + return (lsp10DataKeys, lsp10DataValues); + } - // Set the address of the last vault instead of the asset to be sent - // under the element data key in the array - keys[2] = vaultInArrayKey; - values[2] = bytes.concat(lastVaultInArrayAddress); + uint128 removedElementIndex = uint128(bytes16(bytes20(mapValue) << 32)); - // Remove the address swapped (last vault in the array) from the last element data key in the array - keys[3] = lastVaultInArrayKey; - values[3] = ""; + bytes32 removedElementIndexKey = LSP2Utils + .generateArrayElementKeyAtIndex( + _LSP10_VAULTS_ARRAY_KEY, + uint128(removedElementIndex) + ); - // Update the index and the interfaceId of the address swapped (last vault in the array) - // to point to the new location in the LSP10Vaults array - keys[4] = lastVaultInArrayMapKey; - values[4] = bytes.concat(_INTERFACEID_LSP9, bytes16(vaultIndex)); + if (removedElementIndex == newArrayLength) { + return + LSP2Utils.removeLastElementFromArrayAndMap( + _LSP10_VAULTS_ARRAY_KEY, + newArrayLength, + removedElementIndexKey, + removedElementMapKey + ); + } else if (removedElementIndex < newArrayLength) { + return + LSP2Utils.removeElementFromArrayAndMap( + ERC725YContract, + _LSP10_VAULTS_ARRAY_KEY, + newArrayLength, + removedElementIndexKey, + removedElementIndex, + removedElementMapKey + ); } else { // If index is bigger than the array length, out of bounds - return (keys, values); + return (lsp10DataKeys, lsp10DataValues); } } - - /** - * @dev Get the total number of vault addresses stored under the `LSP10Vaults[]` Array data key. - * @param account The ERC725Y smart contract to read the storage from. - * @return The raw bytes stored under the `LSP10Vaults[]` data key. - * - * @custom:info This function does not return a number but the raw bytes stored under the `LSP10Vaults[]` Array data key. - */ - function getLSP10ReceivedVaultsCount( - IERC725Y account - ) internal view returns (bytes memory) { - return account.getData(_LSP10_VAULTS_ARRAY_KEY); - } } diff --git a/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol b/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol index f509db2c3..ddd7fccae 100644 --- a/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol +++ b/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol @@ -18,6 +18,20 @@ import { import {LSP1Utils} from "../LSP1Utils.sol"; import {LSP2Utils} from "../../LSP2ERC725YJSONSchema/LSP2Utils.sol"; import {LSP5Utils} from "../../LSP5ReceivedAssets/LSP5Utils.sol"; +import { + _TYPEID_LSP7_TOKENSSENDER, + _TYPEID_LSP7_TOKENSRECIPIENT, + _INTERFACEID_LSP7 +} from "../../LSP7DigitalAsset/LSP7Constants.sol"; +import { + _TYPEID_LSP8_TOKENSSENDER, + _TYPEID_LSP8_TOKENSRECIPIENT, + _INTERFACEID_LSP8 +} from "../../LSP8IdentifiableDigitalAsset/LSP8Constants.sol"; +import { + _TYPEID_LSP9_OwnershipTransferred_SenderNotification, + _TYPEID_LSP9_OwnershipTransferred_RecipientNotification +} from "../../LSP9Vault/LSP9Constants.sol"; import {LSP10Utils} from "../../LSP10ReceivedVaults/LSP10Utils.sol"; // constants @@ -59,182 +73,200 @@ contract LSP1UniversalReceiverDelegateUP is ERC165, ILSP1UniversalReceiver { * - This contract should be allowed to use the {setDataBatch(...)} function in order to update the LSP5 and LSP10 Data Keys. * - Cannot accept native tokens * + * @custom:info + * - If some issues occured with generating the `dataKeys` or `dataValues` the `returnedMessage` will be an error message, otherwise it will be empty. + * - If an error occured when trying to use `setDataBatch(dataKeys,dataValues)`, it will return the raw error data back to the caller. + * * @param typeId Unique identifier for a specific notification. - * @return result The result of the reaction for `typeId`. + * @return returnedMessage The result of the reaction for `typeId`. */ function universalReceiver( bytes32 typeId, bytes memory /* data */ - ) public payable virtual returns (bytes memory result) { - if (msg.value != 0) revert NativeTokensNotAccepted(); + ) public payable virtual returns (bytes memory) { + // CHECK that we did not send any native tokens to the LSP1 Delegate, as it cannot transfer them back. + if (msg.value != 0) { + revert NativeTokensNotAccepted(); + } - // This contract acts like a UniversalReceiverDelegate of an LSP0ERC725Account where we append the - // address and the value, sent to the universalReceiver function of the LSP0, to the msg.data - // Check https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-0-ERC725Account.md#universalreceiver address notifier = address(bytes20(msg.data[msg.data.length - 52:])); - // Get the supposed mapPrefix and interfaceId based on the typeID - ( - bool invalid, - bytes10 mapPrefix, - bytes4 interfaceID, - bool isReceiving - ) = LSP1Utils.getTransferDetails(typeId); - - // If it's a typeId different than LSP7/LSP8/LSP9 typeIds - if (invalid) return "LSP1: typeId out of scope"; - // The notifier is supposed to be either the LSP7 or LSP8 or LSP9 contract // If it's EOA we revert to avoid registering the EOA as asset or vault (spam protection) // solhint-disable avoid-tx-origin - if (notifier == tx.origin) revert CannotRegisterEOAsAsAssets(notifier); - - // Generate the LSP5ReceivedAssetsMap/LSP10VaultsMap based on the prefix and the notifier - bytes32 notifierMapKey = LSP2Utils.generateMappingKey( - mapPrefix, - bytes20(notifier) - ); - - // Query the ERC725Y storage of the LSP0-ERC725Account - bytes memory notifierMapValue = IERC725Y(msg.sender).getData( - notifierMapKey - ); - - bool isMapValueSet = bytes20(notifierMapValue) != bytes20(0); - - if (isReceiving) { - // If the mapValue is set, we assume that all other data keys relevant to the asset/vault - // are registered in the account, we don't need to re register the asset being received - if (isMapValueSet) - return "LSP1: asset received is already registered"; - - return - _whenReceiving(typeId, notifier, notifierMapKey, interfaceID); - } else { - // If the mapValue is not set, we assume that all other data keys relevant to the asset/vault - // are not registered in the account, we cannot remove non-existing data keys for the asset being sent - if (!isMapValueSet) return "LSP1: asset sent is not registered"; - - // if the value under the `LSP5ReceivedAssetsMap:` or `LSP10VaultsMap:` - // is not a valid tuple as `(bytes4,uint128)` - if (notifierMapValue.length < 20) - return "LSP1: asset data corrupted"; - - // Identify where the asset/vault is located in the `LSP5ReceivedAssets[]` / `LSP10Vaults[]` Array - // by extracting the index from the tuple value `(bytes4,uint128)` - // fetched under the `LSP5ReceivedAssetsMap` / `LSP10VaultsMap` data key - uint128 arrayIndex = uint128(uint160(bytes20(notifierMapValue))); - - return _whenSending(typeId, notifier, notifierMapKey, arrayIndex); + if (notifier == tx.origin) { + return "LSP1: Cannot register EOAs as assets"; + } + + if (typeId == _TYPEID_LSP7_TOKENSSENDER) { + return _tokenSender(notifier); + } + + if (typeId == _TYPEID_LSP7_TOKENSRECIPIENT) { + return _tokenRecipient(notifier, _INTERFACEID_LSP7); + } + + if (typeId == _TYPEID_LSP8_TOKENSSENDER) { + return _tokenSender(notifier); + } + + if (typeId == _TYPEID_LSP8_TOKENSRECIPIENT) { + return _tokenRecipient(notifier, _INTERFACEID_LSP8); + } + + if (typeId == _TYPEID_LSP9_OwnershipTransferred_SenderNotification) { + return _vaultSender(notifier); } + + if (typeId == _TYPEID_LSP9_OwnershipTransferred_RecipientNotification) { + return _vaultRecipient(notifier); + } + + return "LSP1: typeId out of scope"; } - // --- Internal functions + /** + * @dev Handler for LSP7 and LSP8 token sender type id. + * + * @custom:info + * - Tries to generate LSP5 data key/value pairs for removing asset from the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP5 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP7 or LSP8 token address. + */ + function _tokenSender(address notifier) internal returns (bytes memory) { + // if the amount sent is not the full balance, then do not update the keys + try ILSP7DigitalAsset(notifier).balanceOf(msg.sender) returns ( + uint256 balance + ) { + if (balance != 0) { + return "LSP1: full balance is not sent"; + } + } catch { + return "LSP1: `balanceOf(address)` function not found"; + } + + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP5Utils + .generateSentAssetKeys(msg.sender, bytes20(notifier)); + + // `generateSentAssetKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP5: Error generating data key/value pairs"; + } + + // Set the LSP5 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); + } /** - * @dev To avoid stack too deep error - * Generate the keys/values of the asset/vault received to set and set them - * on the account depending on the type of the transfer (asset/vault) + * @dev Handler for LSP7 and LSP8 token recipient type id. + * + * @custom:info + * - Tries to generate LSP5 data key/value pairs for adding asset to the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP5 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP7 or LSP8 token address. + * @param interfaceId The LSP7 or LSP8 interface id. */ - function _whenReceiving( - bytes32 typeId, + function _tokenRecipient( address notifier, - bytes32 notifierMapKey, - bytes4 interfaceID - ) internal virtual returns (bytes memory) { - bytes32[] memory dataKeys; - bytes[] memory dataValues; - - // if it's a token transfer (LSP7/LSP8) - if (typeId != _TYPEID_LSP9_OwnershipTransferred_RecipientNotification) { - // CHECK balance only when the Token contract is already deployed, - // not when tokens are being transferred on deployment through the `constructor` - if (notifier.code.length > 0) { - // if the amount sent is 0, then do not update the keys - uint256 balance = ILSP7DigitalAsset(notifier).balanceOf( - msg.sender - ); - if (balance == 0) return "LSP1: balance not updated"; + bytes4 interfaceId + ) internal returns (bytes memory) { + // CHECK balance only when the Token contract is already deployed, + // not when tokens are being transferred on deployment through the `constructor` + if (notifier.code.length > 0) { + // if the amount sent is 0, then do not update the keys + try ILSP7DigitalAsset(notifier).balanceOf(msg.sender) returns ( + uint256 balance + ) { + if (balance == 0) { + return "LSP1: balance not updated"; + } + } catch { + return "LSP1: `balanceOf(address)` function not found"; } + } - (dataKeys, dataValues) = LSP5Utils.generateReceivedAssetKeys( - msg.sender, - notifier, - notifierMapKey, - interfaceID - ); - - // Set the LSP5 generated data keys on the account - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); - return ""; - } else { - (dataKeys, dataValues) = LSP10Utils.generateReceivedVaultKeys( + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP5Utils + .generateReceivedAssetKeys( msg.sender, - notifier, - notifierMapKey + bytes20(notifier), + interfaceId ); - // Set the LSP10 generated data keys on the account - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); - return ""; + // `generateReceivedAssetKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP5: Error generating data key/value pairs"; } + + // Set the LSP5 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); } /** - * @dev To avoid stack too deep error - * Generate the keys/values of the asset/vault sent to set and set them - * on the account depending on the type of the transfer (asset/vault) + * @dev Handler for LSP9 vault sender type id. + * + * @custom:info + * - Tries to generate LSP10 data key/value pairs for removing vault from the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP10 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP9 vault address. */ - function _whenSending( - bytes32 typeId, - address notifier, - bytes32 notifierMapKey, - uint128 arrayIndex - ) internal virtual returns (bytes memory) { - bytes32[] memory dataKeys; - bytes[] memory dataValues; - - // if it's a token transfer (LSP7/LSP8) - if (typeId != _TYPEID_LSP9_OwnershipTransferred_SenderNotification) { - // if the amount sent is not the full balance, then do not update the keys - uint256 balance = ILSP7DigitalAsset(notifier).balanceOf(msg.sender); - if (balance != 0) return "LSP1: full balance is not sent"; - - (dataKeys, dataValues) = LSP5Utils.generateSentAssetKeys( - msg.sender, - notifierMapKey, - arrayIndex - ); + function _vaultSender(address notifier) internal returns (bytes memory) { + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP10Utils + .generateSentVaultKeys(msg.sender, bytes20(notifier)); + + // `generateSentVaultKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP10: Error generating data key/value pairs"; + } - /** - * `generateSentAssetKeys(...)` returns empty arrays in the following cases: - * - the index returned from the data key `notifierMapKey` is bigger than - * the length of the `LSP5ReceivedAssets[]`, meaning, index is out of bounds. - */ - if (dataKeys.length == 0 && dataValues.length == 0) - return "LSP1: asset data corrupted"; + // Set the LSP10 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); + } - // Set the LSP5 generated data keys on the account - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); - return ""; - } else { - (dataKeys, dataValues) = LSP10Utils.generateSentVaultKeys( - msg.sender, - notifierMapKey, - arrayIndex - ); + /** + * @dev Handler for LSP9 vault recipient type id. + * + * @custom:info + * - Tries to generate LSP5 data key/value pairs for adding vault to the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP5 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP9 vault address. + */ + function _vaultRecipient(address notifier) internal returns (bytes memory) { + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP10Utils + .generateReceivedVaultKeys(msg.sender, bytes20(notifier)); + + // `generateReceivedVaultKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP10: Error generating data key/value pairs"; + } - /** - * `generateSentAssetKeys(...)` returns empty arrays in the following cases: - * - the index returned from the data key `notifierMapKey` is bigger than - * the length of the `LSP10Vaults[]`, meaning, index is out of bounds. - */ - if (dataKeys.length == 0 && dataValues.length == 0) - return "LSP1: asset data corrupted"; + // Set the LSP10 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); + } - // Set the LSP10 generated data keys on the account - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); + /** + * @dev Calls `bytes4(keccak256(setDataBatch(bytes32[],bytes[])))` without checking for `bool success`, but it returns all the data back. + * + * @custom:info If an the low-level transaction revert, the returned data will be forwarded. Th contract that uses this function can use the `Address` library to revert with the revert reason. + * + * @param dataKeys Data Keys to be set. + * @param dataValues Data Values to be set. + */ + function _setDataBatchWithoutReverting( + bytes32[] memory dataKeys, + bytes[] memory dataValues + ) internal returns (bytes memory) { + try IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues) { return ""; + } catch (bytes memory errorData) { + return errorData; } } diff --git a/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateVault/LSP1UniversalReceiverDelegateVault.sol b/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateVault/LSP1UniversalReceiverDelegateVault.sol index 84b463ed4..b108d3b10 100644 --- a/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateVault/LSP1UniversalReceiverDelegateVault.sol +++ b/contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateVault/LSP1UniversalReceiverDelegateVault.sol @@ -18,6 +18,16 @@ import {LSP5Utils} from "../../LSP5ReceivedAssets/LSP5Utils.sol"; // constants import "../LSP1Constants.sol"; +import { + _TYPEID_LSP7_TOKENSSENDER, + _TYPEID_LSP7_TOKENSRECIPIENT, + _INTERFACEID_LSP7 +} from "../../LSP7DigitalAsset/LSP7Constants.sol"; +import { + _TYPEID_LSP8_TOKENSSENDER, + _TYPEID_LSP8_TOKENSRECIPIENT, + _INTERFACEID_LSP8 +} from "../../LSP8IdentifiableDigitalAsset/LSP8Constants.sol"; import "../../LSP9Vault/LSP9Constants.sol"; // errors @@ -43,101 +53,146 @@ contract LSP1UniversalReceiverDelegateVault is ERC165, ILSP1UniversalReceiver { * @notice Reacted on received notification with `typeId`. * * @custom:requirements Cannot accept native tokens. + * @custom:info + * - If some issues occured with generating the `dataKeys` or `dataValues` the `returnedMessage` will be an error message, otherwise it will be empty. + * - If an error occured when trying to use `setDataBatch(dataKeys,dataValues)`, it will return the raw error data back to the caller. * * @param typeId Unique identifier for a specific notification. - * @return result The result of the reaction for `typeId`. + * @return returnedMessage The result of the reaction for `typeId`. */ function universalReceiver( bytes32 typeId, bytes memory /* data */ - ) public payable virtual returns (bytes memory result) { - if (msg.value != 0) revert NativeTokensNotAccepted(); - // This contract acts like a UniversalReceiverDelegate of a Vault where we append the - // address and the value, sent to the universalReceiver function of the LSP9, to the msg.data - // Check https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-9-Vault.md#universalreceiver + ) public payable virtual returns (bytes memory) { + // CHECK that we did not send any native tokens to the LSP1 Delegate, as it cannot transfer them back. + if (msg.value != 0) { + revert NativeTokensNotAccepted(); + } + address notifier = address(bytes20(msg.data[msg.data.length - 52:])); - ( - bool invalid, - bytes10 mapPrefix, - bytes4 interfaceID, - bool isReceiving - ) = LSP1Utils.getTransferDetails(typeId); + // The notifier is supposed to be either the LSP7 or LSP8 contract + // If it's EOA we revert to avoid registering the EOA as asset (spam protection) + // solhint-disable avoid-tx-origin + if (notifier == tx.origin) { + return "LSP1: Cannot register EOAs as assets"; + } - if (invalid || interfaceID == _INTERFACEID_LSP9) - return "LSP1: typeId out of scope"; + if (typeId == _TYPEID_LSP7_TOKENSSENDER) { + return _tokenSender(notifier); + } - // solhint-disable avoid-tx-origin - if (notifier == tx.origin) revert CannotRegisterEOAsAsAssets(notifier); - - bytes32 notifierMapKey = LSP2Utils.generateMappingKey( - mapPrefix, - bytes20(notifier) - ); - bytes memory notifierMapValue = IERC725Y(msg.sender).getData( - notifierMapKey - ); - - bytes32[] memory dataKeys; - bytes[] memory dataValues; - - if (isReceiving) { - // if the map value is already set, then do nothing - if (bytes20(notifierMapValue) != bytes20(0)) - return "URD: asset received is already registered"; - - // CHECK balance only when the Token contract is already deployed, - // not when tokens are being transferred on deployment through the `constructor` - if (notifier.code.length > 0) { - // if the amount sent is 0, then do not update the keys - uint256 balance = ILSP7DigitalAsset(notifier).balanceOf( - msg.sender - ); - if (balance == 0) return "LSP1: balance not updated"; - } + if (typeId == _TYPEID_LSP7_TOKENSRECIPIENT) { + return _tokenRecipient(notifier, _INTERFACEID_LSP7); + } - (dataKeys, dataValues) = LSP5Utils.generateReceivedAssetKeys( - msg.sender, - notifier, - notifierMapKey, - interfaceID - ); + if (typeId == _TYPEID_LSP8_TOKENSSENDER) { + return _tokenSender(notifier); + } + + if (typeId == _TYPEID_LSP8_TOKENSRECIPIENT) { + return _tokenRecipient(notifier, _INTERFACEID_LSP8); + } + + return "LSP1: typeId out of scope"; + } + + /** + * @dev Handler for LSP7 and LSP8 token sender type id. + * + * @custom:info + * - Tries to generate LSP5 data key/value pairs for removing asset from the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP5 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP7 or LSP8 token address. + */ + function _tokenSender(address notifier) internal returns (bytes memory) { + // if the amount sent is not the full balance, then do not update the keys + try ILSP7DigitalAsset(notifier).balanceOf(msg.sender) returns ( + uint256 balance + ) { + if (balance != 0) { + return "LSP1: full balance is not sent"; + } + } catch { + return "LSP1: `balanceOf(address)` function not found"; + } - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); - } else { - // if there is no map value for the asset to remove, then do nothing - if (bytes20(notifierMapValue) == bytes20(0)) - return "LSP1: asset sent is not registered"; + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP5Utils + .generateSentAssetKeys(msg.sender, bytes20(notifier)); - // if it's a token transfer (LSP7/LSP8) - uint256 balance = ILSP7DigitalAsset(notifier).balanceOf(msg.sender); - if (balance != 0) return "LSP1: full balance is not sent"; + // `generateSentAssetKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP5: Error generating data key/value pairs"; + } - // if the value under the `LSP5ReceivedAssetsMap:` - // is not a valid tuple as `(bytes4,uint128)` - if (notifierMapValue.length < 20) - return "LSP1: asset data corrupted"; + // Set the LSP5 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); + } - // Identify where the asset is located in the `LSP5ReceivedAssets[]` Array - // by extracting the index from the tuple value `(bytes4,uint128)` - // fetched under the LSP5ReceivedAssetsMap/LSP10VaultsMap data key - uint128 assetIndex = uint128(uint160(bytes20(notifierMapValue))); + /** + * @dev Handler for LSP7 and LSP8 token recipient type id. + * + * @custom:info + * - Tries to generate LSP5 data key/value pairs for adding asset to the ERC725Y storage. + * - Tries to use `setDataBatch(bytes32[],bytes[])` if generated proper LSP5 data key/value pairs. + * - Does not revert. But returns an error message. Use off-chain lib to get even more info. + * + * @param notifier The LSP7 or LSP8 token address. + * @param interfaceId The LSP7 or LSP8 interface id. + */ + function _tokenRecipient( + address notifier, + bytes4 interfaceId + ) internal returns (bytes memory) { + // CHECK balance only when the Token contract is already deployed, + // not when tokens are being transferred on deployment through the `constructor` + if (notifier.code.length > 0) { + // if the amount sent is 0, then do not update the keys + try ILSP7DigitalAsset(notifier).balanceOf(msg.sender) returns ( + uint256 balance + ) { + if (balance == 0) { + return "LSP1: balance not updated"; + } + } catch { + return "LSP1: `balanceOf(address)` function not found"; + } + } - (dataKeys, dataValues) = LSP5Utils.generateSentAssetKeys( + (bytes32[] memory dataKeys, bytes[] memory dataValues) = LSP5Utils + .generateReceivedAssetKeys( msg.sender, - notifierMapKey, - assetIndex + bytes20(notifier), + interfaceId ); - /** - * `generateSentAssetKeys(...)` returns empty arrays in the following cases: - * - the index returned from the data key `notifierMapKey` is bigger than - * the length of the `LSP5ReceivedAssets[]`, meaning, index is out of bounds. - */ - if (dataKeys.length == 0 && dataValues.length == 0) - return "LSP1: asset data corrupted"; + // `generateReceivedAssetKeys(...)` returns empty arrays when encountering errors + if (dataKeys.length == 0 && dataValues.length == 0) { + return "LSP5: Error generating data key/value pairs"; + } + + // Set the LSP5 generated data keys on the account + return _setDataBatchWithoutReverting(dataKeys, dataValues); + } - IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues); + /** + * @dev Calls `bytes4(keccak256(setDataBatch(bytes32[],bytes[])))` without checking for `bool succes`, but it returns all the data back. + * + * @custom:info If an the low-level transaction revert, the returned data will be forwarded. Th contract that uses this function can use the `Address` library to revert with the revert reason. + * + * @param dataKeys Data Keys to be set. + * @param dataValues Data Values to be set. + */ + function _setDataBatchWithoutReverting( + bytes32[] memory dataKeys, + bytes[] memory dataValues + ) internal returns (bytes memory) { + try IERC725Y(msg.sender).setDataBatch(dataKeys, dataValues) { + return ""; + } catch (bytes memory errorData) { + return errorData; } } diff --git a/contracts/LSP1UniversalReceiver/LSP1Utils.sol b/contracts/LSP1UniversalReceiver/LSP1Utils.sol index 05223b548..a20a7a368 100644 --- a/contracts/LSP1UniversalReceiver/LSP1Utils.sol +++ b/contracts/LSP1UniversalReceiver/LSP1Utils.sol @@ -91,56 +91,4 @@ library LSP1Utils { ); return result.length != 0 ? abi.decode(result, (bytes)) : result; } - - /** - * @dev Gets all the transfer details based on the provided `bytes32 typeId`. - * - * @param typeId A `bytes32` unique identifier for a specific action or information. - * - * @return invalid `true` if the `typeId` was not recognised, `false otherwise. - * @return mapPrefix The standard 10 bytes defined in a LSP standard associated with the specific `typeId`. - * @return interfaceId The bytes4 ERC165 interface ID defined in a LSP standard associated with a specific `typeId`. - * @return isReceiving When the typeId relate to LSP7/8 tokens or LSP9 Vaults, describe if the `typeId` relates - * to receiving assets/vaults (`true`), or sending them (`false`). - */ - function getTransferDetails( - bytes32 typeId - ) - internal - pure - returns ( - bool invalid, - bytes10 mapPrefix, - bytes4 interfaceId, - bool isReceiving - ) - { - if ( - typeId == _TYPEID_LSP7_TOKENSSENDER || - typeId == _TYPEID_LSP7_TOKENSRECIPIENT - ) { - mapPrefix = _LSP5_RECEIVED_ASSETS_MAP_KEY_PREFIX; - interfaceId = _INTERFACEID_LSP7; - isReceiving = typeId == _TYPEID_LSP7_TOKENSRECIPIENT ? true : false; - } else if ( - typeId == _TYPEID_LSP8_TOKENSSENDER || - typeId == _TYPEID_LSP8_TOKENSRECIPIENT - ) { - mapPrefix = _LSP5_RECEIVED_ASSETS_MAP_KEY_PREFIX; - interfaceId = _INTERFACEID_LSP8; - isReceiving = typeId == _TYPEID_LSP8_TOKENSRECIPIENT ? true : false; - } else if ( - typeId == _TYPEID_LSP9_OwnershipTransferred_SenderNotification || - typeId == _TYPEID_LSP9_OwnershipTransferred_RecipientNotification - ) { - mapPrefix = _LSP10_VAULTS_MAP_KEY_PREFIX; - interfaceId = _INTERFACEID_LSP9; - isReceiving = (typeId == - _TYPEID_LSP9_OwnershipTransferred_RecipientNotification) - ? true - : false; - } else { - invalid = true; - } - } } diff --git a/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol b/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol index 65440b60f..4045e164e 100644 --- a/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol +++ b/contracts/LSP2ERC725YJSONSchema/LSP2Utils.sol @@ -1,6 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.4; +// interfaces +import { + IERC725Y +} from "@erc725/smart-contracts/contracts/interfaces/IERC725Y.sol"; + // libraries import {BytesLib} from "solidity-bytes-utils/contracts/BytesLib.sol"; @@ -415,4 +420,178 @@ library LSP2Utils { if (pointer == compactBytesArray.length) return true; return false; } + + /** + * @dev Validates plain bytes to be the length of an LSP2 Array + * + * @custom:info If the returned value has a length of 16 bytes, it means the value is valid. Otherwise the retuned value will be `0x` which means that the value is invalid as the length of a LSP2 Array. + * + * @param arrayLength Plain bytes that should be validated. + * + * @return Raw bytes of length 0 (invalid) or 16 (valid). + */ + function validateArrayLength( + bytes memory arrayLength + ) internal pure returns (bytes memory) { + if (arrayLength.length == 16) { + return arrayLength; + } else if (arrayLength.length == 0) { + return bytes.concat(bytes16(0)); + } else { + return ""; + } + } + + /** + * @dev Get validated length for a LSP2 Array + * + * @custom:info If the returned value has a length of 16 bytes, it means the value is valid. Otherwise the retuned value will be `0x` which means that the value is invalid as the length of a LSP2 Array. + * + * @param ERC725YContract An ERC725Y Contract. + * @param arrayKey The data key of key type Array that you want to query. + * + * @return Raw bytes of length 0 (invalid) or 16 (valid). + */ + function getArrayLength( + IERC725Y ERC725YContract, + bytes32 arrayKey + ) internal view returns (bytes memory) { + bytes memory arrayLength = ERC725YContract.getData(arrayKey); + + return validateArrayLength(arrayLength); + } + + /** + * @dev Get validated decremented length for a LSP2 Array. Additionally checks for underflow. + * + * @custom:info If the returned value has a length of 16 bytes, it means the value is valid. Otherwise the retuned value will be `0x` which means that the value is invalid as the length of a LSP2 Array. + * + * @param ERC725YContract An ERC725Y Contract. + * @param arrayKey The data key of key type Array that you want to query. + * + * @return Raw bytes of length 0 (invalid) or 16 (valid). + */ + function getDecrementedArrayLength( + IERC725Y ERC725YContract, + bytes32 arrayKey + ) internal view returns (bytes memory) { + bytes memory arrayLength = getArrayLength(ERC725YContract, arrayKey); + + // invalid `arrayLength` + if (arrayLength.length == 0) { + return ""; + } + + // underflow + if (bytes16(arrayLength) == bytes16(0)) { + return ""; + } + + return bytes.concat(bytes16(uint128(bytes16(arrayLength)) - 1)); + } + + /** + * @dev Generates Data Key/Value pairs for removing the last element from an LSP2 Array and a mapping Data Key. + * + * @param arrayKey The Data Key of Key Type Array. + * @param newArrayLength The new Array Length for the `arrayKey`. + * @param removedElementIndexKey The Data Key of Key Type Array Index for the removed element. + * @param removedElementMapKey The Data Key of a mapping to be removed. + */ + function removeLastElementFromArrayAndMap( + bytes32 arrayKey, + uint128 newArrayLength, + bytes32 removedElementIndexKey, + bytes32 removedElementMapKey + ) + internal + pure + returns (bytes32[] memory dataKeys, bytes[] memory dataValues) + { + dataKeys = new bytes32[](3); + dataValues = new bytes[](3); + + // store the number of received assets decremented by 1 + dataKeys[0] = arrayKey; + dataValues[0] = abi.encodePacked(newArrayLength); + + // remove the data value for the map key of the element + dataKeys[1] = removedElementMapKey; + dataValues[1] = ""; + + // remove the data value for the map key of the element + dataKeys[2] = removedElementIndexKey; + dataValues[2] = ""; + } + + /** + * @dev Generates Data Key/Value pairs for removing an element from an LSP2 Array and a mapping Data Key. + * + * @custom:info The function assumes that the Data Value stored under the mapping Data Key is of length 20 where the last 16 bytes are the index of the element in the array. + * + * @param ERC725YContract The ERC725Y contract. + * @param arrayKey The Data Key of Key Type Array. + * @param newArrayLength The new Array Length for the `arrayKey`. + * @param removedElementIndexKey The Data Key of Key Type Array Index for the removed element. + * @param removedElementIndex the index of the removed element. + * @param removedElementMapKey The Data Key of a mapping to be removed. + */ + function removeElementFromArrayAndMap( + IERC725Y ERC725YContract, + bytes32 arrayKey, + uint128 newArrayLength, + bytes32 removedElementIndexKey, + uint128 removedElementIndex, + bytes32 removedElementMapKey + ) + internal + view + returns (bytes32[] memory dataKeys, bytes[] memory dataValues) + { + dataKeys = new bytes32[](5); + dataValues = new bytes[](5); + + // store the number of received assets decremented by 1 + dataKeys[0] = arrayKey; + dataValues[0] = abi.encodePacked(newArrayLength); + + // remove the data value for the map key of the element + dataKeys[1] = removedElementMapKey; + dataValues[1] = ""; + + // Generate the key of the last element in the array + bytes32 lastElementIndexKey = LSP2Utils.generateArrayElementKeyAtIndex( + arrayKey, + newArrayLength + ); + + // Get the data value from the key of the last element in the array + bytes20 lastElementIndexValue = bytes20( + ERC725YContract.getData(lastElementIndexKey) + ); + + // Set data value of the last element instead of the element from the array that will be removed + dataKeys[2] = removedElementIndexKey; + dataValues[2] = bytes.concat(lastElementIndexValue); + + // Remove the data value for the swapped array element + dataKeys[3] = lastElementIndexKey; + dataValues[3] = ""; + + // Generate mapping key for the swapped array element + bytes32 lastElementMapKey = LSP2Utils.generateMappingKey( + bytes10(removedElementMapKey), + lastElementIndexValue + ); + + // Generate the mapping value for the swapped array element + bytes memory lastElementMapValue = abi.encodePacked( + bytes4(ERC725YContract.getData(lastElementMapKey)), + removedElementIndex + ); + + // Update the map value of the swapped array element to the new index + dataKeys[4] = lastElementMapKey; + dataValues[4] = lastElementMapValue; + } } diff --git a/contracts/LSP5ReceivedAssets/LSP5Utils.sol b/contracts/LSP5ReceivedAssets/LSP5Utils.sol index 5cf9b9f24..770eda3d4 100644 --- a/contracts/LSP5ReceivedAssets/LSP5Utils.sol +++ b/contracts/LSP5ReceivedAssets/LSP5Utils.sol @@ -14,37 +14,6 @@ import {LSP2Utils} from "../LSP2ERC725YJSONSchema/LSP2Utils.sol"; import "../LSP5ReceivedAssets/LSP5Constants.sol"; import "../LSP7DigitalAsset/LSP7Constants.sol"; -/** - * @dev Reverts when the value stored under the 'LSP5ReceivedAssets[]' Array data key is not valid. - * The value stored under this data key should be exactly 16 bytes long. - * - * Only possible valid values are: - * - any valid uint128 values - * _e.g: `0x00000000000000000000000000000000` (zero), empty array, no assets received._ - * _e.g. `0x00000000000000000000000000000005` (non-zero), 5 array elements, 5 assets received._ - * - * - `0x` (nothing stored under this data key, equivalent to empty array) - * - * @param invalidValueStored The invalid value stored under the `LSP5ReceivedAssets[]` Array data key. - * @param invalidValueLength The invalid number of bytes stored under the `LSP5ReceivedAssets[]` data key (MUST be exactly 16 bytes long). - */ -error InvalidLSP5ReceivedAssetsArrayLength( - bytes invalidValueStored, - uint256 invalidValueLength -); - -/** - * @dev Reverts when the `LSP5ReceivedAssets[]` Array reaches its maximum limit (`max(uint128)`). - * @param notRegisteredAsset The address of the asset that could not be registered. - */ -error MaxLSP5ReceivedAssetsCountReached(address notRegisteredAsset); - -/** - * @dev Reverts when the received assets index is superior to `max(uint128)`. - * @param index The received assets index. - */ -error ReceivedAssetsIndexSuperiorToUint128(uint256 index); - /** * @title LSP5 Utility library. * @author Yamen Merhi , Jean Cavallera @@ -55,211 +24,160 @@ library LSP5Utils { /** * @dev Generate an array of data key/value pairs to be set on the receiver address after receiving assets. * + * @custom:warning Returns empty arrays when encountering errors. Otherwise the arrays must have 3 data keys and 3 data values. + * * @param receiver The address receiving the asset and where the LSP5 data keys should be added. - * @param asset The address of the asset being received (_e.g: an LSP7 or LSP8 token_). - * @param assetMapKey The `LSP5ReceivedAssetMap:` data key of the asset being received containing the interfaceId of the - * asset and its index in the `LSP5ReceivedAssets[]` Array. - * @param interfaceID The interfaceID of the asset being received. + * @param assetAddress The address of the asset being received (_e.g: an LSP7 or LSP8 token_). + * @param assetInterfaceId The interfaceID of the asset being received. * - * @return keys An array of 3 x data keys: `LSP5ReceivedAssets[]`, `LSP5ReceivedAssets[index]` and `LSP5ReceivedAssetsMap:`. - * @return values An array of 3 x data values: the new length of `LSP5ReceivedAssets[]`, the address of the asset under `LSP5ReceivedAssets[index]` - * and the interfaceId + index stored under `LSP5ReceivedAssetsMap:`. + * @return lsp5DataKeys An array Data Keys used to update the [LSP-5-ReceivedAssets] data. + * @return lsp5DataValues An array Data Values used to update the [LSP-5-ReceivedAssets] data. */ function generateReceivedAssetKeys( address receiver, - address asset, - bytes32 assetMapKey, - bytes4 interfaceID - ) internal view returns (bytes32[] memory keys, bytes[] memory values) { - keys = new bytes32[](3); - values = new bytes[](3); + bytes20 assetAddress, + bytes4 assetInterfaceId + ) + internal + view + returns (bytes32[] memory lsp5DataKeys, bytes[] memory lsp5DataValues) + { + IERC725Y ERC725YContract = IERC725Y(receiver); + + /// --- Array --- + + bytes memory currentArrayLengthBytes = LSP2Utils.getArrayLength( + ERC725YContract, + _LSP5_RECEIVED_ASSETS_ARRAY_KEY + ); - IERC725Y account = IERC725Y(receiver); - bytes memory encodedArrayLength = getLSP5ReceivedAssetsCount(account); + // invalid `currentArrayLengthBytes` + if (currentArrayLengthBytes.length == 0) { + return (lsp5DataKeys, lsp5DataValues); + } - // CHECK it's either the first asset received, - // or the storage is already set with a valid `uint128` value - if (encodedArrayLength.length != 0 && encodedArrayLength.length != 16) { - revert InvalidLSP5ReceivedAssetsArrayLength({ - invalidValueStored: encodedArrayLength, - invalidValueLength: encodedArrayLength.length - }); + uint128 currentArrayLength = uint128(bytes16(currentArrayLengthBytes)); + + // overflow + if (currentArrayLength == type(uint128).max) { + return (lsp5DataKeys, lsp5DataValues); } - uint128 oldArrayLength = uint128(bytes16(encodedArrayLength)); + /// --- Mapping --- + + bytes32 mapKey = LSP2Utils.generateMappingKey( + _LSP5_RECEIVED_ASSETS_MAP_KEY_PREFIX, + assetAddress + ); + + // Query the ERC725Y storage of the LSP0-ERC725Account + bytes memory mapValue = ERC725YContract.getData(mapKey); - if (oldArrayLength == type(uint128).max) { - revert MaxLSP5ReceivedAssetsCountReached({ - notRegisteredAsset: asset - }); + if (mapValue.length != 0) { + return (lsp5DataKeys, lsp5DataValues); } - // store the number of received assets incremented by 1 - keys[0] = _LSP5_RECEIVED_ASSETS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(oldArrayLength + 1)); + /// --- Data Keys & Values --- + + lsp5DataKeys = new bytes32[](3); + lsp5DataValues = new bytes[](3); + + // Update array length + lsp5DataKeys[0] = _LSP5_RECEIVED_ASSETS_ARRAY_KEY; + lsp5DataValues[0] = abi.encodePacked(currentArrayLength + 1); - // store the address of the asset under the element key in the array - keys[1] = LSP2Utils.generateArrayElementKeyAtIndex( + // Add element to array + lsp5DataKeys[1] = LSP2Utils.generateArrayElementKeyAtIndex( _LSP5_RECEIVED_ASSETS_ARRAY_KEY, - oldArrayLength + currentArrayLength ); - values[1] = bytes.concat(bytes20(asset)); + lsp5DataValues[1] = bytes.concat(assetAddress); - // store the interfaceId and the location in the array of the asset - // under the LSP5ReceivedAssetMap key - keys[2] = assetMapKey; - values[2] = bytes.concat(interfaceID, bytes16(oldArrayLength)); + // Add value to the mapping + lsp5DataKeys[2] = mapKey; + lsp5DataValues[2] = bytes.concat( + assetInterfaceId, + currentArrayLengthBytes + ); } /** - * @dev Generate an array of data key/value pairs to be set on the sender address after sending assets. + * @dev Generate an array of Data Key/Value pairs to be set on the sender address after sending assets. + * + * @custom:warning Returns empty arrays when encountering errors. Otherwise the arrays must have at least 3 data keys and 3 data values. * * @param sender The address sending the asset and where the LSP5 data keys should be updated. - * @param assetMapKey The `LSP5ReceivedAssetMap:` data key of the asset being sent containing the interfaceId of the - * asset and the index in the `LSP5ReceivedAssets[]` Array. - * @param assetIndex The index at which the asset is stored under the `LSP5ReceivedAssets[]` Array. + * @param assetAddress The address of the asset that is being sent. * - * @return keys An array of 3 x data keys: `LSP5ReceivedAssets[]`, `LSP5ReceivedAssets[index]` and `LSP5ReceivedAssetsMap:`. - * @return values An array of 3 x data values: the new length of `LSP5ReceivedAssets[]`, the address of the asset under `LSP5ReceivedAssets[index]` - * and the interfaceId + index stored under `LSP5ReceivedAssetsMap:`. + * @return lsp5DataKeys An array Data Keys used to update the [LSP-5-ReceivedAssets] data. + * @return lsp5DataValues An array Data Values used to update the [LSP-5-ReceivedAssets] data. */ function generateSentAssetKeys( address sender, - bytes32 assetMapKey, - uint128 assetIndex - ) internal view returns (bytes32[] memory keys, bytes[] memory values) { - IERC725Y account = IERC725Y(sender); - bytes memory lsp5ReceivedAssetsCountValue = getLSP5ReceivedAssetsCount( - account + bytes20 assetAddress + ) + internal + view + returns (bytes32[] memory lsp5DataKeys, bytes[] memory lsp5DataValues) + { + IERC725Y ERC725YContract = IERC725Y(sender); + + /// --- Array --- + + bytes memory newArrayLengthBytes = LSP2Utils.getDecrementedArrayLength( + ERC725YContract, + _LSP5_RECEIVED_ASSETS_ARRAY_KEY ); - if (lsp5ReceivedAssetsCountValue.length != 16) { - revert InvalidLSP5ReceivedAssetsArrayLength({ - invalidValueStored: lsp5ReceivedAssetsCountValue, - invalidValueLength: lsp5ReceivedAssetsCountValue.length - }); + if (newArrayLengthBytes.length == 0) { + return (lsp5DataKeys, lsp5DataValues); } - uint128 oldArrayLength = uint128(bytes16(lsp5ReceivedAssetsCountValue)); + uint128 newArrayLength = uint128(bytes16(newArrayLengthBytes)); - // Updating the number of the received assets (decrementing by 1 - uint128 newArrayLength = oldArrayLength - 1; + /// --- Mapping --- - // Generate the element key in the array of the asset - bytes32 assetInArrayKey = LSP2Utils.generateArrayElementKeyAtIndex( - _LSP5_RECEIVED_ASSETS_ARRAY_KEY, - assetIndex + bytes32 removedElementMapKey = LSP2Utils.generateMappingKey( + _LSP5_RECEIVED_ASSETS_MAP_KEY_PREFIX, + assetAddress ); - // If the asset to remove is the last element in the array - if (assetIndex == newArrayLength) { - /** - * We will be updating/removing 3 keys: - * - Keys[0]: [Update] The arrayLengthKey to contain the new number of the received assets - * - Keys[1]: [Remove] The element in arrayKey (Remove the address of the asset sent) - * - Keys[2]: [Remove] The mapKey (Remove the interfaceId and the index of the asset sent) - */ - keys = new bytes32[](3); - values = new bytes[](3); - - // store the number of received assets decremented by 1 - keys[0] = _LSP5_RECEIVED_ASSETS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(newArrayLength)); - - // remove the address of the asset from the element key - keys[1] = assetInArrayKey; - values[1] = ""; + // Query the ERC725Y storage of the LSP0-ERC725Account + bytes memory mapValue = ERC725YContract.getData(removedElementMapKey); - // remove the interfaceId and the location in the array of the asset - keys[2] = assetMapKey; - values[2] = ""; - - // Swapping last element in ArrayKey with the element in ArrayKey to remove || {Swap and pop} method; - // check https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol#L80 - } else if (assetIndex < newArrayLength) { - /** - * We will be updating/removing 5 keys: - * - Keys[0]: [Update] The arrayLengthKey to contain the new number of the received assets - * - Keys[1]: [Remove] The mapKey of the asset to remove (Remove the interfaceId and the index of the asset sent) - * - Keys[2]: [Update] The element in arrayKey to remove (Swap with the address of the last element in Array) - * - Keys[3]: [Remove] The last element in arrayKey (Remove (pop) the address of the last element as it's already swapped) - * - Keys[4]: [Update] The mapKey of the last element in array (Update the new index and the interfaceID) - */ - keys = new bytes32[](5); - values = new bytes[](5); - - // store the number of received assets decremented by 1 - keys[0] = _LSP5_RECEIVED_ASSETS_ARRAY_KEY; - values[0] = bytes.concat(bytes16(newArrayLength)); - - // remove the interfaceId and the location in the array of the asset - keys[1] = assetMapKey; - values[1] = ""; + if (mapValue.length != 20) { + return (lsp5DataKeys, lsp5DataValues); + } - if (newArrayLength >= type(uint128).max) { - revert ReceivedAssetsIndexSuperiorToUint128(newArrayLength); - } + uint128 removedElementIndex = uint128(bytes16(bytes20(mapValue) << 32)); - // Generate all data Keys/values of the last element in Array to swap - // with data Keys/values of the asset to remove + bytes32 removedElementIndexKey = LSP2Utils + .generateArrayElementKeyAtIndex( + _LSP5_RECEIVED_ASSETS_ARRAY_KEY, + removedElementIndex + ); - // Generate the element key of the last asset in the array - bytes32 lastAssetInArrayKey = LSP2Utils - .generateArrayElementKeyAtIndex( + if (removedElementIndex == newArrayLength) { + return + LSP2Utils.removeLastElementFromArrayAndMap( _LSP5_RECEIVED_ASSETS_ARRAY_KEY, - newArrayLength + newArrayLength, + removedElementIndexKey, + removedElementMapKey + ); + } else if (removedElementIndex < newArrayLength) { + return + LSP2Utils.removeElementFromArrayAndMap( + ERC725YContract, + _LSP5_RECEIVED_ASSETS_ARRAY_KEY, + newArrayLength, + removedElementIndexKey, + removedElementIndex, + removedElementMapKey ); - - // Get the address of the asset from the element key of the last asset in the array - bytes20 lastAssetInArrayAddress = bytes20( - account.getData(lastAssetInArrayKey) - ); - - // Generate the map key of the last asset in the array - bytes32 lastAssetInArrayMapKey = LSP2Utils.generateMappingKey( - _LSP5_RECEIVED_ASSETS_MAP_KEY_PREFIX, - lastAssetInArrayAddress - ); - - // Get the interfaceId and the location in the array of the last asset - bytes memory lastAssetInterfaceIdAndIndex = account.getData( - lastAssetInArrayMapKey - ); - bytes memory interfaceID = BytesLib.slice( - lastAssetInterfaceIdAndIndex, - 0, - 4 - ); - - // Set the address of the last asset instead of the asset to be sent - // under the element data key in the array - keys[2] = assetInArrayKey; - values[2] = bytes.concat(lastAssetInArrayAddress); - - // Remove the address swapped from the last element data key in the array - keys[3] = lastAssetInArrayKey; - values[3] = ""; - - // Update the index and the interfaceId of the address swapped (last element in the array) - // to point to the new location in the LSP5ReceivedAssets array - keys[4] = lastAssetInArrayMapKey; - values[4] = bytes.concat(interfaceID, bytes16(assetIndex)); } else { // If index is bigger than the array length, out of bounds - return (keys, values); + return (lsp5DataKeys, lsp5DataValues); } } - - /** - * @dev Get the total number of asset addresses stored under the `LSP5ReceivedAssets[]` Array data key. - * @param account The ERC725Y smart contract to read the storage from. - * @return The raw bytes stored under the `LSP5ReceivedAssets[]` data key. - * - * @custom:info This function does not return a number but the raw bytes stored under the `LSP5ReceivedAssets[]` Array data key. - */ - function getLSP5ReceivedAssetsCount( - IERC725Y account - ) internal view returns (bytes memory) { - return account.getData(_LSP5_RECEIVED_ASSETS_ARRAY_KEY); - } } diff --git a/contracts/Mocks/GenericExecutorWithBalanceOfFunction.sol b/contracts/Mocks/GenericExecutorWithBalanceOfFunction.sol new file mode 100644 index 000000000..17f477694 --- /dev/null +++ b/contracts/Mocks/GenericExecutorWithBalanceOfFunction.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/** + * @dev This contract is used only for testing purposes + */ +contract GenericExecutorWithBalanceOfFunction { + function call( + address target, + uint256 value, + bytes memory data + ) public returns (bytes memory result) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, bytes memory returnData) = target.call{value: value}( + data + ); + result = _verifyCallResult(success, returnData, "Unknown Error"); + } + + function balanceOf(address) external pure returns (uint256) { + return 1; + } + + function _verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + _revert(returndata, errorMessage); + } + } + + function _revert( + bytes memory returndata, + string memory errorMessage + ) private pure { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + // solhint-disable-next-line no-inline-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } +}