-
Notifications
You must be signed in to change notification settings - Fork 295
feat: Add caching storage in memory vulnerability details #103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
devtooligan
wants to merge
1
commit into
kadenzipfel:master
Choose a base branch
from
devtooligan:caching-storage-in-memory
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+127
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
## Caching Storage in Memory | ||
|
||
Storage variables such as structs are often loaded into memory for efficiency. When incorrectly handled, this can lead to inconsistent state updates. This type of vulnerability can manifest when developers cache storage values in memory variables but either fail to update the storage after modifying the memory variables or incorrectly synchronize between memory and storage. | ||
|
||
### Storage vs Memory in Solidity | ||
|
||
In Solidity, `storage` refers to data that is persistently stored on the blockchain, while `memory` is temporary and exists only during function execution. Reading from storage is expensive, so developers often cache storage values in memory to reduce gas costs. However, this optimization can lead to vulnerabilities if not handled correctly. | ||
|
||
### Self-transfer Free Minting Bug | ||
|
||
One common example of this vulnerability is the "self-transfer free minting" bug, where tokens can be minted for free by exploiting incorrect memory handling during self-transfers. | ||
|
||
```solidity | ||
struct UserData { | ||
bytes32 id; | ||
uint256 balance; | ||
address account; | ||
} | ||
|
||
mapping(address => UserData) public users; | ||
|
||
// VULNERABLE | ||
function transfer(address from, address to, uint248 transferValue) { | ||
UserData memory _accountFrom = users[from]; | ||
if (blacklisted[_accountFrom.id]) revert(); | ||
|
||
UserData memory _accountTo = users[to]; | ||
if (blacklisted[_accountTo.id]) revert(); | ||
Comment on lines
+25
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: We should drop these blacklisted checks since they're not relevant to the vulnerability. Could also then drop |
||
|
||
if (_accountFrom.balance < transferValue) revert(); | ||
|
||
users[from].balance = _accountFrom.balance - transferValue; | ||
|
||
// CRITICAL BUG | ||
// When from == to, the _accountTo.balance was cached before | ||
// _accountFrom.balance was reduced so it increases the user balance. | ||
users[to].balance = _accountTo.balance + transferValue; | ||
|
||
emit Transfer(from, to, transferValue); | ||
} | ||
``` | ||
|
||
In this example, when a user transfers tokens to themselves (from = to), the vulnerability emerges. The function first caches the account data for both the sender and recipient in memory. When the addresses are the same, these are logically the same storage slot, but two different memory variables are created. | ||
|
||
1. `_accountDataFrom` is loaded with the current balance | ||
2. `accountDataTo` is also loaded with the same current balance | ||
3. When updating storage, the sender's balance is reduced by `transferValue` | ||
4. The recipient's balance is then increased by `transferValue`, but using the cached value in `accountDataTo` which does not reflect the reduction that just happened | ||
|
||
This allows users to artificially increase their token balance by executing self-transfers, effectively minting tokens without authorization. | ||
|
||
### Stale Struct State Bug | ||
|
||
Another example is when a struct is cached in memory, then modified, but not correctly written back to storage: | ||
|
||
```solidity | ||
// VULNERABLE | ||
struct UserAccount { | ||
uint256 balance; | ||
uint256 reward; | ||
uint256 lastUpdateTime; | ||
bool isActive; | ||
} | ||
|
||
mapping(address => UserAccount) public accounts; | ||
|
||
function updateAccountAndClaim(address user) external { | ||
// Cache the account in memory | ||
UserAccount memory account = accounts[user]; | ||
|
||
// Calculate new rewards | ||
uint256 timeElapsed = block.timestamp - account.lastUpdateTime; | ||
uint256 newRewards = calculateRewards(account.balance, timeElapsed); | ||
|
||
account.reward += newRewards; // Update memory values | ||
|
||
// Transfer rewards | ||
if (account.reward > 0 && account.isActive) { | ||
uint256 amountToTransfer = account.reward; | ||
account.reward = 0; // Reset rewards in memory | ||
|
||
// External call | ||
(bool success, ) = user.call{value: amountToTransfer}(""); | ||
require(success, "Transfer failed"); | ||
} | ||
|
||
// Update only timestamp in storage, forgetting to update the reward field | ||
accounts[user].lastUpdateTime = block.timestamp; | ||
|
||
} | ||
``` | ||
|
||
In this case, the function caches the user's account in memory, updates the reward, then transfers rewards to the user. However, it only writes the updated timestamp back to storage, forgetting to update the reward field. As a result, users can claim the same rewards multiple times because the reward balance in storage remains unchanged. | ||
|
||
|
||
### Prevention Methods | ||
|
||
To prevent vulnerabilities related to caching storage in memory: | ||
|
||
1. **Be extra careful when caching storage into memory** - Ensure all updated members are written back to storage after modification. | ||
|
||
|
||
2. **Use fuzz testing** - A simple fuzz test can catch these issues by testing edge cases like self-transfers. | ||
|
||
```solidity | ||
function testFuzz_TransferSelfAndOthers(address from, address to, uint248 amount) public { | ||
// Store balances before transfer | ||
uint256 balanceFromBefore = token.balanceOf(from); | ||
uint256 balanceToBefore = token.balanceOf(to); | ||
|
||
// Execute transfer | ||
vm.prank(from); | ||
token.transfer(to, amount); | ||
|
||
// Assert correct balances whether self-transfer or not | ||
assertEq(token.balanceOf(from), balanceFromBefore - amount); | ||
assertEq(token.balanceOf(to), balanceToBefore + amount); | ||
} | ||
``` | ||
|
||
### Sources | ||
|
||
- [Self transfer can lead to unlimited mint ](https://solodit.cyfrin.io/issues/h-01-self-transfer-can-lead-to-unlimited-mint-code4rena-notional-notional-git) | ||
- [Mint PerpetualYieldTokens for free by self-transfer](https://solodit.cyfrin.io/issues/mint-perpetualyieldtokens-for-free-by-self-transfer-spearbit-timeless-pdf) | ||
- [Smart Contract Auditing Heuristics](https://github.com/OpenCoreCH/smart-contract-auditing-heuristics?tab=readme-ov-file#behavior-when-src--dst) | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wdyt about instead naming it, "Unsafe storage caching"? IMO the current name seems to imply that it's generally unsafe to cache storage in memory
Note that if we change the name, we should also change the filename and the reference in the readme