Security Audit
July 4th, 2023
Version 1.0.0
Presented by 0xMacro
This document includes the results of the security audit for Grateful's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from June 4, 2023 to June 9, 2023.
Note: Grateful contracts are based on the Synthetix’s Router Proxy Architecture using the unstructured storage pattern. The audit was focused around the actual protocol logic, meaning that code related to the Proxy Architecture including the actual deployment of the contracts were outside of the scope of this audit.
The purpose of this audit is to review the source code of certain Grateful Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.
Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.
The following is an aggregation of issues found by the Macro Audit team:
Severity | Count | Acknowledged | Won't Do | Addressed |
---|---|---|---|---|
High | 1 | - | - | 1 |
Low | 4 | - | 1 | 3 |
Code Quality | 3 | - | 1 | 2 |
Informational | 1 | - | 1 | - |
Gas Optimization | 1 | - | - | 1 |
Grateful was quick to respond to these issues.
Our understanding of the specification was based on the following sources:
Grateful contracts use an upgradable proxy architecture, allowing the owner to upgrade to a new implementation. In addition, the owner has the following privileges:
The following source code was reviewed during the audit:
87b9b31b1d5e7a3315d1b70ea22b409427bfed79
Specifically, we audited the following contracts within this repository:
Source Code | SHA256 |
---|---|
contracts/modules/BalancesModule.sol |
|
contracts/modules/ConfigModule.sol |
|
contracts/modules/CoreModule.sol |
|
contracts/modules/FeesModule.sol |
|
contracts/modules/FundsModule.sol |
|
contracts/modules/LiquidationsModule.sol |
|
contracts/modules/MulticallModule.sol |
|
contracts/modules/ProfilesModule.sol |
|
contracts/modules/SubscriptionsModule.sol |
|
contracts/modules/VaultsModule.sol |
|
contracts/errors/BalanceErrors.sol |
|
contracts/errors/InputErrors.sol |
|
contracts/errors/ProfileErrors.sol |
|
contracts/errors/SubscriptionErrors.sol |
|
contracts/errors/VaultErrors.sol |
|
contracts/storage/Balance.sol |
|
contracts/storage/Config.sol |
|
contracts/storage/Fee.sol |
|
contracts/storage/Profile.sol |
|
contracts/storage/ProfileNft.sol |
|
contracts/storage/ProfileRBAC.sol |
|
contracts/storage/Subscription.sol |
|
contracts/storage/SubscriptionNft.sol |
|
contracts/storage/SubscriptionRegistry.sol |
|
contracts/storage/Vault.sol |
|
contracts/utils/ProfileRenderer.sol |
|
contracts/utils/RendererUtils.sol |
|
contracts/utils/SubscriptionRenderer.sol |
|
contracts/utils/SubscriptionUtil.sol |
|
contracts/utils/VaultUtil.sol |
|
Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.
Click on an issue to jump to it, or scroll down to see them all.
liquidatorId
isVaultPaused
always returns wrong value
tokenURI
returns value for invalid token ids
_EDIT_PERMISSIONS
not used
_hasEnoughBalance
makes _isSolvent
redundant
GratefulProfile
NFTs would break the createProfile
operation
We quantify issues in three parts:
This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:
Severity | Description |
---|---|
(C-x) Critical |
We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost. |
(H-x) High |
We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec. |
(M-x) Medium |
We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner. |
(L-x) Low |
The risk is small, unlikely, or may not relevant to the project in a meaningful way. Whether or not the project wants to develop a fix is up to the goals and needs of the project. |
(Q-x) Code Quality |
The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design. |
(I-x) Informational |
Warnings and things to keep in mind when operating the protocol. No immediate action required. |
(G-x) Gas Optimizations |
The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it. |
A malicious user can perform a sandwich attack on the liquidate call which results in the liquidate call to be reverted. Consider the following scenario:
Because step 6 happens in the same block, there are no funds flowing from p3
to p1
, but the setup is enough to get the liquidate
call reverted. Thus, the costs of the attack are only the tx costs for subscribe
and unsubscribe
.
The attacker can repeatedly perform the attack, to keep the funds flowing from p1
to p2
and thereby draining the funds of the protocol.
Note that this attack only pays off under the following circumstances:
liquidate
call for the entire liquidation period and beyond.p2
increased due to preventing liquidation is higher than the cost of the attack (= tx costs for sandwiching liquidate
call).Consider adding a delay between subscribe
and unsubscribe
to make the above attack vector unprofitable.
When a profile NFT is transferred to a new owner, ProfilesModule.notifyProfileTransfer
is called to revoke all existing permissions and to set new owner.
However, for subscription NFTs associated to a profile, the new owner is not set and thus still pointing to the original owner when checking GratefulSubscriptions.ownerOf
.
Consider also transferring the profile’s associated subscription NFTs to the new owner when the the profile is transferred to a new owner.
There is no subscriptions list in the contracts to know which NFTs to transfer. So we agreed to add it to the documentation, and delegate the responsibility to the user (also with a batch call from the frontend)
liquidatorId
LiquidationModule.liquidate
takes liquidatorId
as an argument. However, there is no validation done whether the liquidatorId
is associated to a profile.
If this is not a requirement, consider including msg.sender
when emitting the event.
Also the SubscriptionLiquidated
event is emitted with 0
rewards. Since there are no incentives for liquidators in this version of the contract, consider removing the reward
argument in the emitted event.
Subscription.getDuration
returns the duration + elapsedTime
where elapsedTime
is calculated as follows:
uint256 elapsedTime = block.timestamp - lastUpdate;
While this is true for active subscriptions, the calculation is not correct for unsubscribed subscriptions, as in this case elapsedTime
should be 0.
Some “weird” ERC20 tokens (e.g. USDT) require the approval to be set to 0 before setting it to a new value (see here). For such tokens, VaultsModule.addVault
would fail when trying to add a vault implementation that is already used within another vault.
Note that this is seen as a low-level issue, as the scenario of adding multiple vaults pointing to same implementation is considered as very unlikely according to the Grateful team.
Each vault will have it’s own implementation even though using the same ERC20 asset.
isVaultPaused
always returns wrong value
VaultUtil.isVaultPaused
returns the value from Vault.isPaused()
. However, isPaused
is not implemented correctly and returns true
for unpaused vaults and false
for paused vaults.
The only reason why FundsModule.withdrawFunds
correctly allows to withdraw funds from unpaused vaults is because it checks against the inverted value as seen below:
if (!VaultUtil.isVaultPaused(vaultId))
revert VaultErrors.InvalidVault();
Consider changing Vault.isPaused
so that it returns true
for pause vaults and false
for unpaused vaults.
tokenURI
returns value for invalid token ids
GratefulProfile.tokenURI
and GratefulSubscription.tokenURI
don’t revert when the token id doesn’t exist but instead returns default values for subscription.
In order to comply with behavior of well known ERC721 implementation like the one from Openzeppelin, consider reverting for non-existent token ids.
_EDIT_PERMISSIONS
not used
Consider removing _EDIT_PERMISSIONS
from ProfileRBAC.sol as it is not used anywhere in the code.
The edit permission is used by the frontend to update the optionally off-chain data from the profile
_hasEnoughBalance
makes _isSolvent
redundant
In Balance.sol, canWithdraw
and canStartSubscription
check for both _hasEnoughBalance
and _isSolvent
:
return _hasEnoughBalance(self, time) && _isSolvent(self, time);
However, _hasEnoughBalance
(as it only accounts for outflow) is a much more stricter check, thus making the additional check _isSolvent
redundant.
Consider removing _isSolvent
from the above functions to save gas costs and make the code more readable.
GratefulProfile
NFTs would break the createProfile
operation
Note that at the current state, the protocol doesn’t allow burning of GratefulProfile NFTs. If this is something being considered to allow in future versions, it is worth noting that this would break ProfilesModule.createProfile
.
The function createProfile
calculates the tokenId
as follows:
uint256 tokenId = profile.totalSupply() + 1;
When a token id is burned, totalSupply
decreases by 1, meaning that for the next call to createProfile
, a tokenId
will revert, as the token id is created that was already minted before. As a result, safeMint
will revert, essentially breaking the entire createProfile
functionality.
The Grateful profile will not have the burning capability.
Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.
Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.
The scope of this report and review is limited to a review of only the code presented by the Grateful team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.