Summary
Treasury::deposit
function allows anyone to deposit a fee on transfer or rebasing token into the Treasury and badly influence the Treasury::_totalValue
as well as the particular balance of the deposited FoT or rebasing token.
Vulnerability Details
When you're interacting with standard ERC20 tokens, the transferFrom
function transfers the exact amount specified from the sender to the contract. However, fee-on-transfer tokens deduct a fee during the transfer process, meaning the actual amount received by the contract will be less than the amount specified in the transferFrom
call. In this code from the Treasury::deposit
function:
IERC20(token).transferFrom(msg.sender, address(this), amount);
_balances[token] += amount;
_totalValue += amount;
_balances[token]
and _totalValue
are incremented by the full amount, even though the contract received less due to the fee. This leads to inaccurate internal accounting.
Impact
Impact: High, as it makes important variables like Treasury::_totalValue
and Treasury::_balances
have wrong values
Likelihood: High, since every time a FoT (or rebasing) token is deposited into the treasury the variables become more disconnected from reality
PoC
Create a Mock token to replicate fee on transfer behaviour, add this code into /contracts/mocks/core/tokens
:
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockFeeOnTransferToken is ERC20 {
uint256 public feeBasisPoints;
constructor(
string memory name,
string memory symbol,
uint256 initialSupply,
uint256 _feeBasisPoints
) ERC20(name, symbol) {
feeBasisPoints = _feeBasisPoints;
_mint(msg.sender, initialSupply);
}
function mint(address account, uint256 amount) external {
_mint(account, amount);
}
function _update(
address sender,
address recipient,
uint256 amount
) internal virtual override {
if (sender == address(0)) {
super._update(sender, recipient, amount);
} else if (recipient == address(0)) {
super._update(sender, recipient, amount);
} else {
uint256 fee = (amount * feeBasisPoints) / 10000;
uint256 amountAfterFee = amount - fee;
super._update(sender, recipient, amountAfterFee);
super._update(sender, address(0), fee);
}
}
}
2 . Add this to the beforeEach
of /test/unit/core/collectors/Treasury.test.js
in order to have the Mock fee token deployed and ready for the next step
const FeeMockToken = await ethers.getContractFactory("MockFeeOnTransferToken");
feeToken = await FeeMockToken.deploy("Test Fee Token", "Fee", ethers.parseEther("1000000"), 100);
await feeToken.waitForDeployment();
await feeToken.mint(user1.address, ethers.parseEther("1000"));
await feeToken.mint(user2.address, ethers.parseEther("1000"));
3 . To validate the internal accounting is wrong for such tokens - add this test into /test/unit/core/collectors/Treasury.test.js
under the Deposits
describe section:
it("should demonstrate miscalculation of internal balances with fee-on-transfer tokens", async function () {
const depositAmount = ethers.parseEther("1000");
await feeToken.connect(user1).approve(treasury.getAddress(), depositAmount);
await treasury.connect(user1).deposit(feeToken.getAddress(), depositAmount);
const fee = depositAmount * 1n / 100n;
const expectedReceived = depositAmount - fee;
const actualTreasuryBalance = await feeToken.balanceOf(treasury.getAddress());
expect(actualTreasuryBalance).to.equal(expectedReceived);
const storedBalance = await treasury.getBalance(feeToken.getAddress());
const totalValue = await treasury.getTotalValue();
expect(storedBalance).to.not.equal(actualTreasuryBalance);
expect(totalValue).to.not.equal(actualTreasuryBalance);
});
Tools Used
Manual Review
Recommendations
To ensure _totalValue
and _balances[token]
reflects the actual amount received by the contract, you can compare the contract's balance before and after the transfer:
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
IERC20(token).transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = IERC20(token).balanceOf(address(this));
uint256 receivedAmount = balanceAfter - balanceBefore;
_balances[token] += receivedAmount;
_totalValue += receivedAmount;
emit Deposited(token, receivedAmount);