sherlock-2025-04-burve-h09
[H-09] Reserve Share Overflows Due to Too Strict Reward Calculation Mechanism
Summary
trimBalance는 모든 상호작용을 할 때 먼저 호출되는 함수이다. 재투자에서 발생한 이자를 인출하여 Reserve 로 옮긴다. 이때 소액을 반복하여 호출하면 reserve.shares를 증폭시킬 수 있고, 끝내 오버플로우가 될 만큼 증폭시켜 전체 컨트랙트를 완전히 DoS할 수 있다.
Keyword
integer overflow underflow, arithmetic error, dos
Vulnerability
프로토콜과 상호작용할 때마다 trimBalance를 호출해 재투자로 인해 발생한 이자를 정산한다. 정산된 이자는 재투자 볼트에서 꺼내 수입을 예치하는 Reserve 라는 특수 Vertex의 볼트로 입금한다.
발생한 이자(bgtResidual - residualReal)만큼을 파라미터로 ReserveLib.deposit를 호출하는데, 이때 이 값이 0에 가깝다면(즉, 발생한 이자가 0에 가깝다면) 문제가 발생할 수 있다.
function trimBalance(
Vertex storage self,
ClosureId cid,
uint256 targetReal,
uint256 value,
uint256 bgtValue
) internal returns (uint256 reserveSharesEarned, uint256 bgtResidual) {
VaultProxy memory vProxy = VaultLib.getProxy(self.vid);
@> uint256 realBalance = vProxy.balance(cid, false); // 재투자 vault의 잔고
// We don't error and instead emit in this scenario because clearly the vault is not working properly but if
// we error users can't withdraw funds. Instead the right response is to lock and move vaults immediately.
if (targetReal > realBalance) {
emit InsufficientBalance(self.vid, cid, targetReal, realBalance);
return (0, 0);
}
@> uint256 residualReal = realBalance - targetReal; // 재투자하여 발생한 이자 계산
vProxy.withdraw(cid, residualReal); // 이자 출금
@> bgtResidual = FullMath.mulDiv(residualReal, bgtValue, value);
reserveSharesEarned = ReserveLib.deposit(
vProxy,
self.vid,
@> residualReal - bgtResidual
);
vProxy.commit();
}다음은 ReserveLib.deposit이다. 이 함수에서는 각 Vertex(토큰)에서 발생한 이자의 share를 추적한다. amount는 이번에 정산한 이자의 양이고 balance는 그동안 정산하고 아직 분배되지 않은(Reserve에 예치된) 이자의 양이다.
만약 amount > 0이고 balance 가 0에 가깝다면 shares 크게 부풀려질 수 있다. 조작된 값으로 trimBalance를 반복적으로 호출하면(소량 removeValue 반복 호출을 통해) 이러한 인플레이션이 쌓여 증폭되고, 결국 reserve.shares[idx] 가 오버플로우될 수 있는 수준에 도달한다. 즉, 공격자가 소액을 입금하고 그 소액에 대해 반복적으로 trimBalance를 호출하면 시스템에 대한 완전한 DoS를 일으킬 수 있다.
function deposit(
VaultProxy memory vProxy,
VertexId vid,
uint256 amount // 이번에 정산한 이자 양
) internal returns (uint256 shares) {
Reserve storage reserve = Store.reserve();
uint8 idx = vid.idx();
@> uint128 balance = vProxy.balance(RESERVEID, true);
vProxy.deposit(RESERVEID, amount);
// If someone tries to share inflate attack this, they'd have to donate to the underlying vault,
// which then splits the donation across existing deposits from other people using the vault,
// including the other closures. So there's no way to inflate shares here.
shares = (balance == 0)
? amount * SHARE_RESOLUTION
@> : (amount * reserve.shares[idx]) / balance; // No need for mulDiv.
@> reserve.shares[idx] += shares;
}Impact
오버플로우 될 수 있을만큼 reserve.shares 를 증폭하여 컨트랙트가 작동할 수 없도록 한다.
Mitigation
trimBalance를 매번 호출하지 말고, 최소 금액을 넘었을 때에만 호출한다.
Memo
오버플로우가 되는 부분이 잘 이해가 안된다. 중첩이 되면 오버플로우가 될 수 있는 건 맞으나 현실적으로 오버플로우가 가능한 수준으로 끌어올릴 수 있는지, 다른 조건을 피할 필요는 없는지? PoC도 이해가 잘 안된다. 볼트에 지속적으로 금액을 입금해야 이자가 발생한 것으로 인식할 것 같은데…
tags: bughunting, burve, smart contract, solidity, severity high, solo issue, integer overflow underflow, arithmetic error, dos