Skip to content
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

Unexpected Balances #63

Closed
wants to merge 13 commits into from
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Unchecked Return Value](./vulnerabilities/unchecked-return-values.md)
- [Unsupported Opcodes](./vulnerabilities/unsupported-opcodes.md)
- [Uninitialized Storage Pointer](./vulnerabilities/uninitialized-storage-pointer.md)
- [Unexpected Balances](./vulnerabilities/unexpected-balances.md)
- [Assert Violation](./vulnerabilities/assert-violation.md)
- [Use of Deprecated Functions](./vulnerabilities/use-of-deprecated-functions.md)
- [Delegatecall to Untrusted Callee](./vulnerabilities/delegatecall-untrusted-callee.md)
Expand All @@ -36,3 +37,5 @@
- [Off-By-One](./vulnerabilities/off-by-one.md)
- [Lack of Precision](./vulnerabilities/lack-of-precision.md)
- [Unbounded Return Data](./vulnerabilities/unbounded-return-data.md)
- [Using ``msg.value`` in a Loop](./vulnerabilities/msgvalue-loop.md)
- [Deleting a Mapping Within a Struct](./vulnerabilities/mapping-within-struct.md)
83 changes: 83 additions & 0 deletions vulnerabilities/force-feeding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Unexpected Ether Transfer

Force-feeding is a technique where an attacker sends Ether directly to a smart contract address without invoking any of its functions. This can disrupt the contract's internal accounting mechanisms, particularly if the contract relies on balance checks for its logic.

In typical smart contract operation, Ether is sent to a contract via a transaction that calls a payable function or invokes the `receive()` or `fallback()` functions. If a contract lacks these functions, transactions sending Ether to it will normally be reverted, ensuring the contract does not inadvertently receive funds.

Force-feeding bypasses this by sending Ether in a manner that doesn't require calling the contract's functions, thereby avoiding the checks and logic coded in Solidity.

## Force Feeding Methods

### Block Rewards and Coinbase

In Proof of Stake systems like Ethereum, validators earn block rewards for successfully adding blocks to the blockchain. These rewards are sent to an address known as the **coinbase address**. Validators typically set this address to their own wallets, but an attacker-validator can set it to a target smart contract’s address.

Since block reward transfers are handled at the protocol level, they bypass Solidity-level checks. As a result, the target contract receives Ether directly as part of the block reward, regardless of any Solidity-coded restrictions.

## Preventing Force Feeding

To safeguard against force-feeding, consider the following strategies:
Comment on lines +17 to +19
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self to come back to reconsider these prevention methods further


### 1. Avoid Using the Contract’s Balance Directly

Instead of relying on `address(this).balance` for critical logic, maintain an internal accounting system. For example:

```solidity
/// DO NOT USE IN PRODUCTION
/// ONLY MEANT TO SERVE AS AN EXAMPLE

mapping(address => uint256) internalBalances;

function deposit() external payable {
internalBalances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) external {
require(internalBalances[msg.sender] >= amount, "Insufficient balance");
internalBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}

function getBalance(address user) external view returns (uint256) {
return internalBalances[user];
}
```

### 2. Immediate Funds Transfer

Instead of holding funds within the contract, immediately transfer received Ether to a secure, off-contract storage or another smart contract that handles funds securely:

```solidity
address payable public safeAddress = payable(0xSafeAddress);

receive() external payable {
safeAddress.transfer(msg.value);
}
```

### 3. Event-Based Balance Tracking

Use events for deposit and withdrawal tracking, enabling off-chain monitoring and cross-verification of the contract’s balance:

```solidity
event Deposit(address indexed from, uint256 amount);
event Withdrawal(address indexed to, uint256 amount);

function deposit() external payable {
internalBalances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

function withdraw(uint256 amount) external {
require(internalBalances[msg.sender] >= amount, "Insufficient balance");
internalBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
```

Off-chain systems can then monitor these events and compare them with the on-chain state to ensure integrity.

## Sources
- [Solidity coinbase address](https://docs.soliditylang.org/en/latest/units-and-global-variables.html#block-and-transaction-properties)

24 changes: 24 additions & 0 deletions vulnerabilities/mapping-within-struct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Deleting a Mapping Within a Struct

It is a common assumption that deleting a ``struct`` will delete all of it's data entirely but there is an exception. Deleting structs with dynamic data types does not delete the data stored inside them.

For example: If a ``mapping`` (or dynamic array) is inside a struct, and the struct is deleted, the mapping will not be deleted. This is because mappings are implemented as hash tables and the EVM does not keep track of which keys have been used in the mapping. As a result, EVM doesn't know how to reset a mapping and the remaining data can be used to compromise the contract.

```solidity
struct BalancesStruct{
address owner;
mapping(address => uint) balances;
}

mapping(address => BalancesStruct) public stackBalance;

function remove() internal{
delete stackBalance[msg.sender]; // doesn't delete balances mapping inside BalancesStruct
}
```
``remove()`` function above deletes an item of ``stackBalance``. But the mapping ``balances`` inside ``BalancesStruct`` won't reset. Only individual keys and what they map to can be deleted. Example: ``delete stackBalance[msg.sender].balances[x]`` will delete the data stored at address ``x`` in the balances mapping.



## Sources
- https://docs.soliditylang.org/en/latest/types.html#delete
44 changes: 44 additions & 0 deletions vulnerabilities/msgvalue-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Using ``msg.value`` in a Loop

The value of ``msg.value`` in a transaction’s call never gets updated, even if the called contract ends up sending some or all of the ETH to another contract. This means that using ``msg.value`` in ``for`` or ``while`` loops, without extra accounting logic, will either lead to the transaction reverting (when there are no longer sufficient funds for later iterations), or to the contract being drained (when the contract itself has an ETH balance).

```solidity
contract depositer {
function deposit(address weth) payable external {
for (uint i = 0; i < 5; i ++) {
WETH(weth).deposit{value: msg.value}();
}
}
}
```
In the above example, first iteration will use all the ``msg.value`` for the external call and all other iterations can:
- Drain the contract if enough ETH balance exists inside the contract to cover all the iterations.
- Revert if enough ETH balance doesn't exist inside the contract to cover all the iterations.
- Succeed if the external implementation succeeds with zero value transfers.

Also, if a function has a check like ``require(msg.value == 1e18, "Not Enough Balance")``, that function can be called multiple times in a same transaction by sending ``1 ether`` once as ``msg.value`` is not updated in a transaction call.

```solidity
function batchBuy(address[] memory addr) external payable{
mapping (uint => address) nft;

for (uint i = 0; i < addr.length; i++) {
buy1NFT(addr[i])
}

function buy1NFT(address to) internal {
if (msg.value < 1 ether) { // buy unlimited times after sending 1 ether once
revert("Not enough ether");
}
nft[numero] = address;
}
}
```

Thus, using ``msg.value`` inside a loop is dangerous because this might allow the sender to ``re-use`` the ``msg.value``.

Reuse of ``msg.value`` can also show up with payable multicalls. Multicalls enable a user to submit a list of transactions to avoid paying the 21,000 gas transaction fee over and over. However, If ``msg.value`` gets ``re-used`` while looping through the functions to execute, it can cause a serious issue like the [Opyn Hack](https://peckshield.medium.com/opyn-hacks-root-cause-analysis-c65f3fe249db).

## Sources
- https://www.rareskills.io/post/smart-contract-security#:~:text=Using%20msg.,show%20up%20with%20payable%20multicalls.
- https://trustchain.medium.com/ethereum-msg-value-reuse-vulnerability-5afd0aa2bcef
92 changes: 92 additions & 0 deletions vulnerabilities/unexpected-balances.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Unexpected Balances in Smart Contracts

Unexpected balances in smart contracts can lead to significant accounting issues when the contract's logic relies on the balance for its operations. Two prominent examples of such issues are "force-feeding a contract" and inflation attacks.

## Force-Feeding

Force-feeding is a technique where Ether is sent directly to a smart contract without invoking any of its functions. This can disrupt the contract's internal accounting mechanisms, particularly if the contract relies on balance checks for its logic.

In typical smart contract operation, Ether is sent to a contract via a transaction that calls a payable function or invokes the `receive()` or `fallback()` functions. If a contract lacks these functions, transactions sending Ether to it will normally be reverted, ensuring the contract does not inadvertently receive funds.

Force-feeding bypasses this by sending Ether in a manner that doesn't require calling the contract's functions, thereby avoiding the checks and logic coded in Solidity.

### Force Feeding Methods

#### Block Rewards and Coinbase

In Proof of Stake systems like Ethereum, validators earn block rewards for successfully adding blocks to the blockchain. These rewards are sent to an address known as the **coinbase address**. Validators typically set this address to their own wallets, but an attacker-validator can set it to a target smart contract’s address.

Since block reward transfers are handled at the protocol level, they bypass Solidity-level checks. As a result, the target contract receives Ether directly as part of the block reward, regardless of any Solidity-coded restrictions.

## Inflation Attacks

Consider an inflation attack on a standard ERC-4626 vault. For the attack to work, the attacker has to send funds directly to the vulnerable ERC-4626 vault. This action disrupts the accounting mechanism and for this matter, the attacker is able to get away with funds belonging to other users of the vault.

Detailed explanation of inflation attacks can be found [here](https://www.youtube.com/watch?v=3IMw7xbxJgY)


## Mitigation Strategies

### 1. Avoid Using the Contract’s Balance Directly

Instead of relying on `address(this).balance` for critical logic, maintain an internal accounting system. For example:

```solidity
/// DO NOT USE IN PRODUCTION
/// ONLY MEANT TO SERVE AS AN EXAMPLE

mapping(address => uint256) internalBalances;

function deposit() external payable {
internalBalances[msg.sender] += msg.value;
}

function withdraw(uint256 amount) external {
require(internalBalances[msg.sender] >= amount, "Insufficient balance");
internalBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}

function getBalance(address user) external view returns (uint256) {
return internalBalances[user];
}
```

### 2. Immediate Funds Transfer

Instead of holding funds within the contract, immediately transfer received Ether to a secure, off-contract storage or another smart contract that handles funds securely:

```solidity
address payable public safeAddress = payable(0xSafeAddress);

receive() external payable {
safeAddress.transfer(msg.value);
}
```

### 3. Event-Based Balance Tracking

Use events for deposit and withdrawal tracking, enabling off-chain monitoring and cross-verification of the contract’s balance:

```solidity
event Deposit(address indexed from, uint256 amount);
event Withdrawal(address indexed to, uint256 amount);

function deposit() external payable {
internalBalances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

function withdraw(uint256 amount) external {
require(internalBalances[msg.sender] >= amount, "Insufficient balance");
internalBalances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
```

Off-chain systems can then monitor these events and compare them with the on-chain state to ensure integrity.

## Sources
- [Solidity coinbase address](https://docs.soliditylang.org/en/latest/units-and-global-variables.html#block-and-transaction-properties)