Summary
The _updateStrategyRewards function called by updateStrategyRewards has a vulnerability where the return value is ignored. This can lead to undetected failures in token transfers.
Vulnerability Details
transferAndCallFrom is called in the _updateStrategyRewards function but ignores the return value. This can cause the contract to assume the transfer was successful when it actually failed.
function _updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) private {
int256 totalRewards;
uint256 totalFeeAmounts;
uint256 totalFeeCount;
address[][] memory receivers = new address[][]();
uint256[][] memory feeAmounts = new uint256[][]();
for (uint256 i = 0; i < _strategyIdxs.length; ++i) {
IStrategy strategy = IStrategy(strategies[_strategyIdxs[i]]);
(
int256 depositChange,
address[] memory strategyReceivers,
uint256[] memory strategyFeeAmounts
) = strategy.updateDeposits(_data);
totalRewards += depositChange;
if (strategyReceivers.length != 0) {
receivers[i] = strategyReceivers;
feeAmounts[i] = strategyFeeAmounts;
totalFeeCount += receivers[i].length;
for (uint256 j = 0; j < strategyReceivers.length; ++j) {
totalFeeAmounts += strategyFeeAmounts[j];
}
}
}
if (totalRewards != 0) {
totalStaked = uint256(int256(totalStaked) + totalRewards);
}
if (totalRewards > 0) {
receivers[receivers.length - 1] = new address[]();
feeAmounts[feeAmounts.length - 1] = new uint256[]();
totalFeeCount += fees.length;
for (uint256 i = 0; i < fees.length; i++) {
receivers[receivers.length - 1][i] = fees[i].receiver;
feeAmounts[feeAmounts.length - 1][i] =
(uint256(totalRewards) * fees[i].basisPoints) /
10000;
totalFeeAmounts += feeAmounts[feeAmounts.length - 1][i];
}
}
if (totalFeeAmounts >= totalStaked) {
totalFeeAmounts = 0;
}
if (totalFeeAmounts > 0) {
uint256 sharesToMint = (totalFeeAmounts * totalShares) /
(totalStaked - totalFeeAmounts);
_mintShares(address(this), sharesToMint);
uint256 feesPaidCount;
for (uint256 i = 0; i < receivers.length; i++) {
for (uint256 j = 0; j < receivers[i].length; j++) {
if (feesPaidCount == totalFeeCount - 1) {
@=> transferAndCallFrom(
address(this),
receivers[i][j],
balanceOf(address(this)),
"0x"
);
} else {
@=> transferAndCallFrom(address(this), receivers[i][j], feeAmounts[i][j], "0x");
feesPaidCount++;
}
}
}
}
emit UpdateStrategyRewards(msg.sender, totalStaked, totalRewards, totalFeeAmounts);
}
Test
pragma solidity 0.8.25;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "../contracts/core/StakingPool.sol";
import "../contracts/core/RebaseController.sol";
import "../contracts/core/interfaces/IStakingPool.sol";
import "../contracts/core/interfaces/IStrategy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
contract MockToken is ERC20 {
constructor() ERC20("Mock Token", "MTKN") {}
function initialize() external {
_mint(msg.sender, 1000000 * 10**decimals());
}
}
contract StakingPoolTest is Test {
StakingPool stakingPool;
MockToken mockToken;
address mockStrategy;
address mockReceiver;
function setUp() public {
mockToken = new MockToken();
mockToken.initialize();
StakingPool stakingPoolImplementation = new StakingPool();
ERC1967Proxy proxy = new ERC1967Proxy(
address(stakingPoolImplementation),
abi.encodeWithSelector(
StakingPool.initialize.selector,
address(mockToken),
"Liquid Token",
"LTKN",
new StakingPool.Fee[](0),
1000
)
);
stakingPool = StakingPool(address(proxy));
mockStrategy = address(0x123);
mockReceiver = address(0x456);
mockToken.approve(address(proxy), type(uint256).max);
stakingPool.addStrategy(mockStrategy);
stakingPool.setRebaseController(address(this));
}
function testIgnoredReturnValueImpact() public {
emit log_named_uint("Initial total staked", stakingPool.totalStaked());
vm.mockCall(
mockStrategy,
abi.encodeWithSelector(IStrategy.updateDeposits.selector),
abi.encode(int256(-100), new address[](0), new uint256[]())
);
vm.mockCall(
address(stakingPool),
abi.encodeWithSignature("transferAndCallFrom(address,address,uint256,bytes)", address(this), mockReceiver, 0, ""),
abi.encode(false)
);
stakingPool.updateStrategyRewards(new uint256[](1), "");
emit log_named_uint("Total staked after update", stakingPool.totalStaked());
emit log("Test passed regardless of total staked value");
}
}
Logs:
Initial total staked: 0
Total staked after update: 115792089237316195423570985008687907853269984665640564039457584007913129639836
Test passed regardless of total staked value
Impact
Users get more than they had even though the transfer failed.
Tools Used
Recommendations
Ensure that the return value of transferAndCallFrom is checked and handled appropriately.
function _updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) private {
int256 totalRewards;
uint256 totalFeeAmounts;
uint256 totalFeeCount;
address[][] memory receivers = new address[][]();
uint256[][] memory feeAmounts = new uint256[][]();
// Another logic
if (totalFeeAmounts > 0) {
uint256 sharesToMint = (totalFeeAmounts * totalShares) /
(totalStaked - totalFeeAmounts);
_mintShares(address(this), sharesToMint);
uint256 feesPaidCount;
for (uint256 i = 0; i < receivers.length; i++) {
for (uint256 j = 0; j < receivers[i].length; j++) {
bool success;
if (feesPaidCount == totalFeeCount - 1) {
- transferAndCallFrom(
+ success = transferAndCallFrom(
address(this),
receivers[i][j],
balanceOf(address(this)),
"0x"
);
+ require(success, "Final transfer and call failed");
} else {
- transferAndCallFrom(address(this), receivers[i][j], feeAmounts[i][j], "0x");
+ success = transferAndCallFrom(
+ address(this),
+ receivers[i][j],
+ feeAmounts[i][j],
+ "0x"
);
+ require(success, "Transfer and call failed");
feesPaidCount++;
}
}
}
}
emit UpdateStrategyRewards(msg.sender, totalStaked, totalRewards, totalFeeAmounts);
}