CavalRe AMM
Smart Contract Security Assessment
July 10, 2023
SUMMARY
ABSTRACT
Dedaub was commissioned to audit the CavalRe protocol implementation, at https://github.com/CavalRe/amm. The protocol implements a novel automated market maker (AMM) design, which supports swaps and multiswaps, adding and removing liquidity as well as staking and unstaking.
SETTING AND CAVEATS
The audit report covers commit hash 5db05fa6eb281c1736fdbea70ac17a1e6b41a017.
The audit scope consists of the following files:
- LPToken.sol
- Pool.sol
The audit’s main target is security threats, i.e., what the community understanding would likely call "hacking", rather than regular use of the protocol. Functional correctness (i.e., issues in "regular use") is a secondary consideration. Functional correctness of most aspects (e.g., relative to low-level calculations, including units, scaling, quantities returned from external protocols) is generally most effectively done through thorough testing rather than human auditing. Importantly, thorough integration testing in the setting of final use is also an aspect that is not effectively covered by human auditing and remains the responsibility of the development team.
VULNERABILITIES & FUNCTIONAL ISSUES
This section details issues that affect the functionality of the contracts. Dedaub generally categorizes issues according to the following severities, but may also take other considerations into account such as impact or difficulty in exploitation:
- User or system funds can be lost when third-party systems misbehave.
- DoS, under specific conditions.
- Part of the functionality becomes unusable due to a programming error.
- Breaking important system invariants but without apparent consequences.
- Buggy functionality for trusted users where a workaround exists.
- Security issues which may manifest when the system evolves.
Issue resolution includes “dismissed” or “acknowledged” but no action taken, by the client, or “resolved”, per the auditors.
CRITICAL SEVERITY
[NO CRITICAL SEVERITY ISSUES]
HIGH SEVERITY
Resolved
4697d3cd
The LPToken contract inherits from the ERC20 contract. It then overrides both its external functions such as transfer, as well as its internal functions such as _transfer. Both the overridden external and internal functions adjust the amounts they receive before calling the corresponding version in the super contract. Now, if a user invokes an external overridden function such as LPToken::transfer, the amount is adjusted and then ERC20::transfer is called. Then ERC20::transfer will try to call ERC20::_transfer, but seeing this has been overridden, will call LPToken::_transfer instead, where the amount is adjusted again. Finally LPToken::_transfer will call ERC20::_transfer to finalise the transfer operation. As a result, the amount has been adjusted twice instead of once.
LPToken::approve and LPToken::_approve also suffer from the same issue.
Resolved
4697d3c
An error in the validation of Pool::multiswap fails to ensure that the LPToken only appears on one side of a multiswap.
The error occurs because the LPToken, unlike other tokens, is not tracked in the check_ array, and the isLP variable, which tracks that it has been seen among the payTokens, is reset before the receiveTokens are checked.
Pool::multiswapfunction multiswap(
address[] memory payTokens,
uint256[] memory amounts,
address[] memory receiveTokens,
uint256[] memory allocations
)
public
nonReentrant
onlyInitialized
onlyAllowed
returns (uint256[] memory receiveAmounts)
{
...
// Check duplicates
{
bool isLP;
uint256 temp;
bool[] memory check_ = new bool[](_assetAddress.length);
for (uint256 i; i < payTokens.length; i++) {
address token = payTokens[i];
if (token == address(0)) revert ZeroAddress();
if (address(this) == token) {
if (isLP) revert DuplicateToken(token);
isLP = true;
if (i != 0) {
payTokens[i] = payTokens[0];
payTokens[0] = address(this);
temp = amounts[i];
amounts[i] = amounts[0];
amounts[0] = temp;
}
//Dedaub: LPToken not added to check_
// because loop breaks early
continue;
}
AssetState memory asset_ = _assetState[token];
if (asset_.token != token) revert AssetNotFound(token);
if (check_[asset_.index]) revert DuplicateToken(token);
check_[asset_.index] = true;
}
//Dedaub: presence of LP token is erased
isLP = false;
for (uint256 i; i < receiveTokens.length; i++) {
address token = receiveTokens[i];
if (token == address(0)) revert ZeroAddress();
if (address(this) == token) {
if (isLP) revert DuplicateToken(token);
isLP = true;
if (i != 0) {
receiveTokens[i] = receiveTokens[0];
receiveTokens[0] = address(this);
temp = allocations[i];
allocations[i] = allocations[0];
allocations[0] = temp;
}
continue;
}
AssetState memory asset_ = _assetState[token];
if (asset_.token != token) revert AssetNotFound(token);
if (check_[asset_.index]) revert DuplicateToken(token);
check_[asset_.index] = true;
}
}
...
}