sherlock-2024-01-arcadia-h01
[H-01] AccountV1.flashActionByCreditor can be used to drain assets from account without withdrawing
Summary
AccountV1.flashActionByCreditor는 Account 컨트랙트의 owner 로부터 자금을 이동하는 아토믹 플래시 액션을 허용하도록 설계되었다. Account 컨트랙트를 자신의 owner 지정하면, Account 컨트랙트에서 ERC721 자산을 빼낼 수 있다. Account 컨트랙트에서 빠져나온 자산은 여전히 Account 컨트랙트에 예치된 것으로 표시되어, 실제로는 담보가 없더라도 채권자로부터 대출을 받을 수 있다.
Keyword
lending protocol, erc721, crypto theft
Vulnerability
익스플로잇의 개요는 다음과 같다.
- Uniswap V3 에 유동성을 제공하고 UniV3 Liquidity position ERC721 를 Account 컨트랙트에 예치
- 채권자를 악의적으로 설계된 채권자로 설정
- Account ERC721을 Account 컨트랙트(계정)에게 전송하여 계정의 owner를 자기 자신으로 설정
flashActionByCreditor를 호출하여 UniV3 Liquidity position ERC721을 전송- 계정이 자체 소유(owner 가 즉
address(this)) 이므로_transferFromOwner에서 이체를 허용한다. - 계정은 이제 비어 있지만 여전히 UniV3 Liquidity position ERC721 이 예치되어 있다고 생각한다.
- 계정이 자체 소유(owner 가 즉
- 악의적으로 설계된 청산자 컨트랙트를 사용하여
auctionBoughtIn을 호출한다. 계정을 다시 공격자에게 전송한다. - 채권자를 합법적인 채권자로 업데이트한다.
- 무상으로 대출을 받는다.
- 이익을 얻음
유저는 프로토콜에 참여하기 위해 각자 자신의 Account 컨트랙트를 배포한다. 그리고 Account를 소유한 증명으로 ERC721 NFT를 발급한다. NFT를 전송하면 Account 컨트랙트의 소유도 변경된다. 유저는 Uniswap V3 유동성을 제공하여 받은 UniV3 Liquidity position ERC721 을 계정에 예치하고 대출을 받을 수 있다.
이 익스플로잇의 핵심은 Account 컨트랙트(계정)가 자기 자신의 owner가 될 수 있다는 것이다. 공격자는 악의적으로 설계된 채권자(채권자는 무엇이든 설정할 수 있음)를 사용하여 flashActionByCreditor 를 호출할 수 있다.
flashActionByCreditor 에서 Account 컨트랙트에 예치된 UniV3 Liquidity position ERC721 을 꺼내갈 수 있다. 원래는 owner 계정의 자원을 사용하는 기능이지만, owner가 Account 컨트랙트 자신으로 설정되어 있기에 예치된 NFT를 꺼낼 수 있다.
function flashActionByCreditor(address actionTarget, bytes calldata actionData)
external
nonReentrant
notDuringAuction
updateActionTimestamp
returns (uint256 accountVersion)
{
// Cache the current Creditor.
address currentCreditor = creditor;
// The caller has to be or the Creditor of the Account, or an approved Creditor.
@> if (msg.sender != currentCreditor && msg.sender != approvedCreditor[owner]) revert AccountErrors.OnlyCreditor(); // 악의적인 채권자 사용하여 우회
...
// Transfer assets from owner (that are not assets in this account) to the actionTarget.
@> if (transferFromOwnerData.assets.length > 0) {
@> _transferFromOwner(transferFromOwnerData, actionTarget);
}
...
}
function _transferFromOwner(ActionData memory transferFromOwnerData, address to) internal {
uint256 assetAddressesLength = transferFromOwnerData.assets.length;
address owner_ = owner;
for (uint256 i; i < assetAddressesLength; ++i) {
if (transferFromOwnerData.assetAmounts[i] == 0) {
// Skip if amount is 0 to prevent transferring 0 balances.
continue;
}
if (transferFromOwnerData.assetTypes[i] == 0) {
ERC20(transferFromOwnerData.assets[i]).safeTransferFrom(
owner_, to, transferFromOwnerData.assetAmounts[i]
);
} else if (transferFromOwnerData.assetTypes[i] == 1) {
@> IERC721(transferFromOwnerData.assets[i]).safeTransferFrom(owner_, to, transferFromOwnerData.assetIds[i]);
} else if (transferFromOwnerData.assetTypes[i] == 2) {
IERC1155(transferFromOwnerData.assets[i]).safeTransferFrom(
owner_, to, transferFromOwnerData.assetIds[i], transferFromOwnerData.assetAmounts[i], ""
);
} else {
revert AccountErrors.UnknownAssetType();
}
}
}이후 악의적으로 설계된 청산자 컨트랙트를 사용하여 auctionBoughtIn 을 호출한다. Account ERC721를 전송하여 계정을 다시 공격자에게 전송할 수 있다.
function auctionBoughtIn(address recipient) external onlyLiquidator nonReentrant {
@> _transferOwnership(recipient);
}
function _transferOwnership(address newOwner) internal {
// The Factory will check that the new owner is not address(0).
owner = newOwner;
@> IFactory(FACTORY).safeTransferAccount(newOwner);
}공격자는 UniV3 Liquidity position ERC721 가 예치되어 있다고 간주되는(하지만 실제로는 존재하지 않는) 계정을 소유하게 되었다. 이제 채권자를 합법적인 풀로 설정하고, 담보 없이(UniV3 Liquidity position ERC721 없이) 대출을 받을 수 있다.
Impact
계정이 완전히 무담보 대출을 받을 수 있어 모든 대출 풀에 막대한 손실을 초래할 수 있다.
Mitigation
이 문제의 근본 원인은 Account 컨트랙트가 스스로를 소유할 수 있기 때문이다.
function transferOwnership(address newOwner) external onlyFactory notDuringAuction {
if (block.timestamp <= lastActionTimestamp + COOL_DOWN_PERIOD) revert AccountErrors.CoolDownPeriodNotPassed();
// The Factory will check that the new owner is not address(0).
+ if (newOwner == address(this)) revert NoTransferToSelf();
owner = newOwner;
}
function _transferOwnership(address newOwner) internal {
// The Factory will check that the new owner is not address(0).
+ if (newOwner == address(this)) revert NoTransferToSelf();
owner = newOwner;
IFactory(FACTORY).safeTransferAccount(newOwner);
}Memo
개요를 먼저 쓰고 설명을 하는 보고서 스타일 괜찮은 듯
tags: bughunting, arcadia, smart contract, solidity, solo issue, lending protocol, erc721, crypto theft, severity high