code4rena-2025-01-liquid-ron-h01
[H-01] The calculation of totalAssets() could be wrong if operatorFeeAmount > 0, this can cause potential loss for the new depositors
Summary
관리자 수수료가 totalAssets 조회량에서 제외되지 않아 실제보다 크게 잡힌다. 이로 인해 잘못된 양의 리워드가 분배될 수 있다.
Keyword
logic flaw, arithmetic error
Vulnerability
harvest 를 호출하면 스테이킹 리워드를 수확하여 WRON 토큰으로 래핑 후 Vault 에 전송한다. 그리고 수수료 n% 만큼을 계산하여 operatorFeeAmount 에 누적한다. 추후 수수료 수신자가 operatorFeeAmount 에 누적된 만큼 WRON을 출금해갈 수 있다.
harvestAndDelegateRewards 를 호출한다면 수확된 리워드는 Vault 컨트랙트로 전송되지 않고, _consensusAddrDst 에게 위임된다. 즉, 스테이킹된 토큰의 양이 증가한다.
function harvest(uint256 _proxyIndex, address[] calldata _consensusAddrs) external onlyOperator whenNotPaused {
uint256 harvestedAmount = ILiquidProxy(stakingProxies[_proxyIndex]).harvest(_consensusAddrs);
@> operatorFeeAmount += (harvestedAmount * operatorFee) / BIPS;
emit Harvest(_proxyIndex, harvestedAmount);
}
function harvestAndDelegateRewards(
uint256 _proxyIndex,
address[] calldata _consensusAddrs,
address _consensusAddrDst
) external onlyOperator whenNotPaused {
_tryPushValidator(_consensusAddrDst);
uint256 harvestedAmount = ILiquidProxy(stakingProxies[_proxyIndex]).harvestAndDelegateRewards(
_consensusAddrs,
_consensusAddrDst
);
@> operatorFeeAmount += (harvestedAmount * operatorFee) / BIPS;
emit Harvest(_proxyIndex, harvestedAmount);
}
function fetchOperatorFee() external {
if (msg.sender != feeRecipient) revert ErrNotFeeRecipient();
uint256 amount = operatorFeeAmount;
operatorFeeAmount = 0;
@> _withdrawRONTo(feeRecipient, amount);
}그런데, 유저가 출금할 수 있는 WRON의 양을 계산할 때 사용되는 totalAssets 함수에서는 Vault에 예치된 WRON 양 + 스테이킹된 RON 양 + 아직 수확하지 않은 리워드 양 을 리턴한다. 즉, operatorFeeAmount 는 예치된 WRON으로 볼 수 없지만, harvest 의 경우 super.totalAssets 에, harvestAndDelegateRewards 의 경우 getTotalStaked 에 operatorFeeAmount 가 포함되어버린다.
function totalAssets() public view override returns (uint256) {
@> return super.totalAssets() + getTotalStaked() + getTotalRewards();
}
function getTotalRewards() public view returns (uint256) {
address[] memory consensusAddrs = _getValidators();
uint256 proxyCount = stakingProxyCount;
uint256 totalRewards;
uint256 totalFees;
for (uint256 i = 0; i < proxyCount; i++) totalRewards += _getTotalRewardsInProxy(i, consensusAddrs);
@> totalFees = (totalRewards * operatorFee) / BIPS;
return totalRewards - totalFees;
}
function getTotalStaked() public view returns (uint256) {
address[] memory consensusAddrs = _getValidators();
uint256 proxyCount = stakingProxyCount;
uint256 totalStaked;
for (uint256 i = 0; i < proxyCount; i++) totalStaked += _getTotalStakedInProxy(i, consensusAddrs);
return totalStaked;
}Impact
이로 인해 다음 문제가 발생할 수 있다.
- 리워드를 수확하기 전
totalAssets에서는getTotalRewards에서 수수료만큼을 제외한다. 하지만harvest호출 후에는 수수료가super.totalAssets(balanceOf 호출값)에 포함되고,harvestAndDelegateRewards호출 후에는getTotalStaked에 포함된다.- 즉, 리워드를 수확하고 수수료를 아직 출금하기 전에는 수수료만큼
totalAssets이 증가한다. 따라서 리워드 수확 이후에 출금을 하면 원래 받아야 하는 것보다 더 많은 토큰을 받을 수 있다.
- 즉, 리워드를 수확하고 수수료를 아직 출금하기 전에는 수수료만큼
- 이는
fetchOperatorFee의 호출 전후의 출금양에도 차이를 만든다.fetchOperatorFee보다 먼저 출금 요청을 한다면totalAssets가 원래보다 크게 간주되어 동일한 share로 더 많은 양의 WRON을 출금할 수 있다.
Mitigation
function totalAssets() public view override returns (uint256) {
- return super.totalAssets() + getTotalStaked() + getTotalRewards();
+ return super.totalAssets() + getTotalStaked() + getTotalRewards() - operatorFeeAmount;
}tags: bughunting, liquid ron, smart contract, solidity, logic flaw, arithmetic error, severity high