Provides creators and users with future-proof tools and standards to unleash their creative force in an open interoperable ecosystem.
Platform: Code4rena
Start Date: 30/06/2023
End Date: 14/07/2023
Period: 14 days
Status: Completed
Pot Size: $100,000 USDC
Participants: 22
Reporter: thebrittfactor
Judge: Trust
Id: 253
League: ETH
MiloTruck | 1/22 | $44,459.52 | 7 | 0 | 0 | 6 | 4 | Grade A | 0 | 0 |
codegpt | 2/22 | $12,360.37 | 2 | 0 | 0 | 2 | 1 | 0 | 0 | 0 |
0xc695 | 3/22 | $9,181.99 | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 0 |
gpersoon | 4/22 | $6,244.37 | 3 | 0 | 0 | 1 | 0 | Grade A | 0 | Grade A |
K42 | 5/22 | $2,034.26 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | Grade A |
Rolezn | 6/22 | $714.09 | 2 | 0 | 0 | 0 | 0 | Grade A | Grade A | 0 |
catellatech | 7/22 | $686.13 | 2 | 0 | 0 | 0 | 0 | Grade B | 0 | Grade B |
DavidGiladi | 8/22 | $454.76 | 2 | 0 | 0 | 0 | 0 | Grade A | Grade B | 0 |
Raihan | 9/22 | $380.65 | 1 | 0 | 0 | 0 | 0 | 0 | Grade A | 0 |
LeoS | 10/22 | $292.81 | 1 | 0 | 0 | 0 | 0 | 0 | Grade A | 0 |
Auditor per page
LSP0ERC725Account.sol
LSP1UniversalReceiverDelegateUP.sol
LSP6KeyManager.sol
LSP7DigitalAsset.sol
LSP7CompatibleERC20.sol
and LSP7CompatibleERC20InitAbstract.sol
LSP8IdentifiableDigitalAsset.sol
LSP14Ownable2Step.sol
LSP17Extendable.sol
LSP20CallVerification.sol
Automated findings output for the audit can be found here.
Note for C4 wardens: Anything included in the automated findings output is considered a publicly known issue and is ineligible for awards.
The current contracts have gone through multiple audits and formal verification previous to the contest. You can find all the previous audits reports under the ./audits
folder.
Any issue mentioned in the report listed under the ./audits
folder MUST be considered as a known issue.
No constructor in OwnableUnset.sol
and LSP14Ownable2Step.sol
. We cannot add a constructor at the moment since these 2 contracts are shared currently between the standard and proxy version (with initialize(...)). Once we have the lsp-smart-contract-upgradeable
repo, we will add a constructor in the standard version and an initialize(...)
function in the Init version.
The contracts are using supportsERC165InterfaceUnchecked
to check for support of a single interfaceId for gas cost optimisation. It does not conform to the ERC165 standard but we do this out of gas optimisation as our implementations do a lot of external calls to check for interfaces IDs.
LSP0ERC725Account.sol
The effect of using msg.value
with operation type DELEGATECALL in execute(…)
functions is known. Similar to the issue mentioned in Uniswap V3 Periphery.
When the owner of the LSP0 is an EOA, if a caller calls the protected functions:
lsp20VerifyCall(...)
will pass (because it is a low level call, even if it is calling an EOA owner).A potential collision can happen in the universalReceiver(..)
function when 2 bytes32 typeId
s start with the same 20 bytes. See Trust audit report finding M2 for more details.
The UniversalReceiverDelegate of the receiver can consume a lot of gas, making the caller who initiated the transfer pay a lot in gas fees.
You can have delegate call with selfdestruct that will bypass the second lsp20 check (lsp20VerifyCallResult(…)
). Mentioned in Trust audit report, see finding M3 for more details.
LSP1UniversalReceiverDelegateUP.sol
universalReceiver(...)
function. For instance, by:
balanceOf(…)
function for LSP7 or LSP8 assets transfer.The caller will, however, have to pay for the gas of spamming the account.
0
as an amount and that it calls the universalReceiver(...)
function of the sender and recipient.The reason is we want to allow to react on the data
parameter, for instance.
Example 1:
- Do
Vault.execute(…)
(from ERC725X)- → the data payload would be the
universalReceiver(…)
function of the UP user you want to spam, passing the righttypeId
for VaultTransferRecipient.
Example 2:
On the Vault, under the LSP1Delegate address, put the address of the UP user as a LSP1Delegate you want to spam.
LSP6KeyManager.sol
The executeBatch(..)
function (from ERC725X) is not yet supported in the KeyManager as a path for execution.
The relayer can choose the amount of gas provided when interacting with the executeRelayCall(...)
functions. For more details, see Trust audit report finding L3.
The overlapping issue between the two permissions ADDCONTROLLER
/ EDITPERMISSIONS
is known. For instance:
ADDCONTROLLER
: You can create a new wallet address you control and give it all the permissions via ADDCONTROLLER
.EDITPERMISSIONS
: You can grant yourself all the permissions and take control of the account (you can also grant yourself ADDCONTROLLER
, and create a new wallet that you control).These two permissions are separated for legal reasons. In some implementations or use cases, applications or protocols might require giving a controller only one of the two permissions, not the other (and vice versa).
It is possible to execute some code in the receive/fallback functions of the recipient by only having the permission transferValue/SuperTransferValue.
It is not possible to call LSP17 extensions through the KeyManager.
Possibility to lock the account by setting the KeyManager address as extension of lsp20VerifyCall
selector.
Failed relay calls (via executeRelayCall(…)
don’t increase the nonce). Therefore if one would pre-sign 3 transactions in one channel and the first one is failing, one would have to re-sign the next 2 transactions with a different nonce in order to execute them. Another solution would be signing all 3 transactions in 3 different channels. See first audit report from Watchpug finding M3 for details.
REENTRANCY
permission is checked for the contract that reenters the KeyManager or for the signer if the reentrant call happens through executeRelayCall(..)
& executeRelayCallBatch(..)
. Initiator of the call doesn’t need to have REENTRANCY
permission.
LSP7DigitalAsset.sol
authorizeOperator(..)
CAN NOT avoid front-running and Allowance Double-Spend Exploit. This can be avoided by using the increaseAllowance(..)
and decreaseAllowance(..)
functions.
We are aware that the transferBatch(...)
function could be optimized for gas. For instance for scenarios where the balance of the sender (if it’s the same from address of every iterations) can be updated once instead of on every iterations (to avoid multiple storage writes). Same for operator allowances.
LSP7CompatibleERC20.sol
and LSP7CompatibleERC20InitAbstract.sol
LSP7DigitalAssetCore.sol
includes the non-standard functions increaseAllowance
and decreaseAllowance
to mitigate the issues around double spend exploit. In @openzeppelin/contracts
, the ERC20 implementation of these two functions returns a boolean true
.
We do not return a boolean in LSP7 because we want to stay consistent with the other functions from the LSP7 interface (like authorizeOperator(...)
and transfer(...)
) that do not return anything, as defined in the ILSP7DigitalAsset.sol
interface.
In LSP7CompatibleERC20
and LSP7CompatibleERC20InitAbstract
, we cannot override the LSP7 function to return a boolean in these two contracts, because we cannot override the function definitions "from returning nothing to returning something" (the Solidity compiler does not allow this).
We are currently aware of this issue being not completely in-line with ERC20 in LSP7CompatibleERC20
and LSP7CompatibleERC20InitAbstract
.
One way we are planning to mitigate this issue is by adding an assembly block that returns a true
boolean in these two functions in these two contracts. This will not change anything if these function are interacted with the Solidity function call syntax (e.g: LSP7CompatibleERC20.increaseAllowance(operatorAddress, 10 ether)
), but if interacted via low level call, it will enable to decode the returned data as a bool
and process it, as shown below:
bytes memory increaseAllowanceCalldata = abi.encodeWithSelector( this.increaseAllowance.selector, operatorAddress, 10 ether ); (bool success, bytes memory result) = address( lsp7CompatibleContractAddress ).call(increaseAllowanceCalldata); // if the external call completed successfully if (success) { // decode the `result` as a boolean and ensure it is `true` bool booleanReturned = abi.decode(result, (bool)); require(booleanReturned == true); }
LSP8IdentifiableDigitalAsset.sol
transferBatch(...)
function could be optimized for gas. For instance for scenarios where the balance of the sender (if it’s the same from address of every iterations) can be updated once instead of on every iterations (to avoid multiple storage writes). Same for operator allowances.LSP14Ownable2Step.sol
acceptOwnership(...)
, if the current owner is a contract that implements LSP1, the current owner can block the new owner from accepting ownership by reverting in its universalReceiver(..)
function (the current owner’s UniversalReceiver function).LSP17Extendable.sol
msg.sender
(eg: tokens transfer) is dangerous.LSP20CallVerification.sol
Any known issues from Slither for each contract are listed under the slither/
folder in this repository. We encourage reporting any bugs around them and not just the errors on their own. Slither errors without some proven negative impact will be considered as known issues.
An ERC725 smart contract is composed of two main components:
ERC725X
: a generic executor contract via an execute(...)
and executeBatch()
functions.ERC725Y
: a generic data key value store interacted via setData(...)
and setDataBatch(...)
.The ERC725 smart contracts represent the foundation behind most of the LSP smart contracts, from the LSP0 ERC725 Account to the LSP4 Digital Asset Metadata. You can find the ERC725 smart contracts implementation in Solidity under the submodules/ERC725
folder.
LSP0ERC725Account is an advanced smart contract-based account that offers a comprehensive range of essential features. It is composed of multiple and standards and modules:
bytes32 => bytes
data key-value store (ERC725Y) as smart contract storageuniversalReceiver(bytes32,bytes)
) to be notified about different actions and information, such as token transfers, followers, information, etc...LSP1UniversalReceiver is designed to facilitate a universally standardized way of receiving notifications about various actions, such as token transfers, new followers, or updated information. This standard's core function named universalReceiver(bytes32,bytes)
operates as a standard notification gateway. It standardizes the process of emitting data received, creating for the contract implementing LSP1 a standard gateway to be notified about various information, such as which tokens you received and own or which followers accounts started following you.
LSP1UniversalReceiver also standardizes an optional extension (LSP1UniversalReceiverDelegate, see details below) that enables to define complex logic and custom reactivity depending on the bytes32 typeId
received by the initial universalReceiver(bytes32,bytes)
function.
LSP1UniversalReceiverDelegate standard formalize the procedure of reacting to specific actions. This standard is typically implemented once the universalReceiver(..)
function is invoked.
The universalReceiver(..)
function is called with a unique bytes32 typeId
identifier. Subsequently, the universalReceiver(...)
function forwards the call, along with the sender's address and the value sent, to the UniversalReceiverDelegate. The UniversalReceiverDelegate, in its role, identifies the bytes32
as a specific action and performs a designated response. For instance, if a token transfer is recognized (represented by a unique bytes32 typeId
like LSP7Tokens_RecipientNotification
), the UniversalReceiverDelegate could contain logic that triggers a specific response, for instance reverting the entire transaction if the token received is a spam token known from a public blacklist registry.
The UniversalReceiverDelegate address can be changed in the contract implementing the universalReceiver(..)
function. Also there could be the case where multiple UniversalReceiverDelegates exist.
LSP6KeyManager is a smart contract that acts as a controller for another contract it is linked to (a smart contract-based account, a token contract, etc...). It enables the linked contract to be controlled by multiple addresses. Such addresses, called "controllers", can be granted different permissions defined by the LSP6 standard that allow them to perform different types of actions, including setting data on the ERC725Y storage of the linked account or using the linked account to interact with other addresses on the network (transferring LYX, interact with tokens or any other smart contracts, etc...).
LSP6 enables meta transactions in its interface via the executeRelayCall(...)
function, where any executor address can dispatch transactions signed by another controller, and pay the gas fees on behalf of this controller. Finally, the LSP6KeyManager also allows batching transactions via executeBatch(...)
and executeRelayCallBatch(...)
The new token standards on LUKSO share the following similarities:
transfer(...)
function (only difference is the 3rd parameter where LSP7 takes a uint256 amount
while LSP8 takes a bytes32 tokenId
).bool allowNonLSP1Recipient
in the transfer(...)
function.LSP4DigitalAssetMetadata is a metadata standard that defines metadata keys to store information related to a digital asset inside its ERC725Y storage, including the token name (LSP4TokenName
) and its symbol (LSP4TokenSymbol
). It also defines a standard JSON structure that can contain information describing the asset. Such information can be anything like the description of the asset, an icon for the digital asset, links to find out more (e.g: website, ...), or any additional custom attributes related to the asset or the NFT. Finally, it defines a metadata key that can contain the list of creator addresses for this asset (LSP4Creators[]
).
LSP7DigitalAsset is a standard that defines a fungible token, meaning tokens that are mutually interchangeable (one token has the same value as another token). Like ERC20, tokens can be transferred in quantities. The token holder can transfer multiple tokens by specifying an uint256 amount
when using the transfer(...)
function. By default, LSP7 Digital Assets are divisible like fiat currencies, meaning 1 token can be divided into smaller units (e.g: 1/10th of a token), with their decimals()
set to 18 by default. However, LSP7 includes a feature that enables to specify the token as "non divisible" via the isNonDivisible
parameter on deployment.
LSP8IdentifiableDigitalAsset is a standard that defines a non-fungible token, meaning tokens that are unique and distinguishable from each other (one token cannot be replaced by another token). Each token is uniquely represented by its bytes32 tokenId
, and can be transferred via the transfer(...)
function, where the tokenId
is given as the 3rd parameter of the transfer(...)
function.
LSP8 includes a feature to define the type of token ID through the LSP8TokenIdType
metadata key. Token ID type varies from simple to complex, for instance:
LSP8TokenIdType == 1
).LSP8TokenIdType == 2
).string
for unique NFT names (LSP8TokenIdType == 5
).LSP14Ownable2Step is an advanced ownership module designed to give a more precise and safer way to manage contract ownership. It introduces a crucial feature of two-step processes for ownership transfer and renouncement, significantly reducing the likelihood of accidental or unauthorized changes to the contract's ownership (e.g: transferring ownership to an address to an address where the new owner has shortly before the process lost access to its private key). This enhanced security mechanism ensures that ownership actions require deliberate and careful confirmation, minimizing the risk of unintended transfers or renouncements. Using a two-step process where the new owner has to accept ownership ensures that the contract is always owned by an address that has control over it since the new owner explicitly accepts ownership, proving that it has control over its address.
LSP17ContractExtension is designed to extend a contract's functionality post-deployment. Once a contract with a set of functions is deployed on the blockchain, it becomes immutable, meaning no additional functions can be added after deployment.
The LSP17ContractExtension standard provides a solution to this limitation. It does this by forwarding the call to an extension contract through the fallback
function instead of leading to a revert due to the invocation of an undefined function. This forwarding mechanism allows the contract to be extended and to add functionality after it has been deployed. The standard could be beneficial for contracts that should support standards and functions that get standardized and discussed in the future.
LSP20CallVerification is an innovative module that simplifies access control rules verification within smart contracts. By implementing a standardized approach, this module enables seamless validation of whether an address possesses the necessary permissions to initiate a specific call.
LSP20 aims to reduce this complexity for application and protocol developers, who need to resolve the contract owner first and then have to interact through the owner in order to call any function on the actual targeted contract. This module embedded in a contract through inheritance enables to abstract away this complexity. Applications and other contracts can now interact directly with the targeted contract, and LSP20 will forward in the background any ownership verification logic in the background, by forwarding the call to the owner and returning the verification result in the form of a magicValue
.
📹 Watch the following video for an overview of the LSP Smart Contracts repository.
<a href="https://www.youtube.com/watch?v=E8Ih5n7auKY" target="_blank"> <img src="https://img.youtube.com/vi/E8Ih5n7auKY/mqdefault.jpg" alt="Watch the video" width="240" height="180" border="10" /> </a>Here are some examples of issues that we are primarily concerned about:
LSP1UniversalReceiverDelegateUP
will be set as a default UniversalReceiverDelegate for the UniversalProfiles deployed by the LUKSO foundation, if there is a potential risk to deploy just 1 UniversalReceiverDelegate contract and grant it SUPER_SETDATA
and REENTRANCY
permission on all of these UniversalProfiles?Scope | lines of code | 📄 Standard Specifications | 📚 Documentation |
---|---|---|---|
ERC725 | 503 | ERC-725 | ERC725 |
LSP0ERC725Account | 532 | LSP-0-ERC725Account | LSP0ERC725Account |
UniversalProfile | 47 | LSP-3-UniversalProfile-Metadata | UniversalProfile |
LSP1UniversalReceiverDelegate | 252 | LSP-1-UniversalReceiver | LSP1UniversalReceiver & LSP1UniversalReceiverDelegate |
LSP4DigitalAssetMetadata | 116 | LSP-4-DigitalAsset-Metadata | LSP4DigitalAssetMetadata |
LSP6KeyManager | 1439 | LSP-6-KeyManager | LSP6KeyManager |
LSP7DigitalAsset | 860 | LSP-7-DigitalAsset | LSP7DigitalAsset |
LSP8IdentifiableDigitalAsset | 1233 | LSP-8-IdentifiableDigitalAsset | LSP8IdentifiableDigitalAsset |
LSP14Ownable2Step | 119 | LSP-14-Ownable2Step | LSP14Ownable2Step |
LSP17ContractExtension | 103 | LSP-17-ContractExtension | LSP17ContractExtension |
LSP20CallVerification | 81 | LSP-20-CallVerification | LSP20CallVerification |
LSP2Utils | 51 | LSP-2-ERC725YJSONSchema | LSP2ERC725YJSONSchema |
LSP5Utils | 126 | LSP-4-DigitalAsset-Metadata | LSP5ReceivedVaults |
LSP10Utils | 116 | LSP-10-ReceivedVaults | LSP10ReceivedVaults |
SUM | 5578 |
Below is the complete list of the contracts in scope for this contest.
There are 3 types of contracts per LSP standard:
Core
contracts: contain the core implementation logic of the LSP standard.Example: LSP7DigitalAssetCore.sol
contains the core logic of the LSP7 standard.
constructor(...)
. They are written with the same name as the standard.Example: LSP0ERC725Account.sol
.
Init
contracts: proxy version of the LSP standard, to be used as base contracts behind proxies. Should be initialized via the public initialize(...)
function.Example: LSP0ERC725AccountInit.sol
InitAbstract
contracts: same as Init
but without a public initialize(...)
function. To be inherited for customization.Example: LSP6KeyManagerInitAbstract.sol
Note: the separation between
Core
,Init
andInitAbstract
does not apply to LSP14, LSP17 and LSP20.
Contract | SLOC | Purpose | Libraries used |
---|---|---|---|
ERC725 | These set of contracts are available as a submodules under the submodules/ERC725/implementations folder. | ||
ERC725XCore.sol | 194 | Core implementation logic of the ERC725X sub-standard, a generic executor contract. | @openzeppelin/contracts : [Create2.sol , Address.sol ], solidity-bytes-utils : BytesLib.sol |
ERC725YCore.sol | 71 | Core implementation logic of the ERC725Y sub-standard, a generic data key-value store that uses a mapping of bytes32 data keys mapped to their bytes value. | |
OwnableUnset.sol | 35 | Modified version of Ownable contract with an internal _setOwner(address) function that prevents setting the owner twice when both ERC725X and ERC725Y are inherited (e.g: in ERC725.sol ) | |
ERC725InitAbstract.sol | 30 | Abstract proxy version without a public initialize(...) function, that bundles ERC725X and ERC725Y together into one smart contract. | |
IERC725X.sol | 28 | Interface that describe the standard functions and events defined in the ERC725X sub-standard. | |
ERC725.sol | 23 | Standard version that bundles ERC725X and ERC725Y together into one smart contract. | |
IERC725Y.sol | 16 | Interface that describe the standard functions and events defined in the ERC725Y sub-standard. | |
ERC725XInitAbstract.sol | 15 | Abstract Proxy version of the ERC725X sub-standard without a public initialize(...) function. | |
ERC725YInitAbstract.sol | 15 | Abstract Proxy version of the ERC725Y sub-standard without a public initialize(...) function. | |
ERC725Y.sol | 13 | Standard version of the ERC725Y sub-standard. | |
errors.sol | 13 | List of custom errors related to the internal logic of ERC725XCore.sol and ERC725YCore.sol . | |
ERC725X.sol | 12 | Standard version of the ERC725X sub-standard. | |
ERC725Init.sol | 10 | Proxy version that bundles ERC725X and ERC725Y together into one smart contract. | |
ERC725XInit.sol | 10 | Proxy version of the ERC725X sub-standard. | |
ERC725YInit.sol | 10 | Proxy version of the ERC725Y sub-standard. | |
constants.sol | 8 | ERC165 interface IDs for ERC725X and ERC725Y + list of operation types defined in the ERC725X sub-standard. | |
LSP0ERC725Account | |||
LSP0ERC725AccountCore.sol | 434 | Core implementation of the LSP0 standard, a smart contract based account. | @openzeppelin/contracts : [ECDSA.sol , ERC165Checker.sol , Address.sol ] submodules/ERC725/implementations/contracts/ : [ERC725YCore.sol , ERC725XCore.sol , [custom/OwnableUnset.sol ], constants.sol ] |
LSP0Utils.sol | 23 | Utility functions to query metadata keys from the ERC725Y storage of an LSP0ERC725Account. | |
LSP0ERC725AccountInitAbstract.sol | 21 | Abstract Proxy version of the LSP0 standard without a public initialize(...) function. | @openzeppelin/contracts-upgradeable : Initializable.sol |
ILSP0ERC725Account.sol | 19 | Interface that describes the standard functions and events defined in the LSP0 standard. | |
LSP0ERC725Account.sol | 13 | Standard version of the LSP0 standard. | |
LSP0ERC725AccountInit.sol | 14 | Proxy version of the LSP0 standard. | |
LSP0Constants.sol | 8 | Contains the standard ERC725Y metadata keys and LSP1 type IDs defined in the LSP0 Standard as well as bytes4 ERC165 interface ID. | |
UniversalProfile | These 3 x contracts are implementations of LSP0 + set the metadata keys defined in the LSP3 standard on deployment/initialization. | ||
UniversalProfileInitAbstract.sol | 21 | Abstract Proxy version without a public initialize(...) function. | |
UniversalProfile.sol | 14 | Standard version. | |
UniversalProfileInit.sol | 12 | Proxy version. | |
LSP1UniversalReceiverDelegate | |||
LSP1UniversalReceiverDelegateUP.sol | 131 | Core implementation of the optional extension of LSP1 Universal Receiver Delegate to be used for a Universal Profile. | @openzeppelin/contracts/ : [ERC165.sol , ERC165Checker.sol ] |
LSP1Utils.sol | 99 | Library of utility functions to interact with contracts that implement the LSP1 interface. | @openzeppelin/contracts/ : [Address.sol , ERC165Checker.sol ] |
ILSP1UniversalReceiver.sol | 14 | Interface that describe the standard function defined in the LSP1 standard. | |
LSP1Constants.sol | 4 | Contains the standard ERC725Y metadata keys defined in the LSP1 Universal Receiver standard, as well as its ERC165 interface ID. | |
LSP1Errors.sol | 4 | Custom errors related to the internal logic of LSP1UniversalReceiverDelegateUP.sol . | |
LSP4DigitalAssetMetadata | |||
LSP4DigitalAssetMetadataInitAbstract.sol | 43 | Abstract Proxy version of the LSP4 standard, without a public initialize(...) function. | |
LSP4DigitalAssetMetadata.sol | 40 | Standard version of LSP4 standard. | submodules/ERC725/implementations/contracts/ : ERC725Y.sol , solidity-bytes-utils : BytesLib.sol |
LSP4Compatibility.sol | 14 | Contains backward compatible getters from ERC721Metadata that query the LSP4 metadata keys from the ERC725Y storage. | submodules/ERC725/implementations/contracts/ : ERC725YCore.sol |
LSP4Constants.sol | 8 | Contains the standard ERC725Y metadata keys defined in the LSP4 Digital Asset Metadata standard. | |
ILSP4Compatibility.sol | 8 | Interface for name() and symbol() . | |
LSP4Errors.sol | 3 | Custom errors related to the internal logic of LSP4DigitalAssetMetadata.sol and LSP4DigitalAssetMetadataInitAbstract.sol . | |
LSP6KeyManager | |||
LSP6SetDataModule.sol | 447 | Logic used for verifying ERC725Y.setData() & ERC725Y.setDataBatch() permissions. | submodules/ERC725/implementations/contracts/ : ERC725Y.sol |
LSP6KeyManagerCore.sol | 401 | Core implementation of the LSP6 standard, a smart contract that acts as a controller for an an ERC725 account. | @openzeppelin/contracts : [ERC165.sol , ECDSA.sol , Address.sol ] |
LSP6ExecuteModule.sol | 273 | Logic used for verifying ERC725X.execute() & ERC725X.executeBatch() permissions. | submodules/ERC725/implementations/contracts/ : ERC725Y.sol , solidity-bytes-utils : BytesLib.sol , @openzeppelin/contracts/ : ERC165Checker.sol |
LSP6Utils.sol | 160 | Library of utility functions to read, write and handle permissions for controllers. | |
LSP6Constants.sol | 36 | Contains the standard ERC725Y metadata keys and permissions defined in the LSP6 Key Manager standard, as well as its ERC165 interface ID. | |
ILSP6KeyManager.sol | 36 | Interface that describes the standard functions and events defined in the LSP6 Key Manager standard. | |
LSP6Errors.sol | 28 | Custom errors related to the internal logic of LSP6KeyManagerCore and the modules in the contracts/LSP6KeyManager/LSP6Modules/*.sol folder`. | |
LSP6OwnershipModule.sol | 22 | Logic used for verifying ERC725.acceptOwnership() & ERC725.transferOwnership() permissions. | |
LSP6KeyManagerInitAbstract.sol | 16 | Abstract Proxy version of the LSP6 standard, without a public initialize(...) function. | @openzeppelin/contracts-upgradeable : Initializable.sol |
LSP6KeyManager.sol | 10 | Standard version of LSP6 standard. | Same as LSP6KeyManagerCore.sol |
LSP6KeyManagerInit.sol | 10 | Proxy version of the LSP6 standard. | Same as LSP6KeyManagerCore.sol + LSP6KeyManagerInitAbstract.sol |
LSP7DigitalAsset | |||
LSP7DigitalAssetCore.sol | 277 | Core implementation of the LSP7 standard, a smart contract that represents a fungible token. | @openzeppelin/contracts : ERC165Checker.sol |
LSP7CompatibleERC20InitAbstract.sol | 113 | Extension (Abstract Proxy version without a public initialize(...) function) - of the ILSP7CompatibleERC20 interface. | |
LSP7CompatibleERC20.sol | 102 | Extension (Standard version) - of the ILSP7CompatibleERC20 interface. | |
ILSP7DigitalAsset.sol | 44 | Interface that describes the standard functions and events defined in the LSP7 Digital Asset standard. | |
LSP7DigitalAssetInitAbstract.sol | 33 | Abstract Proxy version of the LSP7 standard, without a public initialize(...) function. | submodules/ERC725/implementations/contracts/ : ERC725YCore.sol |
LSP7CappedSupply.sol | 27 | Extension (Standard version) - to define a maximum total supply of tokens. | |
LSP7CappedSupplyInitAbstract.sol | 31 | Extension (Abstract proxy version) - to define a maximum total supply of tokens. | |
LSP7DigitalAsset.sol | 28 | Standard version of the LSP7 standard. | submodules/ERC725/implementations/contracts/ : ERC725YCore.sol |
LSP7MintableInitAbstract.sol | 31 | Preset deployable contract (Abstract proxy version without a public initialize(...) function) - same as LSP7Mintable.sol . | |
LSP7CompatibleERC20MintableInitAbstract.sol | 23 | Preset deployable contract (Abstract Proxy version without a public initialize(...) function) - same as LSP7Mintable.sol . | |
LSP7Mintable.sol | 19 | Preset deployable contract (Standard version) - contains a public mint(...) only callable by the owner. | |
LSP7CompatibleERC20Mintable.sol | 17 | Preset deployable contract (Standard version) - same as LSP7CompatibleERC20.sol with a public mint(...) function only callable by the owner. | |
LSP7Errors.sol | 22 | Custom errors related to the internal logic of LSP7DigitalAssetCore.sol . | |
LSP7CompatibleERC20MintableInit.sol | 16 | Preset deployable contract (Abstract Proxy version without a public initialize(...) function) - same as LSP7CompatibleERC20Mintable.sol | |
LSP7MintableInit.sol | 20 | Preset deployable contract (Proxy version) - same as LSP7Mintable.sol | |
ILSP7CompatibleERC20.sol | 21 | Interface to enable backward compatibility between LSP7 and the functions from the ERC20 standard. | |
ILSP7Mintable.sol | 10 | Interface for a public mint(...) function for a LSP7 fungible token. | |
LSP7Burnable.sol | 7 | Standard extension that implements a public burn(...) function to burn a specified amount of tokens. | |
LSP7BurnableInitAbstract.sol | 9 | Abstract Proxy extension that implements a public burn(...) function to burn a specified amount of tokens. | |
LSP7Constants.sol | 4 | Contains the standard LSP1 type IDs defined in the LSP7 Digital Asset standard, as well as its ERC165 interface ID. | |
LSP8IdentifiableDigitalAsset | |||
LSP8IdentifiableDigitalAssetCore.sol | 295 | Core implementation of the LSP8 standard, a smart contract that represents a non-fungible token. | @openzeppelin/contracts : [EnumerableSet.sol , ERC165Checker.sol ] |
LSP8CompatibleERC721InitAbstract.sol | 241 | Extension (Abstract Proxy version) - of the ILSP8CompatibleERC721 interface, without a public initialize(...) function. | @openzeppelin/contracts : EnumerableSet.sol , solidity-bytes-utils : BytesLib.sol |
LSP8CompatibleERC721.sol | 231 | Extension (Standard version) - of the ILSP8CompatibleERC721 interface. | @openzeppelin/contracts : EnumerableSet.sol , solidity-bytes-utils : BytesLib.sol |
ILSP8IdentifiableDigitalAsset.sol | 54 | Interface that describes the standard functions and events defined in the LSP8 Identifiable Digital Asset standard. | |
LSP8EnumerableInitAbstract.sol | 34 | Extension (Abstract proxy version) - to enable enumerating over the list of tokenIds. | |
LSP8Enumerable.sol | 36 | Extension (Standard version) - to enable enumerating over the list of tokenIds. | |
LSP8CappedSupplyInitAbstract.sol | 33 | Extension (Abstract proxy version without a public initialize(...) function) - to define a maximum total supply of tokens. | |
LSP8CappedSupply.sol | 29 | Extension (Standard version) - to define a maximum total supply of tokens. | |
LSP8IdentifiableDigitalAssetInitAbstract.sol | 33 | Abstract Proxy version of the LSP8 standard without a public initialize(...) function. | submodules/ERC725/implementations/contracts/ : ERC725YCore.sol |
LSP8MintableInitAbstract.sol | 29 | Preset deployable contract (Abstract proxy version without a public initialize(...) function) - same as LSP8Mintable.sol | |
ILSP8CompatibleERC721.sol | 23 | Interface to enable backward compatibility between LSP8 and functions from the ERC721 standard. | |
LSP8IdentifiableDigitalAsset.sol | 27 | Standard version of the LSP8 standard. | erc725/smart-contracts : ERC725YCore.sol |
LSP8CompatibleERC721MintableInitAbstract.sol | 23 | Preset deployable contract (Abstract Proxy version without a public initialize(...) function) - same as LSP8Mintable.sol . | |
LSP8Mintable.sol | 20 | Preset deployable contract (Standard version) - contains a public mint(...) only callable by the owner. | |
LSP8CompatibleERC721Mintable.sol | 17 | Preset deployable contract (Standard version) - same as LSP8CompatibleERC721.sol with a public mint(...) function only callable by the owner. | |
LSP8CompatibleERC721MintableInit.sol | 22 | Preset deployable contract (Abstract Proxy version without a public initialize(...) function) - same as LSP8CompatibleERC721Mintable.sol | |
LSP8Errors.sol | 16 | Custom errors related to the internal logic of LSP8IdentifiableDigitalAssetCore.sol . | |
LSP8MintableInit.sol | 14 | Preset deployable contract (Proxy version) - same as LSP8Mintable.sol | |
LSP8Burnable.sol | 13 | Extension that implements a public burn(...) function for LSP8 to burn a specific tokenId . | |
ILSP8Mintable.sol | 12 | Interface for a public mint(...) function for a LSP8 non-fungible token. | |
LSP8Constants.sol | 6 | Contains the standard ERC725Y metadata keys, LSP1 type IDs defined in the LSP8 standard, as well as its ERC165 interface ID. | |
LSP14Ownable2Step | |||
LSP14Ownable2Step.sol | 95 | Core implementation of the LSP14 standard. | submodules/ERC725/implementations/contracts/ : [custom/OwnableUnset.sol ] |
ILSP14Ownable2Step.sol | 13 | Interface that describes the standard functions and events defined in the LSP14 Ownable 2 Step standard. | |
LSP14Constants.sol | 5 | Contains the LSP1 type IDs defined in the LSP14 standard, as well as its ERC165 interface ID. | |
LSP14Errors.sol | 6 | Custom errors related to the internal logic of LSP14Ownable2Step.sol . | |
LSP17ContractExtension | |||
LSP17Extendable.sol | 59 | Core implementation of the LSP17 standard. Extendable contract fallback logic. | @openzeppelin/contracts/ : [ERC165.sol , ERC165Checker.sol ] |
LSP17Extension.sol | 29 | Core implementation of the LSP17 standard. Extension logic. | @openzeppelin/contracts/ : ERC165.sol |
LSP17Constants.sol | 4 | Contains the standard ERC725Y metadata keys & ERC165 interface ID defined in the LSP17 standard. | |
LSP17Errors.sol | 2 | Custom errors related to the internal logic of LSP17Extendable.sol . | |
LSP17Utils.sol | 9 | Library of utility functions used to help handling LSP17 Contract Extensins | |
LSP20CallVerification | |||
LSP20CallVerification.sol | 60 | Core implementation of the LSP20 Standard. | |
ILSP20CallVerifier.sol | 12 | Interface that describes the standard functions and events defined in the LSP20 standard. | |
LSP20Constants.sol | 6 | Contains the ERC165 interface ID and return values defined in the LSP20 standard. | |
LSP20Errors.sol | 3 | Custom errors related to the internal logic of LSP20CallVerification.sol . | |
Other Libraries & Constants | |||
LSP2Utils.sol (only 4 functions in scope) | Library of functions to construct ERC725Y Data Keys based on their key type defined by a LSP2 JSON Schema. | solidity-bytes-utils : BytesLib.sol | |
- generateArrayElementKeyAtIndex(bytes32,uint128) | 10 | ||
- generateMappingKey(bytes10,bytes20) | 11 | ||
- generateMappingWithGroupingKey(bytes10,bytes20) | 11 | ||
- isCompactBytesArray(bytes) | 19 | ||
LSP5Utils.sol | 123 | Library of functions to register and manage assets stored in an ERC725Y smart contract storage. | solidity-bytes-utils : BytesLib.sol |
LSP5Constants.sol | 3 | Contains the standard ERC725Y metadata keys, defined in the LSP5 standard. | |
LSP10Utils.sol | 113 | Library of functions to register and manage vaults stored in an ERC725Y smart contract storage. | solidity-bytes-utils : BytesLib.sol |
LSP10Constants.sol | 3 | Contains the standard ERC725Y metadata keys, defined in the LSP10 standard. |
Out of Scope | Details |
---|---|
contracts/LSP9Vault | We decided to incude them in this repository in order for the wardens to get familiar with LSP9Vault , because it's used and mentioned in the LSP1UniversalReceiverDelegateUP to register the addresses of the received vaults to an LSP0ERC725Account . |
contracts/Mocks | Those contracts are only used for testing. |
The LSP1UniversalReceiverDelegateUP contract will be used as the primary UniversalReceiverDelegate (not a UniversalReceiverDelegate mapped to a specific typeId via the LSP1UniversalReceiverDelegate:<typeId>
data key) for the majority of UniversalProfiles deployed on the network. Instead of deploying a UniversalReceiverDelegate for each individual UniversalProfile, this contract operates based on global variables and parameters. A single instance of this contract will be deployed and assigned to all UniversalProfiles.
Additionally, this contract will be granted the SUPER_SETDATA
and REENTRANCY
permissions across all UniversalProfiles, according to LSP6KeyManager. Given this design and architecture, it's essential to investigate and identify potential bugs or vulnerabilities thoroughly. Particular attention should be given to any possible loopholes that could allow for unintended write access to the storage of contracts beyond the msg.sender
(the UniversalProfile initiating the call), bypassing of permissions, among other security concerns.
The architecture implemented in this repository comprises a core contract that encapsulates the main logic and two other contracts that inherit from this core contract. One is designed for the standard constructor version, and the other is designed as an initializable version. The initializable version is intentionally created for MinimalProxies (EIP1167), not for upgradeable proxies (as of the current state). Consequently, no gaps have been introduced or need to respect the use of a separate package.
Meanwhile, the LUKSO team is developing a transpiler to streamline this process. This transpiler will eliminate the need for the core contract, allowing for constructor contracts in a repository. Then, the transpiler will convert all the code into an initializable version compatible with both upgradeable and minimal proxies in a separate repository.
In the execute(uint256,address,uint256,bytes)
function of ERC725X, no additional checks have been introduced to verify that the owner has not changed following a delegatecall. This is a design choice, as introducing such checks might give a false sense of security. It is possible that a malicious actor could momentarily alter the owner variable during the delegatecall, and do malicious action and reset it afterwards, thereby bypassing the check. Additionally, the importance of the owner variable may vary between different contracts and implementations. For instance, a delegatecall could modify the ERC725Y storage, which might serve as the principal access point to the account in certain cases. This is particularly relevant when the account is owned by an LSP6KeyManager, where permissions are stored in the ERC725Y storage rather than being tied to the owner variable.
LSP7CompatibleERC20
and LSP8CompatibleERC721
LSP7 and LSP8 are different in terms of their interface and the way to interact with them compared to the traditional ERC20 and ERC721 token standards. We have also included two contracts that can be used and interacted with the same way as you would interact with ERC20 / ERC721 tokens while leveraging the core functionalities of the LSP7/8 standards.
LSP7CompatibleERC20
, which contains all the LSP7 public functions + the following ERC20 public functions: allowance(...)
, approve(...)
, transfer(...)
and transferFrom(...)
LSP8CompatibleERC721
, which contains the LSP8 public functions + the following ERC721 public functions: tokenURI(...)
, ownerOf(...)
, getApproved(...)
, isApprovedForAll(...)
, approve(...)
, setApprovalForAll(...)
, transferFrom(...)
, safeTransferFrom(...)
and authorizeOperator(...)
.This means that all the logic of the native LSP7/LSP8 functions for token transfers and operator approvals are wrapped inside the ERC20/ERC721 related functions.
In details, in LSP7CompatibleERC20
, for any of these transfer and approval functions.
LSP7.transfer(address,address,uint256,bool,bytes)
LSP7.authorizeOperator(address,address,uint256)
ERC20.transfer(address,uint256)
ERC20.transferFrom(address,address,uint256)
ERC20.approve(address,uint256)
The behavior specific to this contract is as follow:
LSP1.universalReceiver(...)
function on the from
and to
address is made when calling any of the transfer functions listed above (whether from LSP7 or ERC20).Transfer
event is emitted for any of the transfer functions listed above (whether from LSP7 or ERC20).Approval
event is emitted in both the LSP7.authorizeOperator(...)
and the ERC20.approve(...)
function.In details, in LSP8CompatibleERC721
, for any of these transfer and approval functions.
LSP8.transfer(address,address,bytes32,bool,bytes)
LSP8.authorizeOperator(address,address,bytes32)
ERC721.transferFrom(address,address,uint256)
ERC721.safeTransferFrom(address,address,uint256)
ERC721.safeTransferFrom(address,address,uint256,bytes)
ERC721.approve(address,uint256)
The behavior specific to this contract is as follow:
LSP1.universalReceiver(...)
function on the from
and to
address is made when calling any of the transfer functions listed above (whether from LSP8 or ERC721).Transfer
event is emitted for any of the transfer functions listed above (whether from LSP8 or ERC721).Approval
event is emitted in both the LSP8.authorizeOperator(...)
and the ERC721.approve(...)
function.- If you have a public code repo, please share it here: https://github.com/lukso-network/lsp-smart-contracts/tree/v0.10.2 - How many contracts are in scope?: 53 - Total SLoC for these contracts?: 3469 - How many external imports are there?: 3 - How many separate interfaces and struct definitions are there for the contracts within scope?: 14 - Does most of your code generally use composition or inheritance?: Inheritance - How many external calls?: 0 - What is the overall line coverage percentage provided by your tests?: 90 - Is there a need to understand a separate part of the codebase / get context in order to audit this part of the protocol?: True - Please describe required context: Understanding of ERC725 + the LSP standards - Does it use an oracle?: No - Does the token conform to the ERC20 standard?: True - Are there any novel or unique curve logic or mathematical models?: The ERC725Account is a blockchain account, that can do several stuff execution, setting data, validating signatures, receiving and reacting on universalReceiver calls, extend the account with extensions. The owner can execute certain functions, as well as addresses allowed by the logic of the owner. (If the caller is not the owner, verification is forwarded to the owner) The LSP6KeyManager is a smart contract that can own the ERC725Account and allow certain calls to the account linked (ERC725Account) based on the permissions of the caller which are stored in the ERC725Account. Also this contract allows relay execution. LSP1UniversalReceiverDelegate is a contract design to be set as a UniversalReceiverDelegate of the account to react on certain universalReceiver calls, to register the asset of the tokens and vaults received. LSP7 and LSP8 are new token standards based on LSP4 that allow adding metadata to the tokens contracts themselves. Their interface is built in a unified way, using the same functions for transferring tokens and approving operators, including the same number of parameters (and their types) for these functions. They also include features via LSP1 to notify the sender and the recipient on transfer of receiving tokens. - Does it use a timelock function?: - Is it an NFT?: True - Does it have an AMM?: No - Is it a fork of a popular project?: False - Does it use rollups?: No - Is it multi-chain?: LUKSO itself is not a multi-chain. The lsp-smart-contracts are initially intended to be used on the LUKSO network. However, the Universal Profile of a user could be deployed at the same address across multiple chains (using for instance Nick's method), to enable the user to use the same UP across multiple networks / chains and offer a different user experience. - Does it use a side-chain?: No
git clone https://github.com/code-423n4/2023-06-lukso.git \ && cd 2023-06-lukso \ && git submodule update --init --recursive \ && npm install \ && cd ./submodules/ERC725/implementations && npm install && cd ../../../
git clone https://github.com/code-423n4/2023-06-lukso.git cd 2023-06-lukso
forge-std
for the Foundry tests + the ERC725
smart contracts)git submodule update --init --recursive
lsp-smart-contracts
and the ERC725
submodule)npm install cd ./submodules/ERC725/implementations && npm install cd ../../../
To compile the contracts
npm run build
Note: some hardhat tests related to setting the Allowed Calls of a controller under the
AddressPermissions:AllowedCalls:<address>
data key in the LSP6 Key Manager are currently skipped (via.skip
). They come from an older version of the Key Manager that has been refactored, where the behaviour and custom errors used have changed. These tests have not been updated, do not test for the correct behaviour anymore and are marked as skipped for this reason.
To run the mocha unit tests:
npm run test
While it take time to run the full tests using the command above, it is recommended to run the tests seperatly for each contract with the following commands:
npm run test:lsp1 ## OR npm run test:up ## OR npm run test:lsp7
You can find the full list of tests commands in package.json
The test benchmark is for the standard version of the contract and not the proxy version.
To run the gas benchmark tests:
npm run test:benchmark
The foundry tests will also output a gas report.
To run foundry tests (with gas report)
Setup:
git submodule update --init --recursive --remote yarn curl -L https://foundry.paradigm.xyz | bash foundryup
Test:
forge install forge test
Additionally, you can run the coverage:
Coverage might fail if the contract size exceeds the limit (coverage run without optimization)
npm run test:coverage
To view the report, open coverage/index.html in your browser.
Get the contract size by running
npx hardhat size-contracts
Before deployment, add a private key to
.env
file in the root of the project in this variableDEPLOYER_PRIVATE_KEY= ".."
and make sure the EOA has enough LYXt to fund the deployment of the contracts on LUKSO's Testnet (Faucet link: https://faucet.testnet.lukso.network/)
It is possible to run the following commands to deploy few contracts according to the scripts in ./deploy folder:
npx hardhat deploy --network luksoTestnet --tags UniversalProfile npx hardhat deploy --network luksoTestnet --tags LSP6KeyManager npx hardhat deploy --network luksoTestnet --tags LSP1UniversalReceiverDelegateUP // The Mintable preset of LSP7 npx hardhat deploy --network luksoTestnet --tags LSP7Mintable // The Mintable preset of LSP8 npx hardhat deploy --network luksoTestnet --tags LSP8Mintable npx hardhat deploy --network luksoTestnet --tags LSP9Vault
It is also possible to verify the contracts deployed:
npx hardhat verify --network luksoTestnet --contract contracts/UniversalProfile.sol:UniversalProfile <address of the UniversalProfile deployed> <address of the deployer> npx hardhat verify --network luksoTestnet --contract contracts/LSP6KeyManager/LSP6KeyManager.sol:LSP6KeyManager <address of the KeyManager deployed> <address of the target controlled (UP)> npx hardhat verify --network luksoTestnet --contract contracts/LSP6KeyManager/LSP6KeyManager.sol:LSP6KeyManager <address of the KeyManager deployed> <address of the target controlled (UP)> npx hardhat verify --network luksoTestnet --contract contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol:LSP1UniversalReceiverDelegateUP <address of the LSP1UniversalReceiverDelegateUP deployed> npx hardhat verify --network luksoTestnet --contract contracts/LSP1UniversalReceiver/LSP1UniversalReceiverDelegateUP/LSP1UniversalReceiverDelegateUP.sol:LSP1UniversalReceiverDelegateUP <address of the LSP1UniversalReceiverDelegateUP deployed> npx hardhat verify --network luksoTestnet --contract contracts/LSP7DigitalAsset/presets/LSP7Mintable.sol:LSP7Mintable <address of the LSP7 deployed> 'LSP7 Mintable' 'LSP7M' <address of the deployer> false npx hardhat verify --network luksoTestnet --contract contracts/LSP8IdentifiableDigitalAsset/presets/LSP8Mintable.sol:LSP8Mintable <address of the LSP8 deployed> 'LSP8 Mintable' 'LSP8M' <address of the deployer> npx hardhat verify --network luksoTestnet --contract contracts/LSP9Vault/LSP9Vault.sol:LSP9Vault <address of the LSP9 deployed> <address of the deployer>