Summary
The Treasury::deposit()
function assumes the amount of tokens received equals the input amount parameter, which is incorrect for tokens that take fees on transfer or rebase balances.
Vulnerability Details
In the Treasury::deposit()
function, the contract directly adds the input amount
to its balance tracking:
function deposit(address token, uint256 amount) external override nonReentrant {
IERC20(token).transferFrom(msg.sender, address(this), amount);
@> _balances[token] += amount;
@> _totalValue += amount;
}
For tokens that:
The actual received amount will be less than the input amount. This creates a discrepancy between:
Impact
The mentioned discrepancy can lead to:
Tools Used
Manual review
Proof of Concept
Create a mock token that takes a 10% fee on transfers:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract ERC20MockFee is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function _update(
address from,
address to,
uint256 amount
) internal virtual override {
if (from != address(0) && to != address(0)) {
uint256 fee = amount / 10;
uint256 finalAmount = amount - fee;
super._update(from, to, finalAmount);
super._update(from, address(this), fee);
} else {
super._update(from, to, amount);
}
}
}
Add the following test to Treasury.test.js
:
it("fee-on-transfer tokens", async () => {
const MockFeeToken = await ethers.getContractFactory("ERC20MockFee");
const feeToken = await MockFeeToken.deploy("Fee Token", "FEE");
const initialAmount = ethers.parseEther("1000");
await feeToken.mint(user1.address, initialAmount);
await feeToken.connect(user1).approve(treasury.getAddress(), initialAmount);
const depositAmount = ethers.parseEther("100");
const feeAmount = depositAmount / 10n;
const actualReceived = depositAmount - feeAmount;
await treasury.connect(user1).deposit(feeToken.getAddress(), depositAmount);
expect(await treasury.getBalance(feeToken.getAddress())).to.equal(depositAmount);
expect(await treasury.getTotalValue()).to.equal(depositAmount);
expect(await feeToken.balanceOf(treasury.getAddress())).to.equal(actualReceived);
await expect(
treasury.connect(manager).withdraw(feeToken.getAddress(), depositAmount, manager.address)
).to.be.revertedWithCustomError(feeToken, "ERC20InsufficientBalance");
});
Recommendations
Track actual received amounts by comparing balances before and after transfer