Protection logic in rescueERC20
can be bypassed
Description
The sUSDz contract provides the staking feature for USDz holders. Specifically, a user can stake their USDz tokens into the sUSDz contract in order to receive the yield that is deposited by the yield manager. The yield manager deposits the full amount of yield when they start the new vesting period.
The sUSDz contract has the rescueERC20
function, which allows the pool manager to withdraw tokens that may be mistakenly transferred to this contract. To prevent users assets from being withdrawn, there is logic that ensures the deposited asset is not withdrawn through this function:
function rescueERC20(IERC20 token, address to, uint256 amount) external onlyRole(POOL_MANAGER_ROLE) {
// If is USDz, check pooled amount first.
if (address(token) == asset()) {
require(amount <= token.balanceOf(address(this)).sub(totalAssets()), "USDZ_RESCUE_AMOUNT_EXCEED_DEBIT");
}
token.safeTransfer(to, amount);
}
However, it should be noted that the return value of the totalAssets
function does not include the unvested amount:
function totalAssets() public view override returns (uint256) {
uint256 unvested = getUnvestedAmount();
return pooledUSDz.sub(unvested);
}
Impact
The pool manager can withdraw the part of yield that is not vested to users through the rescueERC20
function, despite of the protective logic referenced earlier.
Recommendations
Consider modifying the protection logic of the rescueERC20
function to ensure that USDz tokens of the amount pooledUSDz
is left in the contract.
Remediation
This issue has been acknowledged by Anzen Group Ltd., and a fix was implemented in commit e64b88de↗.