Name: STA token had a deflationary model with transfer fee of 1% charged from a recipient.
Description: The actual deposited amount might be lower than the specified depositAmount of the function parameter.
VulnVault: Incompatability with deflationary / fee-on-transfer tokens
Mitigation:
Transfer the tokens first and compare pre-/after token balances to compute the actual deposited amount.
REF:
https://twitter.com/1nf0s3cpt/status/1671084918506684418
https://medium.com/1inch-network/balancer-hack-2020-a8f7131c980e
https://twitter.com/BlockSecTeam/status/1600442137811689473
VulnVault Contract:
contract VulnVault {
mapping(address => uint256) private balances;
uint256 private fee;
IERC20 private token;
event Deposit(address indexed depositor, uint256 amount);
event Withdrawal(address indexed recipient, uint256 amount);
constructor(address _tokenAddress) {
token = IERC20(_tokenAddress);
}
function deposit(uint256 amount) external {
require(amount > 0, "Deposit amount must be greater than zero");
token.transferFrom(msg.sender, address(this), amount);
balances[msg.sender] += amount;
emit Deposit(msg.sender, amount);
}
function withdraw(uint256 amount) external {
require(amount > 0, "Withdrawal amount must be greater than zero");
require(amount <= balances[msg.sender], "Insufficient balance");
balances[msg.sender] -= amount;
token.transfer(msg.sender, amount);
emit Withdrawal(msg.sender, amount);
}
function getBalance(address account) external view returns (uint256) {
return balances[account];
}
}
//Mitigated vault
contract Vault {
mapping(address => uint256) private balances;
uint256 private fee;
IERC20 private token;
event Deposit(address indexed depositor, uint256 amount);
event Withdrawal(address indexed recipient, uint256 amount);
constructor(address _tokenAddress) {
token = IERC20(_tokenAddress);
}
function deposit(uint256 amount) external {
require(amount > 0, "Deposit amount must be greater than zero");
uint256 balanceBefore = token.balanceOf(address(this));
token.transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = token.balanceOf(address(this));
uint256 actualDepositAmount = balanceAfter - balanceBefore;
balances[msg.sender] += actualDepositAmount;
emit Deposit(msg.sender, actualDepositAmount);
}
function withdraw(uint256 amount) external {
require(amount > 0, "Withdrawal amount must be greater than zero");
require(amount <= balances[msg.sender], "Insufficient balance");
balances[msg.sender] -= amount;
token.transfer(msg.sender, amount);
emit Withdrawal(msg.sender, amount);
}
function getBalance(address account) external view returns (uint256) {
return balances[account];
}
}
How to Test:
forge test --contracts src/test/fee-on-transfer.sol-vvvv
// Function to test the vulnerability on transfer fees
function testVulnFeeOnTransfer() public {
// The address of Alice is set as the first address in the virtual machine
address alice = vm.addr(1);
// The address of Bob is set as the second address in the virtual machine
address bob = vm.addr(2);
// Fetches the balance of the current contract from the STAContract
STAContract.balanceOf(address(this));
// Transfers 1,000,000 tokens from the current contract to Alice
STAContract.transfer(alice, 1000000);
// Logs Alice's balance in the console, after deducting a 1% transfer fee
console.log("Alice's STA balance:", STAContract.balanceOf(alice)); // charge 1% fee
// Initiates a "prank" on Alice's account in the virtual machine (it's not clear what the prank does without additional context)
vm.startPrank(alice);
// Alice approves the Vulnerable Vault Contract to spend the maximum possible amount of tokens on her behalf
STAContract.approve(address(VulnVaultContract), type(uint256).max);
// Deposits 10,000 tokens into the Vulnerable Vault Contract from Alice's account
VulnVaultContract.deposit(10000);
// Logs the balance of Alice in the Vulnerable Vault Contract in the console, after deducting a 1% deposit fee
console.log(
"Alice deposit 10000 STA, but Alice's STA balance in VulnVaultContract:",
VulnVaultContract.getBalance(alice)
); // charge 1% fee
// Asserts that the balance of the Vulnerable Vault Contract in the STAContract should be equal to Alice's balance in the Vulnerable Vault Contract
// If they are not equal, it throws an error
assertEq(
STAContract.balanceOf(address(VulnVaultContract)),
VulnVaultContract.getBalance(alice)
);
}