Additions over the CurveTwoCryptoOptimized Vyper code
As initially stated, our security review focused on the changes made by the SyncSwap Labs team when translating the Vyper version of Curve's TwocryptoOptimized pool↗ to Solidity. Below, we list the most notable changes to the overall architecture of the original code.
1. Possibility of callbacks
SyncSwap allows callbacks to contracts that implement a predefined interface. For example, in swap
, the following operation occurs:
function swap(bytes memory _data, address _sender, _callback, _callbackData) {
// ...
if (_callback != address(0)) {
// Fills additional values for callback params.
params.sender = _sender;
params.callbackData = _callbackData;
// Calculates fee amount for callback.
params.feeIn = Math.mulDivUnsafeFirstLast(params.amountIn, params.swapFee, 1e5);
ICallback(_callback).syncSwapBaseSwapCallback(params);
}
}
This addition allows for more flexibility in the pool's usage and is not present in the Curve Vyper code. We note, however, that due to the nature of reentrant capabilities of callbacks, this addition may introduce additional attack vectors in the future and should be carefully considered.
The actual interface is defined in the ICallback
interface and is as follows:
// SPDX-License-Identifier: AGPL-3.0-or-later
pragma solidity >=0.5.0;
/// @dev The callback interface for SyncSwap base pool operations.
/// Note additional checks will be required for some callbacks, see below for more information.
/// Visit the documentation https://syncswap.gitbook.io/api-documentation/ for more details.
interface ICallback {
struct BaseMintCallbackParams {
address sender;
address to;
uint reserve0;
uint reserve1;
uint balance0;
uint balance1;
uint amount0;
uint amount1;
uint fee0;
uint fee1;
uint newInvariant;
uint oldInvariant;
uint totalSupply;
uint liquidity;
uint24 swapFee;
bytes callbackData;
}
function syncSwapBaseMintCallback(BaseMintCallbackParams calldata params) external;
struct BaseBurnCallbackParams {
address sender;
address to;
uint balance0;
uint balance1;
uint liquidity;
uint totalSupply;
uint amount0;
uint amount1;
uint8 withdrawMode;
bytes callbackData;
}
function syncSwapBaseBurnCallback(BaseBurnCallbackParams calldata params) external;
struct BaseBurnSingleCallbackParams {
address sender;
address to;
address tokenIn;
address tokenOut;
uint balance0;
uint balance1;
uint liquidity;
uint totalSupply;
uint amount0;
uint amount1;
uint amountOut;
uint amountSwapped;
uint feeIn;
uint24 swapFee;
uint8 withdrawMode;
bytes callbackData;
}
/// @dev Note the `tokenOut` parameter can be decided by the caller, and the correctness is not guaranteed.
/// Additional checks MUST be performed in callback to ensure the `tokenOut` is one of the pools tokens if the sender
/// is not a trusted source to avoid potential issues.
function syncSwapBaseBurnSingleCallback(BaseBurnSingleCallbackParams calldata params) external;
struct BaseSwapCallbackParams {
address sender;
address to;
address tokenIn;
address tokenOut;
uint reserve0;
uint reserve1;
uint balance0;
uint balance1;
uint amountIn;
uint amountOut;
uint feeIn;
uint24 swapFee;
uint8 withdrawMode;
bytes callbackData;
}
/// @dev Note the `tokenIn` parameter can be decided by the caller, and the correctness is not guaranteed.
/// Additional checks MUST be performed in callback to ensure the `tokenIn` is one of the pools tokens if the sender
/// is not a trusted source to avoid potential issues.
function syncSwapBaseSwapCallback(BaseSwapCallbackParams calldata params) external;
}
2. Usage of a Router contract
The pool is designed to be used through a Router contract, which is responsible for calling the pool's functions as well as performing additional checks (such as slippage checks).
Additionally, the transfer of tokens is done through the Router contract atomically with the pool's operations. From the user's perspective, the Router contract is the only contract that interacts with the pool, and the user only needs to interact with the Router contract.
3. Interface differences
We note that when compared to the default CurveTwoCryptoOptimized Vyper code, the Solidity version has a drastically different interface. Functions, events, and variable names are different. Some functions also have different parameters.
4. Precision differences
The fee precision has been changed from 1e10
to 1e5
.