Forked protocols are not battle-tested: Agave Uninitialized Proxy Vulnerability

Hacxyk
4 min readAug 5, 2022

--

Having a similar name, Agave.finance is a forked from Aave V2 on the Gnosis chain. One would think that a fork of a battle-tested protocol is secure, but on May 23rd, we discovered an uninitialized proxy vulnerability on Agave that was supposed to be long patched in the Aave V2 codebase after Trail of Bits’ report.

The bug itself is not particularly interesting, but we want to warn developers to make sure they understand how to properly fork and configure a protocol, and users to not blindly believe forked battle-tested protocols are battle-tested.

The bug

The bug is rather simple and Trail of Bits explained it pretty well. Here’s a quick recap: LendingPool.sol is an implementation contract that is behind a proxy. It has an initialize function that changes the _addressesProvider property. The initializer modifier allows calling this function by anyone if it has never been called before.

function initialize(ILendingPoolAddressesProvider provider) public initializer {
_addressesProvider = provider;
}

Normally, this wouldn’t be a problem even if anyone could call initialize on an implementation contract. This is because an implementation contract only provides the necessary logic for the proxy contract to function - The actual data is stored in a proxy contract. Making state changes to the implementation contract has absolutely no effects on the logic.

There is one caveat. If an implementation contract itself uses delegatecall and an attacker is able to make it call an attacker’s contract, the attacker’s contract can then call selfdestruct on behalf of the implementation contract, and subsequently destroys the implementation contract.

diagram we drew

This is exactly the case for Aave. The liquidationCall function uses delegatecall to any attacker selected address (returned via _addressesProvider.getLendingPoolCollateralManager) .

function liquidationCall(
address collateralAsset,
address debtAsset,
address user,
uint256 debtToCover,
bool receiveAToken
) external override whenNotPaused {
address collateralManager = _addressesProvider.getLendingPoolCollateralManager();
//solium-disable-next-line
(bool success, bytes memory result) =
collateralManager.delegatecall(
abi.encodeWithSignature(
'liquidationCall(address,address,address,uint256,bool)',
collateralAsset,
debtAsset,
user,
debtToCover,
receiveAToken
)
);
require(success, Errors.LP_LIQUIDATION_CALL_FAILED);
(uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string)); require(returnCode == 0, string(abi.encodePacked(returnMessage)));
}

The impact we identified was not devastating. It did not allow draining all deposited funds on Agave. Nevertheless, it still allowed stealing new xDAI(ETH) deposits via WETHGateway.sol.

When users call depositETH to deposit ETH, nothing will happen since POOL is destroyed, and the ETH will be left in the WETHGateway contract.

function depositETH(address onBehalfOf, uint16 referralCode) external payable override {
WETH.deposit{value: msg.value}();
POOL.deposit(address(WETH), msg.value, onBehalfOf, referralCode);
}

After that, the attacker can call borrowETH to transfer the leftover ETH to themselves, as POOL.borrow no longer imposes any validation.

function borrowETH(
uint256 amount,
uint256 interesRateMode,
uint16 referralCode
) external override {
POOL.borrow(address(WETH), amount, interesRateMode, referralCode, msg.sender);
WETH.withdraw(amount);
_safeTransferETH(msg.sender, amount);
}

Discovery and report

After finding several bugs on various lending protocols, we decided to check all Compound/Aave forks. One thing came to mind was to look for bugs that are fixed in the codebase, but continue to exist in the old versions.

In order to get a list of forked protocols, we used DefiLlama and chose filtered by Aave.

As we went through the list, we noticed Agave did not initialize the implementation of the LendingPool contract. We quickly wrote up a PoC and reported it to Agave. The developer responded they were aware of the issue, but did not have time to fix it as they believed the issue was only exploitable from an admin. We explained to them anyone could exploit/fix it. After getting approval, we helped initialize the contract and point addressesProvider to a NULL address and the issue was resolved.

https://blockscout.com/xdai/mainnet/tx/0x729a26603f6e9a3781aacbe8303bc091a19c43a47c6cffba865efb71ce5e3394

Interestingly, of all the forked Aave projects, only Agave was affected. We came to the conclusion that Agave was using older deployment scripts that were still vulnerable. Specifically, commits before Mar 31, 2021.

Timeline

May 23rd — Issue reported to Agave via Discord
May 23rd — Hacxyk fixed the issue by initializing the contract
July 26rd — Agave awarded $20k for the issue

--

--