code4rena-2023-09-centrifuge-m04
[M-04] You can deposit really small amount for other users to DoS them
Summary
deposit 함수와 mint 함수는 접근 제한이 없다. 유저가 입금액에 대한 트랜치 토큰을 요청했을 때 프론트러닝으로 해당 유저에게 트랜치 토큰을 소액 발행시키면 유저의 트랜잭션은 실패한다. 소액을 선수쳐서 보내어 유저가 최대로 요청 가능한 토큰 개수를 줄인다. 유저의 요청은 최대값을 넘기 때문에 실패한다.
Keyword
dos, griefing attack, frontrunning
Vulnerability
트랜치 토큰을 발행하기 위해서는 먼저 토큰을 입금하고 브릿지를 통해 Centrifuge 체인에게 입금을 알린다. 다음 epoch부터는 입금한 asset 토큰 금액 내에서 트랜치 토큰을 수령할 수 있다.
deposit 함수와 mint 함수는 동일한 역할을 하며, 수령 요청할 트랜치 토큰 개수를 asset 토큰을 기준으로 계산할 지(예를 들어 10 asset 토큰 상당의 트랜치 토큰을 달라고 요청. 10 asset 토큰 상당의 트랜치 토큰 개수를 계산해야 함), 트랜치 토큰 수를 바로 입력할 지(10개의 트랜치 토큰을 달라고 요청. 입금액이 충분한지 계산해야 함)에 따라 선택한다.
LP의 deposit 함수와 mint 함수는 아무나 호출 가능하다. 호출자가 receiver일 필요도 없다. 이미 receiver가 입금을 했다면 이를 트랜치 토큰으로 변환해주는 작업은 누구나 할 수 있다.
function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
shares = investmentManager.processDeposit(receiver, assets);
emit Deposit(address(this), receiver, assets, shares);
}
function mint(uint256 shares, address receiver) public returns (uint256 assets) {
assets = investmentManager.processMint(receiver, shares);
emit Deposit(address(this), receiver, assets, shares);
}즉, investmentManager에서 processDeposit와 processMint의 호출자를 제한하긴 하지만 결과적으로 아무나 호출 가능하다는 것을 의미한다.
function processDeposit(address user, uint256 currencyAmount) public auth returns (uint256 trancheTokenAmount) {
address liquidityPool = msg.sender;
uint128 _currencyAmount = _toUint128(currencyAmount);
require(
(_currencyAmount <= orderbook[user][liquidityPool].maxDeposit && _currencyAmount != 0),
"InvestmentManager/amount-exceeds-deposit-limits"
);
uint256 depositPrice = calculateDepositPrice(user, liquidityPool);
require(depositPrice != 0, "LiquidityPool/deposit-token-price-0");
uint128 _trancheTokenAmount = _calculateTrancheTokenAmount(_currencyAmount, liquidityPool, depositPrice);
@> _deposit(_trancheTokenAmount, _currencyAmount, liquidityPool, user);
trancheTokenAmount = uint256(_trancheTokenAmount);
}
function processMint(address user, uint256 trancheTokenAmount) public auth returns (uint256 currencyAmount) {
address liquidityPool = msg.sender;
uint128 _trancheTokenAmount = _toUint128(trancheTokenAmount);
require(
(_trancheTokenAmount <= orderbook[user][liquidityPool].maxMint && _trancheTokenAmount != 0),
"InvestmentManager/amount-exceeds-mint-limits"
);
uint256 depositPrice = calculateDepositPrice(user, liquidityPool);
require(depositPrice != 0, "LiquidityPool/deposit-token-price-0");
uint128 _currencyAmount = _calculateCurrencyAmount(_trancheTokenAmount, liquidityPool, depositPrice);
@> _deposit(_trancheTokenAmount, _currencyAmount, liquidityPool, user);
currencyAmount = uint256(_currencyAmount);
}
function _deposit(uint128 trancheTokenAmount, uint128 currencyAmount, address liquidityPool, address user)
internal
{
LiquidityPoolLike lPool = LiquidityPoolLike(liquidityPool);
@> _decreaseDepositLimits(user, liquidityPool, currencyAmount, trancheTokenAmount); // decrease the possible deposit limits
require(lPool.checkTransferRestriction(msg.sender, user, 0), "InvestmentManager/trancheTokens-not-a-member");
require(
lPool.transferFrom(address(escrow), user, trancheTokenAmount),
"InvestmentManager/trancheTokens-transfer-failed"
);
emit DepositProcessed(liquidityPool, user, currencyAmount);
}
function _decreaseDepositLimits(address user, address liquidityPool, uint128 _currency, uint128 trancheTokens)
internal
{
LPValues storage lpValues = orderbook[user][liquidityPool];
if (lpValues.maxDeposit < _currency) {
lpValues.maxDeposit = 0;
} else {
@> lpValues.maxDeposit = lpValues.maxDeposit - _currency;
}
if (lpValues.maxMint < trancheTokens) {
lpValues.maxMint = 0;
} else {
@> lpValues.maxMint = lpValues.maxMint - trancheTokens;
}
}유저가 deposit 또는 mint를 호출하여 멤풀에 트랜잭션이 있을 때, 프론트러닝을 통해 1 wei 정도의 소량의 토큰을 deposit 또는 mint 한다. 프론트러닝으로 먼저 트랜치 토큰을 소량 발행하면 _decreaseDepositLimits에서 발행한 만큼 입금액과 최대 민팅 가능 개수를 업데이트 한다.
이로 인해 유저가 요청한, 멤풀에 머물고 있는 트랜잭션에서 유저가 소비하는 asset 토큰 양과 수령할 트랜치 토큰 양이 maxDeposit와 maxMint보다 많아지게 되며, 트랜잭션은 취소된다.
Impact
유저를 방해하여 프로토콜을 저해한다.
Mitigation
LP의 deposit과 mint에 접근 제한을 두어 msg.sender가 즉 receiver일 것을 강제한다. (withApproval modifier는 이미 코드에 있는 것을 재활용)
- function deposit(uint256 assets, address receiver) public returns (uint256 shares) {
+ function deposit(uint256 assets, address receiver) public returns (uint256 shares) withApproval(receiver) {
shares = investmentManager.processDeposit(receiver, assets);
emit Deposit(address(this), receiver, assets, shares);
}
- function mint(uint256 shares, address receiver) public returns (uint256 assets) {
+ function mint(uint256 shares, address receiver) public returns (uint256 assets) withApproval(receiver) {
assets = investmentManager.processMint(receiver, shares);
emit Deposit(address(this), receiver, assets, shares);
}tags: bughunting, centrifuge, smart contract, solidity, dos, griefing attack, frontrunning, severity medium