Merge pull request #1 from propeller-heads/dev-integration/mp/ENG-1937-Move-a-second-evm-implementation-to-this-interface
ENG-1937 Implement Balancer V2 with SwapAdapter interface
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"solidity.formatter": "forge",
|
"solidity.formatter": "forge",
|
||||||
"solidity.compileUsingRemoteVersion": "v0.8.19",
|
"solidity.compileUsingRemoteVersion": "v0.8.20",
|
||||||
"solidity.packageDefaultDependenciesContractsDirectory": "evm/src",
|
"solidity.packageDefaultDependenciesContractsDirectory": "evm/src",
|
||||||
"solidity.packageDefaultDependenciesDirectory": "evm/lib",
|
"solidity.packageDefaultDependenciesDirectory": "evm/lib",
|
||||||
}
|
}
|
||||||
@@ -2,25 +2,27 @@
|
|||||||
|
|
||||||
Protocol lib is a library used by Propellerheads.xyz solvers to integrate decentralized protocols. Currently, only swap/exchange protocols are supported.
|
Protocol lib is a library used by Propellerheads.xyz solvers to integrate decentralized protocols. Currently, only swap/exchange protocols are supported.
|
||||||
|
|
||||||
### Integration Process
|
## Integration Process
|
||||||
|
|
||||||
To integrate with PropellerHeads solvers, two components need to be provided:
|
To integrate with PropellerHeads solvers, two components need to be provided:
|
||||||
|
|
||||||
* **Protocol logic:** Provides simulations, of the protocols logic.
|
* **Protocol logic:** Provides simulations of the protocols logic.
|
||||||
* **Indexing**: Provides access to the protocol state used by the simulation. This component is optional if your protocol is stateless.
|
* **Indexing**: Provides access to the protocol state used by the simulation. This component is optional if your protocol is stateless.
|
||||||
|
|
||||||
#### Protocol Logic
|
To propose an integration, create a pull request in this repository with the above components implemented.
|
||||||
|
|
||||||
|
### Protocol Logic
|
||||||
|
|
||||||
PropellerHeads currently exposes two integration modes to specify the protocols' underlying logic:
|
PropellerHeads currently exposes two integration modes to specify the protocols' underlying logic:
|
||||||
|
|
||||||
* **VM Integration:** This integration type requires implementing an adapter interface in any language that compiles to the respective vm byte code. Currently, only Solidity is supported.
|
* **VM Integration:** This integration type requires implementing an adapter interface in any language that compiles to the respective vm byte code. This SDK provides the interface only in Solidity. **[Read more here.](logic/vm-integration/README.md)**
|
||||||
* **Native Rust Integration:** Coming soon, this integration type requires implementing a Rust trait that describes the protocol logic.
|
* **Native Rust Integration:** Coming soon, this integration type requires implementing a Rust trait that describes the protocol logic.
|
||||||
|
|
||||||
While VM integration is certainly the quickest and probably most accessible one for protocol developers, native implementations are much faster and allow us to consider the protocol for more time-sensitive use cases - e.g. quoting.
|
While VM integration is certainly the quickest and probably most accessible one for protocol developers, native implementations are much faster and allow us to consider the protocol for more time-sensitive use cases - e.g. quoting.
|
||||||
|
|
||||||
#### Indexing
|
### Indexing
|
||||||
|
|
||||||
For indexing purposes, it is required that you provide a [substreams](https://thegraph.com/docs/en/substreams/) package that emits a specified set of messages. Most new protocols will already have a [substreams](https://thegraph.com/docs/en/substreams/) package for indexing implemented this will only need to be adjusted to emit the required messages.
|
For indexing purposes, it is required that you provide a [substreams](https://thegraph.com/docs/en/substreams/) package that emits a specified set of messages. If your protocol already has a [substreams](https://thegraph.com/docs/en/substreams/) package for indexing implemented, you can adjust it to emit the required messages.
|
||||||
|
|
||||||
_Specifications coming soon._
|
_Specifications coming soon._
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,62 @@
|
|||||||
|
|
||||||
This page describes the interface required to implement protocol logic component.
|
This page describes the interface required to implement protocol logic component.
|
||||||
|
|
||||||
To create a VM implementation, it is required two provide a manifest file as well as a implementation of the corresponding adapter interface.
|
To create a VM integration, it is required to provide a manifest file as well as an implementation of the corresponding adapter interface.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Following exchanges have been integrated using VM approach:
|
||||||
|
|
||||||
|
- Uniswap V2 (see `/evm/src/uniswap-v2`)
|
||||||
|
- Balancer V2 (see `/evm/src/balancer-v2`)
|
||||||
|
|
||||||
|
## Step by step
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. Install [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup), start by downloading and installing the Foundry installer:
|
||||||
|
```bash
|
||||||
|
curl -L https://foundry.paradigm.xyz | bash
|
||||||
|
```
|
||||||
|
then start a new terminal session and run
|
||||||
|
```bash
|
||||||
|
foundryup
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Start by making a local copy of the Propeller Protocol Lib repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/propeller-heads/propeller-protocol-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install forge dependencies:
|
||||||
|
```bash
|
||||||
|
cd ./propeller-protocol-lib/evm/
|
||||||
|
forge install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Understanding the ISwapAdapter
|
||||||
|
|
||||||
|
Read the the documentation of the [Ethereum Solidity interface](ethereum-solidity.md). It describes the functions that need to be implemented as well as the manifest file.
|
||||||
|
Additionally read through the docstring of the [ISwapAdapter.sol](../../../evm/src/interfaces/ISwapAdapter.sol) interface and the [ISwapAdapterTypes.sol](../../../evm/src/interfaces/ISwapAdapterTypes.sol) interface which defines the data types and errors used by the adapter interface.
|
||||||
|
You can also generate the documentation locally and the look at the generated documentation in the `./docs` folder:
|
||||||
|
```bash
|
||||||
|
cd ./evm/
|
||||||
|
forge doc
|
||||||
|
```
|
||||||
|
### Implementing the ISwapAdapter interface
|
||||||
|
Your integration should be in a separate directory in the `evm/src` folder. Start by cloning the template directory:
|
||||||
|
```bash
|
||||||
|
cp ./evm/src/template ./evm/src/<your-adapter-name>
|
||||||
|
```
|
||||||
|
Implement the `ISwapAdapter` interface in the `./evm/src/<your-adapter-name>.sol` file. There are two reference implementations, one for Uniswap V2 and the other for Balancer V2.
|
||||||
|
|
||||||
|
### Testing your implementation
|
||||||
|
Clone the `evm/test/TemplateSwapAdapter.t.sol` file and rename it to `<your-adapter-name>.t.sol`. Implement the tests for your adapter, make sure all implemented functions are tested and working correctly. Look at the examples of `UniswapV2SwapAdapter.t.sol` and `BalancerV2SwapAdapter.t.sol` for reference. The [Foundry test guide](https://book.getfoundry.sh/forge/tests) is a good reference, especially the chapter for [Fuzz testing](https://book.getfoundry.sh/forge/fuzz-testing), which is used in both the Uniswap and Balancer tests.
|
||||||
|
|
||||||
|
We are using fork testing, i.e. we are running a local Ethereum node and fork the mainnet state. This allows us to test the integration against the real contracts and real data. To run the tests, you need to set the `ETH_RPC_URL` environment variable to the URL of an ethereum RPC. It can be your own node or a public one, like [Alchemy](https://www.alchemy.com/) or [Infura](https://infura.io/).
|
||||||
|
|
||||||
|
Finally, run the tests with:
|
||||||
|
```bash
|
||||||
|
cd ./evm
|
||||||
|
forge test
|
||||||
|
```
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ description: Provide protocol logic using the ethereum virtual machine
|
|||||||
|
|
||||||
# Ethereum: Solidity
|
# Ethereum: Solidity
|
||||||
|
|
||||||
### Swap
|
## Swap/exchange protocol
|
||||||
|
|
||||||
To integrate an EVM exchange protocol the [ISwapAdapter.sol ](https://github.com/propeller-heads/propeller-protocol-lib/blob/main/evm/interfaces/ISwapAdapter.sol)should be implemented. Additionally a manifest file is required that summarises some metadata about the protocol.
|
To integrate an EVM exchange protocol the [ISwapAdapter.sol ](https://github.com/propeller-heads/propeller-protocol-lib/blob/main/evm/interfaces/ISwapAdapter.sol)should be implemented. Additionally a manifest file is required that summarises some metadata about the protocol.
|
||||||
|
|
||||||
{% hint style="info" %}
|
{% hint style="info" %}
|
||||||
Although the interface is specified for Solidity, you are not limited to writing the adapater contract in solidity. We can use any compiled evm bytecode. So if you prefer e.g. Vyper you are welcome to implement the interface using vyper. Unfortunately we do not provide all the tooling for vyper contracts yet, but you can certainly submit compiled vyper byte code.
|
Although the interface is specified for Solidity, you are not limited to writing the adapater contract in Solidity. We can use any compiled evm bytecode. So if you prefer e.g. Vyper, you are welcome to implement the interface using Vyper. Unfortunately we do not provide all the tooling for Vyper contracts yet, but you can certainly submit compiled Vyper byte code.
|
||||||
{% endhint %}
|
{% endhint %}
|
||||||
|
|
||||||
The manifest file contains information about the author, as well as additional static information about the protocol and how to test the current implementation. The file below lists all valid keys.
|
The manifest file contains information about the author, as well as additional static information about the protocol and how to test the current implementation. The file below lists all valid keys.
|
||||||
@@ -22,9 +22,10 @@ author:
|
|||||||
|
|
||||||
# Protocol Constants
|
# Protocol Constants
|
||||||
constants:
|
constants:
|
||||||
# The minimum gas usage of the protocol, excluding any token transfers
|
# The minimum gas usage of a swap using the protocol, excluding any token transfers
|
||||||
protocol_gas: 30000
|
protocol_gas: 30000
|
||||||
# Minimum capabilities we can expect, individual pools may extend these
|
# Minimum capabilities we can expect, individual pools may extend these.
|
||||||
|
# To learn about Capabilities, see ISwapAdapter.sol
|
||||||
capabilities:
|
capabilities:
|
||||||
- SellSide
|
- SellSide
|
||||||
- BuySide
|
- BuySide
|
||||||
@@ -32,25 +33,26 @@ constants:
|
|||||||
|
|
||||||
# The files containing the adapter contract (byte)code
|
# The files containing the adapter contract (byte)code
|
||||||
contract:
|
contract:
|
||||||
# The contract bytecode (required if no source is provided)
|
# The contract runtime (i.e. deployed) bytecode (required if no source is provided)
|
||||||
runtime: UniswapV2SwapAdapter.bin
|
runtime: UniswapV2SwapAdapter.bin
|
||||||
# If you submit the source our CI can generate the bytecode
|
# If you submit the source our CI can generate the bytecode
|
||||||
source: UniswapV2SwapAdapter.sol
|
source: UniswapV2SwapAdapter.sol
|
||||||
|
|
||||||
# Deployment instances used to generate chain specific bytecode.
|
# Deployment instances used to generate chain-specific bytecode.
|
||||||
# Used by the runtime bytecode build script.
|
# Used by the runtime bytecode build script.
|
||||||
instances:
|
instances:
|
||||||
- chain:
|
- chain:
|
||||||
name: mainnet
|
name: mainnet
|
||||||
id: 0
|
id: 0
|
||||||
|
# Arguments passed to the constructor when building the contract
|
||||||
arguments:
|
arguments:
|
||||||
- "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
|
- "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
|
||||||
|
|
||||||
# Specify some automatic test cases in case getPools and
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
# getTokens are not implemented.
|
# getTokens are not implemented.
|
||||||
tests:
|
tests:
|
||||||
instances:
|
instances:
|
||||||
- pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
- pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
||||||
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||||
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
block: 17000000
|
block: 17000000
|
||||||
@@ -61,17 +63,19 @@ tests:
|
|||||||
|
|
||||||
#### Price (optional)
|
#### Price (optional)
|
||||||
|
|
||||||
Calculates pair prices for specified amounts (optional).
|
Calculates pool prices for specified amounts (optional).
|
||||||
|
|
||||||
The returned prices should include all protocol fees, in case the fee is dynamic, the returned price is expected to include the minimum fee. 
|
The returned prices should be in `buyToken/sellToken` units.
|
||||||
|
|
||||||
Ideally this method should be implemented, although it is optional as the price function can be numerically estimated from the swap function. In case it is not available it should be flagged accordingly via capabilities and calling it should revert using the NotImplemented error. 
|
The returned prices should include all protocol fees. In case the fee is dynamic, the returned price is expected to include the minimum fee.
|
||||||
|
|
||||||
|
Ideally this method should be implemented, although it is optional as the price function can be numerically estimated from the swap function. In case it is not available, it should be flagged accordingly via capabilities, and calling it should revert using the NotImplemented error.
|
||||||
|
|
||||||
The method needs to be implemented as view as this is usually more efficient and can be run in parallel.
|
The method needs to be implemented as view as this is usually more efficient and can be run in parallel.
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function price(
|
function price(
|
||||||
bytes32 pairId,
|
bytes32 poolId,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
IERC20 buyToken,
|
IERC20 buyToken,
|
||||||
uint256[] memory sellAmounts
|
uint256[] memory sellAmounts
|
||||||
@@ -80,20 +84,20 @@ function price(
|
|||||||
|
|
||||||
#### Swap
|
#### Swap
|
||||||
|
|
||||||
Simulates swapping tokens on a given pair.
|
Simulates swapping tokens on a given pool.
|
||||||
|
|
||||||
This function should be state modifying meaning it should actually execute the swap and change the state of the vm accordingly.
|
This function should be state modifying, meaning it should actually execute the swap and change the state of the VM accordingly.
|
||||||
|
|
||||||
Please include a gas usage estimate for each amount. This can be achieved e.g. by using the gasleft() function.
|
Please include a gas usage estimate for each amount. This can be achieved e.g. by using the `gasleft()` function.
|
||||||
|
|
||||||
The return type Trade, has a price attribute which should contain the value of price(specifiedAmount). As previously mentioned, the price function support is optional, it is valid to return a zero value for this price in that case it will be estimated numerically. To return zero please use Fraction(0, 1).
|
The return type `Trade` has a price attribute which should contain the value of `price(specifiedAmount)`. As previously mentioned, the price function support is optional, it is valid to return a zero value for this price (in that case it will be estimated numerically). To return zero please use `Fraction(0, 1)`.
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function swap(
|
function swap(
|
||||||
bytes32 pairId,
|
bytes32 poolId,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
IERC20 buyToken,
|
IERC20 buyToken,
|
||||||
SwapSide side,
|
OrderSide side,
|
||||||
uint256 specifiedAmount
|
uint256 specifiedAmount
|
||||||
) external returns (Trade memory trade);
|
) external returns (Trade memory trade);
|
||||||
```
|
```
|
||||||
@@ -102,32 +106,32 @@ function swap(
|
|||||||
|
|
||||||
Retrieves the limits for each token.
|
Retrieves the limits for each token.
|
||||||
|
|
||||||
This method returns the maximum limits of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt over estimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit.
|
This method returns the maximum amount of a token that can be traded. The limit is reached when the change in the received amounts is zero or close to zero. If in doubt, overestimate the limit. The swap function should not error with LimitExceeded if called with any amounts below the limit.
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function getLimits(bytes32 pairId, SwapSide side)
|
function getLimits(bytes32 poolId, OrderSide side)
|
||||||
external
|
external
|
||||||
returns (uint256[] memory);
|
returns (uint256[] memory);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### getCapabilities
|
#### getCapabilities
|
||||||
|
|
||||||
Retrieves the capabilities of the selected pair.
|
Retrieves the capabilities of the selected pool.
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken)
|
function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
external
|
external
|
||||||
returns (Capabilities[] memory);
|
returns (Capability[] memory);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### getTokens (optional)
|
#### getTokens (optional)
|
||||||
|
|
||||||
Retrieves the tokens for the given pair.
|
Retrieves the tokens for the given pool.
|
||||||
|
|
||||||
_Mainly used for testing as this is redundant with the required substreams implementation._
|
_Mainly used for testing as this is redundant with the required substreams implementation._
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function getTokens(bytes32 pairId)
|
function getTokens(bytes32 poolId)
|
||||||
external
|
external
|
||||||
returns (IERC20[] memory tokens);
|
returns (IERC20[] memory tokens);
|
||||||
```
|
```
|
||||||
@@ -136,7 +140,7 @@ function getTokens(bytes32 pairId)
|
|||||||
|
|
||||||
Retrieves a range of pool IDs.
|
Retrieves a range of pool IDs.
|
||||||
|
|
||||||
_Mainly used for testing it is alright to not return all available pools here. Nevertheless this is useful to test against the substreams implementation. If implemented it safes time writing custom tests._
|
_Mainly used for testing. It is alright to not return all available pools here. Nevertheless, this is useful to test against the substreams implementation. If implemented, it saves time writing custom tests._
|
||||||
|
|
||||||
```solidity
|
```solidity
|
||||||
function getPoolIds(uint256 offset, uint256 limit)
|
function getPoolIds(uint256 offset, uint256 limit)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ mainnet = "${ETH_RPC_URL}"
|
|||||||
|
|
||||||
[fmt]
|
[fmt]
|
||||||
line_length = 80
|
line_length = 80
|
||||||
|
wrap_comments = true
|
||||||
|
|
||||||
[etherscan]
|
[etherscan]
|
||||||
mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" }
|
mainnet = { key = "${ETHERSCAN_MAINNET_KEY}" }
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
pragma solidity ^0.8.13;
|
|
||||||
|
|
||||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
|
||||||
import "interfaces/ISwapAdapterTypes.sol";
|
|
||||||
|
|
||||||
/// @title ISwapAdapterTypes
|
|
||||||
/// @dev Implement this interface to support propeller routing through your pairs.
|
|
||||||
/// @dev Before implementing the interface we need to introduce three function for a
|
|
||||||
/// @dev given pair: The swap(x), gas(x) and price(x) functions:
|
|
||||||
/// @dev The swap function accepts some specified token amount: x and returns the
|
|
||||||
/// @dev amount y a user can get by swapping x through the venue.
|
|
||||||
/// @dev The gas function simply returns the estimated gas cost given a specified
|
|
||||||
/// @dev amount x.
|
|
||||||
/// @dev Last but not least, the price function is the derivative of the swap
|
|
||||||
/// @dev function. It represents the best possible price a user can get from a
|
|
||||||
/// @dev pair after swapping x of the specified token.
|
|
||||||
/// @dev During calls to swap and getLimits, the caller can be assumed to
|
|
||||||
/// @dev have the required sell or buy token balance as well as unlimited approvals
|
|
||||||
/// @dev to this contract.
|
|
||||||
interface ISwapAdapter is ISwapAdapterTypes {
|
|
||||||
/// @notice Calculates pair prices for specified amounts (optional).
|
|
||||||
/// @dev The returned prices should include all dex fees, in case the fee
|
|
||||||
/// @dev is dynamic, the returned price is expected to include the minimum fee.
|
|
||||||
/// @dev Ideally this method should be implemented, although it is optional as
|
|
||||||
/// @dev the price function can be numerically estimated from the swap function.
|
|
||||||
/// @dev In case it is not available it should be flagged via capabilities and
|
|
||||||
/// @dev calling it should revert using the `NotImplemented` error.
|
|
||||||
/// @dev The method needs to be implemented as view as this is usually more efficient
|
|
||||||
/// @dev and can be run in parallel.
|
|
||||||
/// @dev all.
|
|
||||||
/// @param pairId The ID of the trading pair.
|
|
||||||
/// @param sellToken The token being sold.
|
|
||||||
/// @param buyToken The token being bought.
|
|
||||||
/// @param sellAmounts The specified amounts used for price calculation.
|
|
||||||
/// @return prices array of prices as fractions corresponding to the provided amounts.
|
|
||||||
function price(
|
|
||||||
bytes32 pairId,
|
|
||||||
IERC20 sellToken,
|
|
||||||
IERC20 buyToken,
|
|
||||||
uint256[] memory sellAmounts
|
|
||||||
) external view returns (Fraction[] memory prices);
|
|
||||||
|
|
||||||
/// @notice Simulates swapping tokens on a given pair.
|
|
||||||
/// @dev This function should be state modifying meaning it should actually execute
|
|
||||||
/// @dev the swap and change the state of the evm accordingly.
|
|
||||||
/// @dev Please include a gas usage estimate for each amount. This can be achieved
|
|
||||||
/// @dev e.g. by using the `gasleft()` function.
|
|
||||||
/// @dev The return type trade, has a price attribute which should contain the
|
|
||||||
/// value of `price(specifiedAmount)`. As this is optional, defined via
|
|
||||||
/// `Capability.PriceFunction`, it is valid to return a zero value for this
|
|
||||||
/// price in that case it will be estimated numerically. To return zero use
|
|
||||||
/// Fraction(0, 1).
|
|
||||||
/// @param pairId The ID of the trading pair.
|
|
||||||
/// @param sellToken The token being sold.
|
|
||||||
/// @param buyToken The token being bought.
|
|
||||||
/// @param side The side of the trade (Sell or Buy).
|
|
||||||
/// @param specifiedAmount The amount to be traded.
|
|
||||||
/// @return trade Trade struct representing the executed trade.
|
|
||||||
function swap(
|
|
||||||
bytes32 pairId,
|
|
||||||
IERC20 sellToken,
|
|
||||||
IERC20 buyToken,
|
|
||||||
SwapSide side,
|
|
||||||
uint256 specifiedAmount
|
|
||||||
) external returns (Trade memory trade);
|
|
||||||
|
|
||||||
/// @notice Retrieves the limits for each token.
|
|
||||||
/// @dev Retrieve the maximum limits of a token that can be traded. The limit is reached
|
|
||||||
/// @dev when the change in the received amounts is zero or close to zero. If in doubt
|
|
||||||
/// @dev over estimate. The swap function should not error with `LimitExceeded` if
|
|
||||||
/// @dev called with amounts below the limit.
|
|
||||||
/// @param pairId The ID of the trading pair.
|
|
||||||
/// @return An array of limits.
|
|
||||||
function getLimits(bytes32 pairId, SwapSide side)
|
|
||||||
external
|
|
||||||
returns (uint256[] memory);
|
|
||||||
|
|
||||||
/// @notice Retrieves the capabilities of the selected pair.
|
|
||||||
/// @param pairId The ID of the trading pair.
|
|
||||||
/// @return An array of Capabilities.
|
|
||||||
function getCapabilities(bytes32 pairId, IERC20 sellToken, IERC20 buyToken)
|
|
||||||
external
|
|
||||||
returns (Capabilities[] memory);
|
|
||||||
|
|
||||||
/// @notice Retrieves the tokens in the selected pair.
|
|
||||||
/// @dev Mainly used for testing as this is redundant with the required substreams
|
|
||||||
/// @dev implementation.
|
|
||||||
/// @param pairId The ID of the trading pair.
|
|
||||||
/// @return tokens array of IERC20 contracts.
|
|
||||||
function getTokens(bytes32 pairId)
|
|
||||||
external
|
|
||||||
returns (IERC20[] memory tokens);
|
|
||||||
|
|
||||||
/// @notice Retrieves a range of pool IDs.
|
|
||||||
/// @dev Mainly used for testing it is alright to not return all available pools here.
|
|
||||||
/// @dev Nevertheless this is useful to test against the substreams implementation. If
|
|
||||||
/// @dev implemented it safes time writing custom tests.
|
|
||||||
/// @param offset The starting index from which to retrieve pool IDs.
|
|
||||||
/// @param limit The maximum number of pool IDs to retrieve.
|
|
||||||
/// @return ids array of pool IDs.
|
|
||||||
function getPoolIds(uint256 offset, uint256 limit)
|
|
||||||
external
|
|
||||||
returns (bytes32[] memory ids);
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
pragma solidity ^0.8.13;
|
|
||||||
|
|
||||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
|
||||||
|
|
||||||
interface ISwapAdapterTypes {
|
|
||||||
/// @dev The SwapSide enum represents possible sides of a trade: Sell or Buy.
|
|
||||||
/// @dev E.g. if SwapSide is Sell, the sell amount is interpreted to be fixed.
|
|
||||||
enum SwapSide {
|
|
||||||
Sell,
|
|
||||||
Buy
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @dev The Capabilities enum represents possible features of a trading pair.
|
|
||||||
enum Capabilities {
|
|
||||||
Unset,
|
|
||||||
// Support SwapSide.Sell values (required)
|
|
||||||
SellSide,
|
|
||||||
// Support SwapSide.Buy values (optional)
|
|
||||||
BuySide,
|
|
||||||
// Support evaluating the price function (optional)
|
|
||||||
PriceFunction,
|
|
||||||
// Support tokens that charge a fee on transfer (optional)
|
|
||||||
FeeOnTransfer,
|
|
||||||
// The pair does not suffer from price impact and mantains
|
|
||||||
// a constant price for increasingly larger speficied amounts.
|
|
||||||
// (optional)
|
|
||||||
ConstantPrice,
|
|
||||||
// Indicates that the pair does not read it's own token balances
|
|
||||||
// while swapping. (optional)
|
|
||||||
TokenBalanceIndependent,
|
|
||||||
// Indicates that prices are returned scaled, else it is assumed
|
|
||||||
// prices still require scaling by token decimals.
|
|
||||||
ScaledPrices
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @dev Representation used for rational numbers such as prices.
|
|
||||||
struct Fraction {
|
|
||||||
// TODO: rename numerator
|
|
||||||
uint256 nominator;
|
|
||||||
uint256 denominator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @dev The Trade struct holds data about an executed trade.
|
|
||||||
struct Trade {
|
|
||||||
uint256 receivedAmount; // The amount received from the trade.
|
|
||||||
uint256 gasUsed; // The amount of gas used in the trade.
|
|
||||||
Fraction price; // The price of the pair after the trade.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @dev The Unavailable error is thrown when a pool or swap is not
|
|
||||||
/// @dev available for unexpected reason, e.g. because it was paused
|
|
||||||
/// @dev due to a bug.
|
|
||||||
error Unavailable(string reason);
|
|
||||||
|
|
||||||
/// @dev The LimitExceeded error is thrown when a limit has been
|
|
||||||
/// @dev exceeded. E.g. the specified amount can't be traded safely.
|
|
||||||
error LimitExceeded(uint256 limit);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
interfaces/=interfaces/
|
|
||||||
forge-std/=lib/forge-std/src/
|
forge-std/=lib/forge-std/src/
|
||||||
openzeppelin-contracts/=lib/openzeppelin-contracts/
|
openzeppelin-contracts/=lib/openzeppelin-contracts/
|
||||||
src/=src/
|
src/=src/
|
||||||
|
balancer-v2/interfaces/=lib/balancer-v2-monorepo/pkg/interfaces/contracts
|
||||||
|
|||||||
488
evm/src/balancer-v2/BalancerV2SwapAdapter.sol
Normal file
488
evm/src/balancer-v2/BalancerV2SwapAdapter.sol
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
|
|
||||||
|
// Maximum Swap In/Out Ratio - 0.3
|
||||||
|
// https://balancer.gitbook.io/balancer/core-concepts/protocol/limitations#v2-limits
|
||||||
|
uint256 constant RESERVE_LIMIT_FACTOR = 4;
|
||||||
|
uint256 constant SWAP_DEADLINE_SEC = 1000;
|
||||||
|
|
||||||
|
contract BalancerV2SwapAdapter is ISwapAdapter {
|
||||||
|
IVault immutable vault;
|
||||||
|
|
||||||
|
constructor(address payable vault_) {
|
||||||
|
vault = IVault(vault_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @notice Calculate the price of the buy token in terms of the sell token.
|
||||||
|
/// @dev The resulting price is not scaled by the token decimals.
|
||||||
|
/// Also this function is not 'view' because Balancer V2 simulates the swap
|
||||||
|
/// and then returns the amount diff in revert data.
|
||||||
|
/// @param poolId The ID of the trading pool.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param buyToken The token being bought.
|
||||||
|
/// @param sellAmount The amount of tokens being sold.
|
||||||
|
/// @return calculatedPrice The price of the buy token in terms of the sell
|
||||||
|
/// as a Fraction struct.
|
||||||
|
function priceSingle(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
uint256 sellAmount
|
||||||
|
) public returns (Fraction memory calculatedPrice) {
|
||||||
|
IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1);
|
||||||
|
swapSteps[0] = IVault.BatchSwapStep({
|
||||||
|
poolId: poolId,
|
||||||
|
assetInIndex: 0,
|
||||||
|
assetOutIndex: 1,
|
||||||
|
amount: sellAmount,
|
||||||
|
userData: ""
|
||||||
|
});
|
||||||
|
address[] memory assets = new address[](2);
|
||||||
|
assets[0] = address(sellToken);
|
||||||
|
assets[1] = address(buyToken);
|
||||||
|
IVault.FundManagement memory funds = IVault.FundManagement({
|
||||||
|
sender: msg.sender,
|
||||||
|
fromInternalBalance: false,
|
||||||
|
recipient: payable(msg.sender),
|
||||||
|
toInternalBalance: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// assetDeltas correspond to the assets array
|
||||||
|
int256[] memory assetDeltas = new int256[](2);
|
||||||
|
assetDeltas = vault.queryBatchSwap(
|
||||||
|
IVault.SwapKind.GIVEN_IN, swapSteps, assets, funds
|
||||||
|
);
|
||||||
|
// assetDeltas[1] is the amount of tokens sent from the vault (i.e.
|
||||||
|
// bought), so the sign is negative, which means the sign should be
|
||||||
|
// flipped to get the price.
|
||||||
|
calculatedPrice =
|
||||||
|
Fraction(uint256(-assetDeltas[1]), uint256(assetDeltas[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSellAmount(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
uint256 buyAmount
|
||||||
|
) public returns (uint256 sellAmount) {
|
||||||
|
IVault.BatchSwapStep[] memory swapSteps = new IVault.BatchSwapStep[](1);
|
||||||
|
swapSteps[0] = IVault.BatchSwapStep({
|
||||||
|
poolId: poolId,
|
||||||
|
assetInIndex: 0,
|
||||||
|
assetOutIndex: 1,
|
||||||
|
amount: buyAmount,
|
||||||
|
userData: ""
|
||||||
|
});
|
||||||
|
address[] memory assets = new address[](2);
|
||||||
|
assets[0] = address(sellToken);
|
||||||
|
assets[1] = address(buyToken);
|
||||||
|
IVault.FundManagement memory funds = IVault.FundManagement({
|
||||||
|
sender: msg.sender,
|
||||||
|
fromInternalBalance: false,
|
||||||
|
recipient: payable(msg.sender),
|
||||||
|
toInternalBalance: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// assetDeltas correspond to the assets array
|
||||||
|
int256[] memory assetDeltas = new int256[](2);
|
||||||
|
assetDeltas = vault.queryBatchSwap(
|
||||||
|
IVault.SwapKind.GIVEN_OUT, swapSteps, assets, funds
|
||||||
|
);
|
||||||
|
|
||||||
|
sellAmount = uint256(assetDeltas[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function priceBatch(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
uint256[] memory specifiedAmounts
|
||||||
|
) external returns (Fraction[] memory calculatedPrices) {
|
||||||
|
for (uint256 i = 0; i < specifiedAmounts.length; i++) {
|
||||||
|
calculatedPrices[i] =
|
||||||
|
priceSingle(poolId, sellToken, buyToken, specifiedAmounts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function price(bytes32, IERC20, IERC20, uint256[] memory)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (Fraction[] memory)
|
||||||
|
{
|
||||||
|
revert NotImplemented("BalancerV2SwapAdapter.price");
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
OrderSide side,
|
||||||
|
uint256 specifiedAmount
|
||||||
|
) external override returns (Trade memory trade) {
|
||||||
|
uint256 sellAmount;
|
||||||
|
IVault.SwapKind kind;
|
||||||
|
uint256 limit; // TODO set this slippage limit properly
|
||||||
|
if (side == OrderSide.Sell) {
|
||||||
|
kind = IVault.SwapKind.GIVEN_IN;
|
||||||
|
sellAmount = specifiedAmount;
|
||||||
|
limit = 0;
|
||||||
|
} else {
|
||||||
|
kind = IVault.SwapKind.GIVEN_OUT;
|
||||||
|
sellAmount =
|
||||||
|
getSellAmount(poolId, sellToken, buyToken, specifiedAmount);
|
||||||
|
limit = type(uint256).max;
|
||||||
|
}
|
||||||
|
|
||||||
|
sellToken.transferFrom(msg.sender, address(this), sellAmount);
|
||||||
|
sellToken.approve(address(vault), sellAmount);
|
||||||
|
|
||||||
|
uint256 gasBefore = gasleft();
|
||||||
|
trade.calculatedAmount = vault.swap(
|
||||||
|
IVault.SingleSwap({
|
||||||
|
poolId: poolId,
|
||||||
|
kind: kind,
|
||||||
|
assetIn: address(sellToken),
|
||||||
|
assetOut: address(buyToken),
|
||||||
|
amount: specifiedAmount,
|
||||||
|
userData: ""
|
||||||
|
}),
|
||||||
|
IVault.FundManagement({
|
||||||
|
sender: address(this),
|
||||||
|
fromInternalBalance: false,
|
||||||
|
recipient: msg.sender,
|
||||||
|
toInternalBalance: false
|
||||||
|
}),
|
||||||
|
limit,
|
||||||
|
block.timestamp + SWAP_DEADLINE_SEC
|
||||||
|
);
|
||||||
|
trade.gasUsed = gasBefore - gasleft();
|
||||||
|
trade.price = priceSingle(poolId, sellToken, buyToken, specifiedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (uint256[] memory limits)
|
||||||
|
{
|
||||||
|
limits = new uint256[](2);
|
||||||
|
(IERC20[] memory tokens, uint256[] memory balances,) =
|
||||||
|
vault.getPoolTokens(poolId);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < tokens.length; i++) {
|
||||||
|
if (tokens[i] == sellToken) {
|
||||||
|
limits[0] = balances[i] / RESERVE_LIMIT_FACTOR;
|
||||||
|
}
|
||||||
|
if (tokens[i] == buyToken) {
|
||||||
|
limits[1] = balances[i] / RESERVE_LIMIT_FACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCapabilities(bytes32, IERC20, IERC20)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (Capability[] memory capabilities)
|
||||||
|
{
|
||||||
|
capabilities = new Capability[](2);
|
||||||
|
capabilities[0] = Capability.SellOrder;
|
||||||
|
capabilities[1] = Capability.BuyOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokens(bytes32 poolId)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
override
|
||||||
|
returns (IERC20[] memory tokens)
|
||||||
|
{
|
||||||
|
(tokens,,) = vault.getPoolTokens(poolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Balancer V2 does not support enumerating pools, they have to be
|
||||||
|
/// indexed off-chain.
|
||||||
|
function getPoolIds(uint256, uint256)
|
||||||
|
external
|
||||||
|
pure
|
||||||
|
override
|
||||||
|
returns (bytes32[] memory)
|
||||||
|
{
|
||||||
|
revert NotImplemented("BalancerV2SwapAdapter.getPoolIds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IVault {
|
||||||
|
struct BatchSwapStep {
|
||||||
|
bytes32 poolId;
|
||||||
|
uint256 assetInIndex;
|
||||||
|
uint256 assetOutIndex;
|
||||||
|
uint256 amount;
|
||||||
|
bytes userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FundManagement {
|
||||||
|
address sender;
|
||||||
|
bool fromInternalBalance;
|
||||||
|
address recipient;
|
||||||
|
bool toInternalBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExitPoolRequest {
|
||||||
|
address[] assets;
|
||||||
|
uint256[] minAmountsOut;
|
||||||
|
bytes userData;
|
||||||
|
bool toInternalBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JoinPoolRequest {
|
||||||
|
address[] assets;
|
||||||
|
uint256[] maxAmountsIn;
|
||||||
|
bytes userData;
|
||||||
|
bool fromInternalBalance;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PoolBalanceOp {
|
||||||
|
SwapKind kind;
|
||||||
|
bytes32 poolId;
|
||||||
|
address token;
|
||||||
|
uint256 amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserBalanceOp {
|
||||||
|
SwapKind kind;
|
||||||
|
address asset;
|
||||||
|
uint256 amount;
|
||||||
|
address sender;
|
||||||
|
address recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SingleSwap {
|
||||||
|
bytes32 poolId;
|
||||||
|
SwapKind kind;
|
||||||
|
address assetIn;
|
||||||
|
address assetOut;
|
||||||
|
uint256 amount;
|
||||||
|
bytes userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
event AuthorizerChanged(address indexed newAuthorizer);
|
||||||
|
event ExternalBalanceTransfer(
|
||||||
|
address indexed token,
|
||||||
|
address indexed sender,
|
||||||
|
address recipient,
|
||||||
|
uint256 amount
|
||||||
|
);
|
||||||
|
event FlashLoan(
|
||||||
|
address indexed recipient,
|
||||||
|
address indexed token,
|
||||||
|
uint256 amount,
|
||||||
|
uint256 feeAmount
|
||||||
|
);
|
||||||
|
event InternalBalanceChanged(
|
||||||
|
address indexed user, address indexed token, int256 delta
|
||||||
|
);
|
||||||
|
event PausedStateChanged(bool paused);
|
||||||
|
event PoolBalanceChanged(
|
||||||
|
bytes32 indexed poolId,
|
||||||
|
address indexed liquidityProvider,
|
||||||
|
address[] tokens,
|
||||||
|
int256[] deltas,
|
||||||
|
uint256[] protocolFeeAmounts
|
||||||
|
);
|
||||||
|
event PoolBalanceManaged(
|
||||||
|
bytes32 indexed poolId,
|
||||||
|
address indexed assetManager,
|
||||||
|
address indexed token,
|
||||||
|
int256 cashDelta,
|
||||||
|
int256 managedDelta
|
||||||
|
);
|
||||||
|
event PoolRegistered(
|
||||||
|
bytes32 indexed poolId,
|
||||||
|
address indexed poolAddress,
|
||||||
|
uint8 specialization
|
||||||
|
);
|
||||||
|
event RelayerApprovalChanged(
|
||||||
|
address indexed relayer, address indexed sender, bool approved
|
||||||
|
);
|
||||||
|
event Swap(
|
||||||
|
bytes32 indexed poolId,
|
||||||
|
address indexed tokenIn,
|
||||||
|
address indexed tokenOut,
|
||||||
|
uint256 amountIn,
|
||||||
|
uint256 amountOut
|
||||||
|
);
|
||||||
|
event TokensDeregistered(bytes32 indexed poolId, address[] tokens);
|
||||||
|
event TokensRegistered(
|
||||||
|
bytes32 indexed poolId, address[] tokens, address[] assetManagers
|
||||||
|
);
|
||||||
|
|
||||||
|
function WETH() external view returns (address);
|
||||||
|
|
||||||
|
function batchSwap(
|
||||||
|
SwapKind kind,
|
||||||
|
BatchSwapStep[] memory swaps,
|
||||||
|
address[] memory assets,
|
||||||
|
FundManagement memory funds,
|
||||||
|
int256[] memory limits,
|
||||||
|
uint256 deadline
|
||||||
|
) external payable returns (int256[] memory assetDeltas);
|
||||||
|
|
||||||
|
function deregisterTokens(bytes32 poolId, address[] memory tokens)
|
||||||
|
external;
|
||||||
|
|
||||||
|
function exitPool(
|
||||||
|
bytes32 poolId,
|
||||||
|
address sender,
|
||||||
|
address recipient,
|
||||||
|
ExitPoolRequest memory request
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function flashLoan(
|
||||||
|
address recipient,
|
||||||
|
address[] memory tokens,
|
||||||
|
uint256[] memory amounts,
|
||||||
|
bytes memory userData
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function getActionId(bytes4 selector) external view returns (bytes32);
|
||||||
|
|
||||||
|
function getAuthorizer() external view returns (address);
|
||||||
|
|
||||||
|
function getDomainSeparator() external view returns (bytes32);
|
||||||
|
|
||||||
|
function getInternalBalance(address user, address[] memory tokens)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (uint256[] memory balances);
|
||||||
|
|
||||||
|
function getNextNonce(address user) external view returns (uint256);
|
||||||
|
|
||||||
|
function getPausedState()
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
bool paused,
|
||||||
|
uint256 pauseWindowEndTime,
|
||||||
|
uint256 bufferPeriodEndTime
|
||||||
|
);
|
||||||
|
|
||||||
|
function getPool(bytes32 poolId) external view returns (address, uint8);
|
||||||
|
|
||||||
|
function getPoolTokenInfo(bytes32 poolId, address token)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
uint256 cash,
|
||||||
|
uint256 managed,
|
||||||
|
uint256 lastChangeBlock,
|
||||||
|
address assetManager
|
||||||
|
);
|
||||||
|
|
||||||
|
function getProtocolFeesCollector() external view returns (address);
|
||||||
|
|
||||||
|
function hasApprovedRelayer(address user, address relayer)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (bool);
|
||||||
|
|
||||||
|
function joinPool(
|
||||||
|
bytes32 poolId,
|
||||||
|
address sender,
|
||||||
|
address recipient,
|
||||||
|
JoinPoolRequest memory request
|
||||||
|
) external payable;
|
||||||
|
|
||||||
|
function managePoolBalance(PoolBalanceOp[] memory ops) external;
|
||||||
|
|
||||||
|
function manageUserBalance(UserBalanceOp[] memory ops) external payable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Simulates a call to `batchSwap`, returning an array of Vault asset
|
||||||
|
* deltas. Calls to `swap` cannot be
|
||||||
|
* simulated directly, but an equivalent `batchSwap` call can and will yield
|
||||||
|
* the exact same result.
|
||||||
|
*
|
||||||
|
* Each element in the array corresponds to the asset at the same index, and
|
||||||
|
* indicates the number of tokens (or ETH)
|
||||||
|
* the Vault would take from the sender (if positive) or send to the
|
||||||
|
* recipient (if negative). The arguments it
|
||||||
|
* receives are the same that an equivalent `batchSwap` call would receive.
|
||||||
|
*
|
||||||
|
* Unlike `batchSwap`, this function performs no checks on the sender or
|
||||||
|
* recipient field in the `funds` struct.
|
||||||
|
* This makes it suitable to be called by off-chain applications via
|
||||||
|
* eth_call without needing to hold tokens,
|
||||||
|
* approve them for the Vault, or even know a user's address.
|
||||||
|
*
|
||||||
|
* Note that this function is not 'view' (due to implementation details):
|
||||||
|
* the client code must explicitly execute
|
||||||
|
* eth_call instead of eth_sendTransaction.
|
||||||
|
*/
|
||||||
|
function queryBatchSwap(
|
||||||
|
SwapKind kind,
|
||||||
|
BatchSwapStep[] memory swaps,
|
||||||
|
address[] memory assets,
|
||||||
|
FundManagement memory funds
|
||||||
|
) external returns (int256[] memory);
|
||||||
|
|
||||||
|
function registerPool(uint8 specialization) external returns (bytes32);
|
||||||
|
|
||||||
|
function registerTokens(
|
||||||
|
bytes32 poolId,
|
||||||
|
address[] memory tokens,
|
||||||
|
address[] memory assetManagers
|
||||||
|
) external;
|
||||||
|
|
||||||
|
function setAuthorizer(address newAuthorizer) external;
|
||||||
|
|
||||||
|
function setPaused(bool paused) external;
|
||||||
|
|
||||||
|
function setRelayerApproval(address sender, address relayer, bool approved)
|
||||||
|
external;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dev Performs a swap with a single Pool.
|
||||||
|
*
|
||||||
|
* If the swap is 'given in' (the number of tokens to send to the Pool is
|
||||||
|
* known), it returns the amount of tokens
|
||||||
|
* taken from the Pool, which must be greater than or equal to `limit`.
|
||||||
|
*
|
||||||
|
* If the swap is 'given out' (the number of tokens to take from the Pool is
|
||||||
|
* known), it returns the amount of tokens
|
||||||
|
* sent to the Pool, which must be less than or equal to `limit`.
|
||||||
|
*
|
||||||
|
* Internal Balance usage and the recipient are determined by the `funds`
|
||||||
|
* struct.
|
||||||
|
*
|
||||||
|
* Emits a `Swap` event.
|
||||||
|
*/
|
||||||
|
function swap(
|
||||||
|
SingleSwap memory singleSwap,
|
||||||
|
FundManagement memory funds,
|
||||||
|
uint256 limit,
|
||||||
|
uint256 deadline
|
||||||
|
) external payable returns (uint256);
|
||||||
|
|
||||||
|
receive() external payable;
|
||||||
|
|
||||||
|
function getPoolTokens(bytes32 poolId)
|
||||||
|
external
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
IERC20[] memory tokens,
|
||||||
|
uint256[] memory balances,
|
||||||
|
uint256 lastChangeBlock
|
||||||
|
);
|
||||||
|
|
||||||
|
enum SwapKind
|
||||||
|
/// The number of tokens to send to the Pool is known
|
||||||
|
{
|
||||||
|
GIVEN_IN,
|
||||||
|
/// The number of tokens to take from the Pool is known
|
||||||
|
GIVEN_OUT
|
||||||
|
}
|
||||||
|
}
|
||||||
36
evm/src/balancer-v2/manifest.yaml
Normal file
36
evm/src/balancer-v2/manifest.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# information about the author helps us reach out in case of issues.
|
||||||
|
author:
|
||||||
|
name: Propellerheads.xyz
|
||||||
|
email: pistomat@propellerheads.xyz
|
||||||
|
|
||||||
|
# Protocol Constants
|
||||||
|
constants:
|
||||||
|
protocol_gas: 30000
|
||||||
|
# minimum capabilities we can expect, individual pools may extend these
|
||||||
|
capabilities:
|
||||||
|
- SellSide
|
||||||
|
- BuySide
|
||||||
|
- PriceFunction
|
||||||
|
|
||||||
|
# The file containing the adapter contract
|
||||||
|
contract: BalancerV2SwapAdapter.sol
|
||||||
|
|
||||||
|
# Deployment instances used to generate chain specific bytecode.
|
||||||
|
instances:
|
||||||
|
- chain:
|
||||||
|
name: mainnet
|
||||||
|
id: 0
|
||||||
|
arguments:
|
||||||
|
- "0xBA12222222228d8Ba445958a75a0704d566BF2C8"
|
||||||
|
|
||||||
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
|
# getTokens are not implemented.
|
||||||
|
tests:
|
||||||
|
instances:
|
||||||
|
- pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
||||||
|
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||||
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
|
block: 17000000
|
||||||
|
chain:
|
||||||
|
id: 0
|
||||||
|
name: mainnet
|
||||||
109
evm/src/interfaces/ISwapAdapter.sol
Normal file
109
evm/src/interfaces/ISwapAdapter.sol
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||||
|
import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
|
||||||
|
/// @title ISwapAdapter
|
||||||
|
/// @dev Implement this interface to support Propeller routing through your
|
||||||
|
/// pools. Before implementing the interface we need to introduce some function
|
||||||
|
/// for a given pool. The main one, the swap(x) function, implements a sell
|
||||||
|
/// order of a specified token.
|
||||||
|
/// The gas function simply
|
||||||
|
/// returns the estimated gas cost given a specified amount x. Last but not
|
||||||
|
/// least, the price function is the derivative of the swap function. It
|
||||||
|
/// represents the best possible price a user can get from a pool after swapping
|
||||||
|
/// x of the specified token. During calls to swap and getLimits, the caller can
|
||||||
|
/// be assumed to have the required sell or buy token balance as well as
|
||||||
|
/// unlimited approvals to this contract.
|
||||||
|
interface ISwapAdapter is ISwapAdapterTypes {
|
||||||
|
/// @notice Calculates pool prices for specified amounts (optional).
|
||||||
|
/// @dev The returned prices should include all dex fees. In case the fee is
|
||||||
|
/// dynamic, the returned price is expected to include the minimum fee.
|
||||||
|
/// Ideally this method should be implemented, although it is optional as
|
||||||
|
/// the price function can be numerically estimated from the swap function.
|
||||||
|
/// In case it is not available, it should be flagged via capabilities and
|
||||||
|
/// calling it should revert using the `NotImplemented` error. The method
|
||||||
|
/// needs to be implemented as view as this is usually more efficient and
|
||||||
|
/// can be run in parallel.
|
||||||
|
/// @param poolId The ID of the trading pool.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param buyToken The token being bought.
|
||||||
|
/// @param specifiedAmounts The specified amounts used for price
|
||||||
|
/// calculation.
|
||||||
|
/// @return prices array of prices as fractions corresponding to the
|
||||||
|
/// provided amounts.
|
||||||
|
function price(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
uint256[] memory specifiedAmounts
|
||||||
|
) external view returns (Fraction[] memory prices);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @notice Simulates swapping tokens on a given pool.
|
||||||
|
* @dev This function should be state modifying, meaning it should actually
|
||||||
|
* execute the swap and change the state of the EVM accordingly. Please
|
||||||
|
* include a gas usage estimate for each amount. This can be achieved e.g. by
|
||||||
|
* using the `gasleft()` function. The return type `Trade` has an attribute
|
||||||
|
* called price which should contain the value of `price(specifiedAmount)`.
|
||||||
|
* As this is optional, defined via `Capability.PriceFunction`, it is valid
|
||||||
|
* to return a Fraction(0, 0) value for this price. In that case the price
|
||||||
|
* will be estimated numerically.
|
||||||
|
* @param poolId The ID of the trading pool.
|
||||||
|
* @param sellToken The token being sold.
|
||||||
|
* @param buyToken The token being bought.
|
||||||
|
* @param side The side of the trade (Sell or Buy).
|
||||||
|
* @param specifiedAmount The amount to be traded.
|
||||||
|
* @return trade Trade struct representing the executed trade.
|
||||||
|
*/
|
||||||
|
function swap(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
OrderSide side,
|
||||||
|
uint256 specifiedAmount
|
||||||
|
) external returns (Trade memory trade);
|
||||||
|
|
||||||
|
/// @notice Retrieves the limits for each token.
|
||||||
|
/// @dev Retrieve the maximum limits of a token that can be traded. The
|
||||||
|
/// limit is reached when the change in the received amounts is zero or
|
||||||
|
/// close to zero or when the swap fails because of the pools restrictions.
|
||||||
|
/// Overestimate if in doubt rather than underestimate. The
|
||||||
|
/// swap function should not error with `LimitExceeded` if called with
|
||||||
|
/// amounts below the limit.
|
||||||
|
/// @param poolId The ID of the trading pool.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param buyToken The token being bought.
|
||||||
|
/// @return limits An array of limits.
|
||||||
|
function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
returns (uint256[] memory limits);
|
||||||
|
|
||||||
|
/// @notice Retrieves the capabilities of the selected pool.
|
||||||
|
/// @param poolId The ID of the trading pool.
|
||||||
|
/// @return capabilities An array of Capability.
|
||||||
|
function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
returns (Capability[] memory capabilities);
|
||||||
|
|
||||||
|
/// @notice Retrieves the tokens in the selected pool.
|
||||||
|
/// @dev Mainly used for testing as this is redundant with the required
|
||||||
|
/// substreams implementation.
|
||||||
|
/// @param poolId The ID of the trading pool.
|
||||||
|
/// @return tokens An array of IERC20 contracts.
|
||||||
|
function getTokens(bytes32 poolId)
|
||||||
|
external
|
||||||
|
returns (IERC20[] memory tokens);
|
||||||
|
|
||||||
|
/// @notice Retrieves a range of pool IDs.
|
||||||
|
/// @dev Mainly used for testing. It is alright to not return all available
|
||||||
|
/// pools here. Nevertheless, this is useful to test against the substreams
|
||||||
|
/// implementation. If implemented, it saves time writing custom tests.
|
||||||
|
/// @param offset The starting index from which to retrieve pool IDs.
|
||||||
|
/// @param limit The maximum number of pool IDs to retrieve.
|
||||||
|
/// @return ids An array of pool IDs.
|
||||||
|
function getPoolIds(uint256 offset, uint256 limit)
|
||||||
|
external
|
||||||
|
returns (bytes32[] memory ids);
|
||||||
|
}
|
||||||
67
evm/src/interfaces/ISwapAdapterTypes.sol
Normal file
67
evm/src/interfaces/ISwapAdapterTypes.sol
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {IERC20} from "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||||
|
|
||||||
|
interface ISwapAdapterTypes {
|
||||||
|
/// @dev The OrderSide enum represents possible sides of a trade: Sell or
|
||||||
|
/// Buy. E.g. if OrderSide is Sell, the sell amount is interpreted to be
|
||||||
|
/// fixed.
|
||||||
|
enum OrderSide {
|
||||||
|
Sell,
|
||||||
|
Buy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev The Capability enum represents possible features of a trading
|
||||||
|
/// pool.
|
||||||
|
enum Capability {
|
||||||
|
Unset,
|
||||||
|
// Support OrderSide.Sell values (required)
|
||||||
|
SellOrder,
|
||||||
|
// Support OrderSide.Buy values (optional)
|
||||||
|
BuyOrder,
|
||||||
|
// Support evaluating the price function (optional)
|
||||||
|
PriceFunction,
|
||||||
|
// Support tokens that charge a fee on transfer (optional)
|
||||||
|
FeeOnTransfer,
|
||||||
|
// The pool does not suffer from price impact and maintains a constant
|
||||||
|
// price for increasingly larger specified amounts. (optional)
|
||||||
|
ConstantPrice,
|
||||||
|
// Indicates that the pool does not read its own token balances
|
||||||
|
// from token contracts while swapping. (optional)
|
||||||
|
TokenBalanceIndependent,
|
||||||
|
// Indicates that prices are returned scaled, else it is assumed prices
|
||||||
|
// still require scaling by token decimals. (required)
|
||||||
|
ScaledPrices
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev Representation used for rational numbers such as prices.
|
||||||
|
// TODO: Use only uint128 for numerator and denominator.
|
||||||
|
struct Fraction {
|
||||||
|
uint256 numerator;
|
||||||
|
uint256 denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev The Trade struct holds data about an executed trade.
|
||||||
|
struct Trade {
|
||||||
|
// If the side is sell, it is the amount of tokens sold. If the side is
|
||||||
|
// buy, it is the amount of tokens bought.
|
||||||
|
uint256 calculatedAmount;
|
||||||
|
// The amount of gas used in the trade.
|
||||||
|
uint256 gasUsed;
|
||||||
|
// The price of the pool after the trade. For zero use Fraction(0, 1).
|
||||||
|
Fraction price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @dev The Unavailable error is thrown when a pool or swap is not
|
||||||
|
/// available for unexpected reason. E.g. it was paused due to a bug.
|
||||||
|
error Unavailable(string reason);
|
||||||
|
|
||||||
|
/// @dev The LimitExceeded error is thrown when a limit has been exceeded.
|
||||||
|
/// E.g. the specified amount can't be traded safely.
|
||||||
|
error LimitExceeded(uint256 limit);
|
||||||
|
|
||||||
|
/// @dev The NotImplemented error is thrown when a function is not
|
||||||
|
/// implemented.
|
||||||
|
error NotImplemented(string reason);
|
||||||
|
}
|
||||||
26
evm/src/libraries/FractionMath.sol
Normal file
26
evm/src/libraries/FractionMath.sol
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
|
||||||
|
library FractionMath {
|
||||||
|
/// @dev Compares two Fraction instances from ISwapAdapterTypes.
|
||||||
|
/// @param frac1 The first Fraction instance.
|
||||||
|
/// @param frac2 The second Fraction instance.
|
||||||
|
/// @return int8 Returns 0 if fractions are equal, 1 if frac1 is greater, -1
|
||||||
|
/// if frac1 is lesser.
|
||||||
|
function compareFractions(
|
||||||
|
ISwapAdapterTypes.Fraction memory frac1,
|
||||||
|
ISwapAdapterTypes.Fraction memory frac2
|
||||||
|
) internal pure returns (int8) {
|
||||||
|
uint256 crossProduct1 = frac1.numerator * frac2.denominator;
|
||||||
|
uint256 crossProduct2 = frac2.numerator * frac1.denominator;
|
||||||
|
|
||||||
|
// fractions are equal
|
||||||
|
if (crossProduct1 == crossProduct2) return 0;
|
||||||
|
// frac1 is greater than frac2
|
||||||
|
else if (crossProduct1 > crossProduct2) return 1;
|
||||||
|
// frac1 is less than frac2
|
||||||
|
else return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
evm/src/template/TemplateSwapAdapter.sol
Normal file
58
evm/src/template/TemplateSwapAdapter.sol
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
|
|
||||||
|
/// @title TemplateSwapAdapter
|
||||||
|
/// @dev This is a template for a swap adapter.
|
||||||
|
/// Rename it to your own protocol's name and implement it according to the
|
||||||
|
/// specification.
|
||||||
|
contract TemplateSwapAdapter is ISwapAdapter {
|
||||||
|
function price(
|
||||||
|
bytes32 _poolId,
|
||||||
|
IERC20 _sellToken,
|
||||||
|
IERC20 _buyToken,
|
||||||
|
uint256[] memory _specifiedAmounts
|
||||||
|
) external view override returns (Fraction[] memory _prices) {
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.price");
|
||||||
|
}
|
||||||
|
|
||||||
|
function swap(
|
||||||
|
bytes32 poolId,
|
||||||
|
IERC20 sellToken,
|
||||||
|
IERC20 buyToken,
|
||||||
|
OrderSide side,
|
||||||
|
uint256 specifiedAmount
|
||||||
|
) external returns (Trade memory trade) {
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.swap");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
returns (uint256[] memory limits)
|
||||||
|
{
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.getLimits");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
|
external
|
||||||
|
returns (Capability[] memory capabilities)
|
||||||
|
{
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.getCapabilities");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokens(bytes32 poolId)
|
||||||
|
external
|
||||||
|
returns (IERC20[] memory tokens)
|
||||||
|
{
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.getTokens");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoolIds(uint256 offset, uint256 limit)
|
||||||
|
external
|
||||||
|
returns (bytes32[] memory ids)
|
||||||
|
{
|
||||||
|
revert NotImplemented("TemplateSwapAdapter.getPoolIds");
|
||||||
|
}
|
||||||
|
}
|
||||||
36
evm/src/template/manifest.yaml
Normal file
36
evm/src/template/manifest.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# information about the author helps us reach out in case of issues.
|
||||||
|
author:
|
||||||
|
name: YourCompany
|
||||||
|
email: developer@yourcompany.xyz
|
||||||
|
|
||||||
|
# Protocol Constants
|
||||||
|
constants:
|
||||||
|
protocol_gas: 30000
|
||||||
|
# minimum capabilities we can expect, individual pools may extend these
|
||||||
|
capabilities:
|
||||||
|
- SellSide
|
||||||
|
- BuySide
|
||||||
|
- PriceFunction
|
||||||
|
|
||||||
|
# The file containing the adapter contract
|
||||||
|
contract: TemplateSwapAdapter.sol
|
||||||
|
|
||||||
|
# Deployment instances used to generate chain specific bytecode.
|
||||||
|
instances:
|
||||||
|
- chain:
|
||||||
|
name: mainnet
|
||||||
|
id: 0
|
||||||
|
arguments:
|
||||||
|
- "0xBA12222222228d8Ba445958a75a0704d566BF2C8"
|
||||||
|
|
||||||
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
|
# getTokens are not implemented.
|
||||||
|
tests:
|
||||||
|
instances:
|
||||||
|
- pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
||||||
|
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||||
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
|
block: 17000000
|
||||||
|
chain:
|
||||||
|
id: 0
|
||||||
|
name: mainnet
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
import "interfaces/ISwapAdapter.sol";
|
import {IERC20, ISwapAdapter} from "src/interfaces/ISwapAdapter.sol";
|
||||||
|
|
||||||
|
// Uniswap handles arbirary amounts, but we limit the amount to 10x just in case
|
||||||
|
uint256 constant RESERVE_LIMIT_FACTOR = 10;
|
||||||
|
|
||||||
contract UniswapV2SwapAdapter is ISwapAdapter {
|
contract UniswapV2SwapAdapter is ISwapAdapter {
|
||||||
IUniswapV2Factory immutable factory;
|
IUniswapV2Factory immutable factory;
|
||||||
@@ -10,14 +13,15 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
factory = IUniswapV2Factory(factory_);
|
factory = IUniswapV2Factory(factory_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
function price(
|
function price(
|
||||||
bytes32 pairId,
|
bytes32 poolId,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
IERC20 buyToken,
|
IERC20 buyToken,
|
||||||
uint256[] memory sellAmounts
|
uint256[] memory specifiedAmounts
|
||||||
) external view override returns (Fraction[] memory prices) {
|
) external view override returns (Fraction[] memory prices) {
|
||||||
prices = new Fraction[](sellAmounts.length);
|
prices = new Fraction[](specifiedAmounts.length);
|
||||||
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
|
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId)));
|
||||||
uint112 r0;
|
uint112 r0;
|
||||||
uint112 r1;
|
uint112 r1;
|
||||||
if (sellToken < buyToken) {
|
if (sellToken < buyToken) {
|
||||||
@@ -26,11 +30,16 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
(r1, r0,) = pair.getReserves();
|
(r1, r0,) = pair.getReserves();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint256 i = 0; i < sellAmounts.length; i++) {
|
for (uint256 i = 0; i < specifiedAmounts.length; i++) {
|
||||||
prices[i] = getPriceAt(sellAmounts[i], r0, r1);
|
prices[i] = getPriceAt(specifiedAmounts[i], r0, r1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Calculates pool prices for specified amounts
|
||||||
|
/// @param amountIn The amount of the token being sold.
|
||||||
|
/// @param reserveIn The reserve of the token being sold.
|
||||||
|
/// @param reserveOut The reserve of the token being bought.
|
||||||
|
/// @return The price as a fraction corresponding to the provided amount.
|
||||||
function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
|
function getPriceAt(uint256 amountIn, uint256 reserveIn, uint256 reserveOut)
|
||||||
internal
|
internal
|
||||||
pure
|
pure
|
||||||
@@ -48,19 +57,19 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
return Fraction(newReserveOut * 1000, newReserveIn * 997);
|
return Fraction(newReserveOut * 1000, newReserveIn * 997);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
function swap(
|
function swap(
|
||||||
bytes32 pairId,
|
bytes32 poolId,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
IERC20 buyToken,
|
IERC20 buyToken,
|
||||||
SwapSide side,
|
OrderSide side,
|
||||||
uint256 specifiedAmount
|
uint256 specifiedAmount
|
||||||
) external override returns (Trade memory trade) {
|
) external override returns (Trade memory trade) {
|
||||||
if (specifiedAmount == 0) {
|
if (specifiedAmount == 0) {
|
||||||
return trade;
|
return trade;
|
||||||
}
|
}
|
||||||
|
|
||||||
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
|
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId)));
|
||||||
uint256 gasBefore = 0;
|
|
||||||
uint112 r0;
|
uint112 r0;
|
||||||
uint112 r1;
|
uint112 r1;
|
||||||
bool zero2one = sellToken < buyToken;
|
bool zero2one = sellToken < buyToken;
|
||||||
@@ -69,18 +78,26 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
} else {
|
} else {
|
||||||
(r1, r0,) = pair.getReserves();
|
(r1, r0,) = pair.getReserves();
|
||||||
}
|
}
|
||||||
gasBefore = gasleft();
|
uint256 gasBefore = gasleft();
|
||||||
if (side == SwapSide.Sell) {
|
if (side == OrderSide.Sell) {
|
||||||
trade.receivedAmount =
|
trade.calculatedAmount =
|
||||||
sell(pair, sellToken, zero2one, r0, r1, specifiedAmount);
|
sell(pair, sellToken, zero2one, r0, r1, specifiedAmount);
|
||||||
} else {
|
} else {
|
||||||
trade.receivedAmount =
|
trade.calculatedAmount =
|
||||||
buy(pair, sellToken, zero2one, r0, r1, specifiedAmount);
|
buy(pair, sellToken, zero2one, r0, r1, specifiedAmount);
|
||||||
}
|
}
|
||||||
trade.gasUsed = gasBefore - gasleft();
|
trade.gasUsed = gasBefore - gasleft();
|
||||||
trade.price = getPriceAt(specifiedAmount, r0, r1);
|
trade.price = getPriceAt(specifiedAmount, r0, r1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Executes a sell order on a given pool.
|
||||||
|
/// @param pair The pair to trade on.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param zero2one Whether the sell token is token0 or token1.
|
||||||
|
/// @param reserveIn The reserve of the token being sold.
|
||||||
|
/// @param reserveOut The reserve of the token being bought.
|
||||||
|
/// @param amount The amount to be traded.
|
||||||
|
/// @return calculatedAmount The amount of tokens received.
|
||||||
function sell(
|
function sell(
|
||||||
IUniswapV2Pair pair,
|
IUniswapV2Pair pair,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
@@ -88,11 +105,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
uint112 reserveIn,
|
uint112 reserveIn,
|
||||||
uint112 reserveOut,
|
uint112 reserveOut,
|
||||||
uint256 amount
|
uint256 amount
|
||||||
) internal returns (uint256 receivedAmount) {
|
) internal returns (uint256 calculatedAmount) {
|
||||||
address swapper = msg.sender;
|
address swapper = msg.sender;
|
||||||
|
uint256 amountOut = getAmountOut(amount, reserveIn, reserveOut);
|
||||||
|
|
||||||
// TODO: use safeTransferFrom
|
// TODO: use safeTransferFrom
|
||||||
sellToken.transferFrom(swapper, address(pair), amount);
|
sellToken.transferFrom(swapper, address(pair), amount);
|
||||||
uint256 amountOut = getAmountOut(amount, reserveIn, reserveOut);
|
|
||||||
if (zero2one) {
|
if (zero2one) {
|
||||||
pair.swap(0, amountOut, swapper, "");
|
pair.swap(0, amountOut, swapper, "");
|
||||||
} else {
|
} else {
|
||||||
@@ -101,7 +119,12 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
return amountOut;
|
return amountOut;
|
||||||
}
|
}
|
||||||
|
|
||||||
// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
|
/// @notice Given an input amount of an asset and pair reserves, returns the
|
||||||
|
/// maximum output amount of the other asset
|
||||||
|
/// @param amountIn The amount of the token being sold.
|
||||||
|
/// @param reserveIn The reserve of the token being sold.
|
||||||
|
/// @param reserveOut The reserve of the token being bought.
|
||||||
|
/// @return amountOut The amount of tokens received.
|
||||||
function getAmountOut(
|
function getAmountOut(
|
||||||
uint256 amountIn,
|
uint256 amountIn,
|
||||||
uint256 reserveIn,
|
uint256 reserveIn,
|
||||||
@@ -119,6 +142,14 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
amountOut = numerator / denominator;
|
amountOut = numerator / denominator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @notice Execute a buy order on a given pool.
|
||||||
|
/// @param pair The pair to trade on.
|
||||||
|
/// @param sellToken The token being sold.
|
||||||
|
/// @param zero2one Whether the sell token is token0 or token1.
|
||||||
|
/// @param reserveIn The reserve of the token being sold.
|
||||||
|
/// @param reserveOut The reserve of the token being bought.
|
||||||
|
/// @param amountOut The amount of tokens to be bought.
|
||||||
|
/// @return calculatedAmount The amount of tokens sold.
|
||||||
function buy(
|
function buy(
|
||||||
IUniswapV2Pair pair,
|
IUniswapV2Pair pair,
|
||||||
IERC20 sellToken,
|
IERC20 sellToken,
|
||||||
@@ -126,9 +157,10 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
uint112 reserveIn,
|
uint112 reserveIn,
|
||||||
uint112 reserveOut,
|
uint112 reserveOut,
|
||||||
uint256 amountOut
|
uint256 amountOut
|
||||||
) internal returns (uint256 receivedAmount) {
|
) internal returns (uint256 calculatedAmount) {
|
||||||
address swapper = msg.sender;
|
address swapper = msg.sender;
|
||||||
uint256 amount = getAmountIn(amountOut, reserveIn, reserveOut);
|
uint256 amount = getAmountIn(amountOut, reserveIn, reserveOut);
|
||||||
|
|
||||||
if (amount == 0) {
|
if (amount == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -142,65 +174,76 @@ contract UniswapV2SwapAdapter is ISwapAdapter {
|
|||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// given an output amount of an asset and pair reserves, returns a required input amount of the other asset
|
/// @notice Given an output amount of an asset and pair reserves, returns a
|
||||||
|
/// required input amount of the other asset
|
||||||
|
/// @param amountOut The amount of the token being bought.
|
||||||
|
/// @param reserveIn The reserve of the token being sold.
|
||||||
|
/// @param reserveOut The reserve of the token being bought.
|
||||||
function getAmountIn(
|
function getAmountIn(
|
||||||
uint256 amountOut,
|
uint256 amountOut,
|
||||||
uint256 reserveIn,
|
uint256 reserveIn,
|
||||||
uint256 reserveOut
|
uint256 reserveOut
|
||||||
) internal pure returns (uint256 amountIn) {
|
) internal pure returns (uint256 amountIn) {
|
||||||
if (amountIn == 0) {
|
if (amountOut == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (reserveIn == 0 || reserveOut == 0) {
|
if (reserveIn == 0) {
|
||||||
revert Unavailable("At least one reserve is zero!");
|
revert Unavailable("reserveIn is zero");
|
||||||
|
}
|
||||||
|
if (reserveOut == 0) {
|
||||||
|
revert Unavailable("reserveOut is zero");
|
||||||
}
|
}
|
||||||
uint256 numerator = reserveIn * amountOut * 1000;
|
uint256 numerator = reserveIn * amountOut * 1000;
|
||||||
uint256 denominator = (reserveOut - amountOut) * 997;
|
uint256 denominator = (reserveOut - amountOut) * 997;
|
||||||
amountIn = (numerator / denominator) + 1;
|
amountIn = (numerator / denominator) + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLimits(bytes32 pairId, SwapSide side)
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getLimits(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
|
||||||
external
|
external
|
||||||
view
|
view
|
||||||
override
|
override
|
||||||
returns (uint256[] memory limits)
|
returns (uint256[] memory limits)
|
||||||
{
|
{
|
||||||
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
|
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId)));
|
||||||
limits = new uint256[](2);
|
limits = new uint256[](2);
|
||||||
(uint256 r0, uint256 r1,) = pair.getReserves();
|
(uint256 r0, uint256 r1,) = pair.getReserves();
|
||||||
if (side == SwapSide.Sell) {
|
if (sellToken < buyToken) {
|
||||||
limits[0] = r0 * 10;
|
limits[0] = r0 / RESERVE_LIMIT_FACTOR;
|
||||||
limits[1] = r1 * 10;
|
limits[1] = r1 / RESERVE_LIMIT_FACTOR;
|
||||||
} else {
|
} else {
|
||||||
limits[0] = r1 * 10;
|
limits[0] = r1 / RESERVE_LIMIT_FACTOR;
|
||||||
limits[1] = r0 * 10;
|
limits[1] = r0 / RESERVE_LIMIT_FACTOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
function getCapabilities(bytes32, IERC20, IERC20)
|
function getCapabilities(bytes32, IERC20, IERC20)
|
||||||
external
|
external
|
||||||
pure
|
pure
|
||||||
override
|
override
|
||||||
returns (Capabilities[] memory capabilities)
|
returns (Capability[] memory capabilities)
|
||||||
{
|
{
|
||||||
capabilities = new Capabilities[](3);
|
capabilities = new Capability[](3);
|
||||||
capabilities[0] = Capabilities.SellSide;
|
capabilities[0] = Capability.SellOrder;
|
||||||
capabilities[1] = Capabilities.BuySide;
|
capabilities[1] = Capability.BuyOrder;
|
||||||
capabilities[2] = Capabilities.PriceFunction;
|
capabilities[2] = Capability.PriceFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTokens(bytes32 pairId)
|
/// @inheritdoc ISwapAdapter
|
||||||
|
function getTokens(bytes32 poolId)
|
||||||
external
|
external
|
||||||
view
|
view
|
||||||
override
|
override
|
||||||
returns (IERC20[] memory tokens)
|
returns (IERC20[] memory tokens)
|
||||||
{
|
{
|
||||||
tokens = new IERC20[](2);
|
tokens = new IERC20[](2);
|
||||||
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(pairId)));
|
IUniswapV2Pair pair = IUniswapV2Pair(address(bytes20(poolId)));
|
||||||
tokens[0] = IERC20(pair.token0());
|
tokens[0] = IERC20(pair.token0());
|
||||||
tokens[1] = IERC20(pair.token1());
|
tokens[1] = IERC20(pair.token1());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @inheritdoc ISwapAdapter
|
||||||
function getPoolIds(uint256 offset, uint256 limit)
|
function getPoolIds(uint256 offset, uint256 limit)
|
||||||
external
|
external
|
||||||
view
|
view
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ instances:
|
|||||||
arguments:
|
arguments:
|
||||||
- "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
|
- "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
|
||||||
|
|
||||||
# Specify some automatic test cases in case getPools and
|
# Specify some automatic test cases in case getPoolIds and
|
||||||
# getTokens are not implemented.
|
# getTokens are not implemented.
|
||||||
tests:
|
tests:
|
||||||
instances:
|
instances:
|
||||||
- pair_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
- pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
|
||||||
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
||||||
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||||
block: 17000000
|
block: 17000000
|
||||||
|
|||||||
229
evm/test/BalancerV2SwapAdapter.t.sol
Normal file
229
evm/test/BalancerV2SwapAdapter.t.sol
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
import {
|
||||||
|
BalancerV2SwapAdapter,
|
||||||
|
IERC20,
|
||||||
|
IVault
|
||||||
|
} from "src/balancer-v2/BalancerV2SwapAdapter.sol";
|
||||||
|
import {ISwapAdapterTypes} from "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
import {FractionMath} from "src/libraries/FractionMath.sol";
|
||||||
|
|
||||||
|
contract BalancerV2SwapAdapterTest is Test, ISwapAdapterTypes {
|
||||||
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
IVault constant balancerV2Vault =
|
||||||
|
IVault(payable(0xBA12222222228d8Ba445958a75a0704d566BF2C8));
|
||||||
|
BalancerV2SwapAdapter adapter;
|
||||||
|
|
||||||
|
IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||||
|
IERC20 constant BAL = IERC20(0xba100000625a3754423978a60c9317c58a424e3D);
|
||||||
|
address constant B_80BAL_20WETH = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56;
|
||||||
|
bytes32 constant B_80BAL_20WETH_POOL_ID =
|
||||||
|
0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014;
|
||||||
|
|
||||||
|
uint256 constant TEST_ITERATIONS = 100;
|
||||||
|
|
||||||
|
function setUp() public {
|
||||||
|
uint256 forkBlock = 18710000;
|
||||||
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
|
|
||||||
|
adapter = new BalancerV2SwapAdapter(payable(address(balancerV2Vault)));
|
||||||
|
|
||||||
|
vm.label(address(balancerV2Vault), "IVault");
|
||||||
|
vm.label(address(adapter), "BalancerV2SwapAdapter");
|
||||||
|
vm.label(address(WETH), "WETH");
|
||||||
|
vm.label(address(BAL), "BAL");
|
||||||
|
vm.label(address(B_80BAL_20WETH), "B_80BAL_20WETH");
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPrice() public {
|
||||||
|
uint256[] memory amounts = new uint256[](2);
|
||||||
|
amounts[0] = 100;
|
||||||
|
amounts[1] = 200;
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
NotImplemented.selector, "BalancerV2SwapAdapter.price"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
adapter.price(B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPriceSingleFuzz() public {
|
||||||
|
uint256 specifiedAmount = 100 * 10 ** 18;
|
||||||
|
// Assume OrderSide.Sell
|
||||||
|
uint256[] memory limits =
|
||||||
|
adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH);
|
||||||
|
|
||||||
|
vm.assume(specifiedAmount > 0);
|
||||||
|
vm.assume(specifiedAmount < limits[0]);
|
||||||
|
|
||||||
|
Fraction memory price = adapter.priceSingle(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, specifiedAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
assertGt(price.numerator, 0);
|
||||||
|
assertGt(price.denominator, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPriceDecreasing() public {
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
Fraction[] memory prices = new Fraction[](TEST_ITERATIONS);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = 1000 * (i + 1) * 10 ** 18;
|
||||||
|
prices[i] = adapter.priceSingle(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertEq(prices[i].compareFractions(prices[i + 1]), 1);
|
||||||
|
assertGt(prices[i].denominator, 0);
|
||||||
|
assertGt(prices[i + 1].denominator, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public {
|
||||||
|
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
|
||||||
|
vm.assume(specifiedAmount > 0);
|
||||||
|
|
||||||
|
uint256[] memory limits =
|
||||||
|
adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH);
|
||||||
|
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
vm.assume(specifiedAmount < limits[1]);
|
||||||
|
|
||||||
|
// TODO calculate the amountIn by using price function as in
|
||||||
|
// testPriceDecreasing
|
||||||
|
deal(address(BAL), address(this), type(uint256).max);
|
||||||
|
BAL.approve(address(adapter), type(uint256).max);
|
||||||
|
} else {
|
||||||
|
vm.assume(specifiedAmount < limits[0]);
|
||||||
|
|
||||||
|
deal(address(BAL), address(this), specifiedAmount);
|
||||||
|
BAL.approve(address(adapter), specifiedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 bal_balance = BAL.balanceOf(address(this));
|
||||||
|
uint256 weth_balance = WETH.balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade = adapter.swap(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, side, specifiedAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
if (trade.calculatedAmount > 0) {
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
WETH.balanceOf(address(this)) - weth_balance
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
bal_balance - BAL.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount, bal_balance - BAL.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
WETH.balanceOf(address(this)) - weth_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapSellIncreasing() public {
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = 1000 * (i + 1) * 10 ** 18;
|
||||||
|
|
||||||
|
uint256 beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
|
deal(address(BAL), address(this), amounts[i]);
|
||||||
|
BAL.approve(address(adapter), amounts[i]);
|
||||||
|
trades[i] = adapter.swap(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Sell, amounts[i]
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.revertTo(beforeSwap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
|
assertLe(trades[i].gasUsed, trades[i + 1].gasUsed);
|
||||||
|
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testSwapBuyIncreasing() public {
|
||||||
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
|
amounts[i] = 10 * (i + 1) * 10 ** 18;
|
||||||
|
|
||||||
|
uint256 beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
|
Fraction memory price = adapter.priceSingle(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, amounts[i]
|
||||||
|
);
|
||||||
|
uint256 amountIn =
|
||||||
|
(amounts[i] * price.denominator / price.numerator) * 2;
|
||||||
|
|
||||||
|
deal(address(BAL), address(this), amountIn);
|
||||||
|
BAL.approve(address(adapter), amountIn);
|
||||||
|
trades[i] = adapter.swap(
|
||||||
|
B_80BAL_20WETH_POOL_ID, BAL, WETH, OrderSide.Buy, amounts[i]
|
||||||
|
);
|
||||||
|
|
||||||
|
vm.revertTo(beforeSwap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
|
||||||
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
|
assertLe(trades[i].gasUsed, trades[i + 1].gasUsed);
|
||||||
|
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetLimits() public view {
|
||||||
|
uint256[] memory limits =
|
||||||
|
adapter.getLimits(B_80BAL_20WETH_POOL_ID, BAL, WETH);
|
||||||
|
|
||||||
|
assert(limits.length == 2);
|
||||||
|
assert(limits[0] > 0);
|
||||||
|
assert(limits[1] > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetCapabilitiesFuzz(bytes32 pool, address t0, address t1)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
Capability[] memory res =
|
||||||
|
adapter.getCapabilities(pool, IERC20(t0), IERC20(t1));
|
||||||
|
|
||||||
|
assertEq(res.length, 2);
|
||||||
|
assertEq(uint256(res[0]), uint256(Capability.SellOrder));
|
||||||
|
assertEq(uint256(res[1]), uint256(Capability.BuyOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetTokens() public {
|
||||||
|
IERC20[] memory tokens = adapter.getTokens(B_80BAL_20WETH_POOL_ID);
|
||||||
|
|
||||||
|
assertEq(address(tokens[0]), address(BAL));
|
||||||
|
assertEq(address(tokens[1]), address(WETH));
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGetPoolIds() public {
|
||||||
|
vm.expectRevert(
|
||||||
|
abi.encodeWithSelector(
|
||||||
|
NotImplemented.selector, "BalancerV2SwapAdapter.getPoolIds"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
adapter.getPoolIds(100, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
evm/test/TemplateSwapAdapter.t.sol
Normal file
18
evm/test/TemplateSwapAdapter.t.sol
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
|
import "forge-std/Test.sol";
|
||||||
|
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
import "src/libraries/FractionMath.sol";
|
||||||
|
|
||||||
|
/// @title TemplateSwapAdapterTest
|
||||||
|
/// @dev This is a template for a swap adapter test.
|
||||||
|
/// Test all functions that are implemented in your swap adapter, the two test included here are just an example.
|
||||||
|
/// Feel free to use UniswapV2SwapAdapterTest and BalancerV2SwapAdapterTest as a reference.
|
||||||
|
contract TemplateSwapAdapterTest is Test, ISwapAdapterTypes {
|
||||||
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
function testPriceFuzz(uint256 amount0, uint256 amount1) public {}
|
||||||
|
|
||||||
|
function testSwapFuzz(uint256 specifiedAmount) public {}
|
||||||
|
}
|
||||||
@@ -1,27 +1,37 @@
|
|||||||
// SPDX-License-Identifier: UNLICENSED
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
pragma solidity ^0.8.13;
|
pragma solidity ^0.8.13;
|
||||||
|
|
||||||
import "forge-std/Test.sol";
|
import "forge-std/Test.sol";
|
||||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||||
import "src/uniswap-v2/UniswapV2SwapAdapter.sol";
|
import "src/uniswap-v2/UniswapV2SwapAdapter.sol";
|
||||||
import "interfaces/ISwapAdapterTypes.sol";
|
import "src/interfaces/ISwapAdapterTypes.sol";
|
||||||
|
import "src/libraries/FractionMath.sol";
|
||||||
|
|
||||||
contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
|
contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
|
||||||
UniswapV2SwapAdapter pairFunctions;
|
using FractionMath for Fraction;
|
||||||
|
|
||||||
|
UniswapV2SwapAdapter adapter;
|
||||||
IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
IERC20 constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
||||||
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
||||||
address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc;
|
address constant USDC_WETH_PAIR = 0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc;
|
||||||
|
|
||||||
|
uint256 constant TEST_ITERATIONS = 100;
|
||||||
|
|
||||||
function setUp() public {
|
function setUp() public {
|
||||||
uint256 forkBlock = 17000000;
|
uint256 forkBlock = 17000000;
|
||||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||||
pairFunctions = new
|
adapter = new
|
||||||
UniswapV2SwapAdapter(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
|
UniswapV2SwapAdapter(0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f);
|
||||||
|
|
||||||
|
vm.label(address(adapter), "UniswapV2SwapAdapter");
|
||||||
|
vm.label(address(WETH), "WETH");
|
||||||
|
vm.label(address(USDC), "USDC");
|
||||||
|
vm.label(address(USDC_WETH_PAIR), "USDC_WETH_PAIR");
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPriceFuzz(uint256 amount0, uint256 amount1) public {
|
function testPriceFuzz(uint256 amount0, uint256 amount1) public {
|
||||||
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
||||||
uint256[] memory limits = pairFunctions.getLimits(pair, SwapSide.Sell);
|
uint256[] memory limits = adapter.getLimits(pair, USDC, WETH);
|
||||||
vm.assume(amount0 < limits[0]);
|
vm.assume(amount0 < limits[0]);
|
||||||
vm.assume(amount1 < limits[0]);
|
vm.assume(amount1 < limits[0]);
|
||||||
|
|
||||||
@@ -29,104 +39,126 @@ contract UniswapV2PairFunctionTest is Test, ISwapAdapterTypes {
|
|||||||
amounts[0] = amount0;
|
amounts[0] = amount0;
|
||||||
amounts[1] = amount1;
|
amounts[1] = amount1;
|
||||||
|
|
||||||
Fraction[] memory prices =
|
Fraction[] memory prices = adapter.price(pair, WETH, USDC, amounts);
|
||||||
pairFunctions.price(pair, WETH, USDC, amounts);
|
|
||||||
|
|
||||||
for (uint256 i = 0; i < prices.length; i++) {
|
for (uint256 i = 0; i < prices.length; i++) {
|
||||||
assertGt(prices[i].nominator, 0);
|
assertGt(prices[i].numerator, 0);
|
||||||
assertGt(prices[i].denominator, 0);
|
assertGt(prices[i].denominator, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testPriceDecreasing() public {
|
function testPriceDecreasing() public {
|
||||||
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
||||||
uint256[] memory amounts = new uint256[](100);
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
|
|
||||||
for (uint256 i = 0; i < 100; i++) {
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
amounts[i] = 1000 * i * 10 ** 6;
|
amounts[i] = 1000 * i * 10 ** 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
Fraction[] memory prices =
|
Fraction[] memory prices = adapter.price(pair, WETH, USDC, amounts);
|
||||||
pairFunctions.price(pair, WETH, USDC, amounts);
|
|
||||||
|
|
||||||
for (uint256 i = 0; i < 99; i++) {
|
for (uint256 i = 0; i < TEST_ITERATIONS - 1; i++) {
|
||||||
assertEq(compareFractions(prices[i], prices[i + 1]), 1);
|
assertEq(prices[i].compareFractions(prices[i + 1]), 1);
|
||||||
assertGt(prices[i].denominator, 0);
|
assertGt(prices[i].denominator, 0);
|
||||||
assertGt(prices[i + 1].denominator, 0);
|
assertGt(prices[i + 1].denominator, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareFractions(Fraction memory frac1, Fraction memory frac2)
|
function testSwapFuzz(uint256 specifiedAmount, bool isBuy) public {
|
||||||
internal
|
OrderSide side = isBuy ? OrderSide.Buy : OrderSide.Sell;
|
||||||
pure
|
|
||||||
returns (int8)
|
|
||||||
{
|
|
||||||
uint256 crossProduct1 = frac1.nominator * frac2.denominator;
|
|
||||||
uint256 crossProduct2 = frac2.nominator * frac1.denominator;
|
|
||||||
|
|
||||||
if (crossProduct1 == crossProduct2) return 0; // fractions are equal
|
|
||||||
|
|
||||||
else if (crossProduct1 > crossProduct2) return 1; // frac1 is greater than frac2
|
|
||||||
|
|
||||||
else return -1; // frac1 is less than frac2
|
|
||||||
}
|
|
||||||
|
|
||||||
function testSwapFuzz(uint256 amount, bool isBuy) public {
|
|
||||||
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
||||||
SwapSide side = SwapSide.Sell;
|
uint256[] memory limits = adapter.getLimits(pair, USDC, WETH);
|
||||||
if (isBuy) {
|
|
||||||
side = SwapSide.Buy;
|
|
||||||
}
|
|
||||||
uint256[] memory limits = pairFunctions.getLimits(pair, side);
|
|
||||||
vm.assume(amount < limits[0]);
|
|
||||||
deal(address(USDC), address(this), amount);
|
|
||||||
USDC.approve(address(pairFunctions), amount);
|
|
||||||
|
|
||||||
pairFunctions.swap(pair, USDC, WETH, side, amount);
|
if (side == OrderSide.Buy) {
|
||||||
|
vm.assume(specifiedAmount < limits[1]);
|
||||||
|
|
||||||
|
// TODO calculate the amountIn by using price function as in
|
||||||
|
// BalancerV2 testPriceDecreasing
|
||||||
|
deal(address(USDC), address(this), type(uint256).max);
|
||||||
|
USDC.approve(address(adapter), type(uint256).max);
|
||||||
|
} else {
|
||||||
|
vm.assume(specifiedAmount < limits[0]);
|
||||||
|
|
||||||
|
deal(address(USDC), address(this), specifiedAmount);
|
||||||
|
USDC.approve(address(adapter), specifiedAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint256 usdc_balance = USDC.balanceOf(address(this));
|
||||||
|
uint256 weth_balance = WETH.balanceOf(address(this));
|
||||||
|
|
||||||
|
Trade memory trade =
|
||||||
|
adapter.swap(pair, USDC, WETH, side, specifiedAmount);
|
||||||
|
|
||||||
|
if (trade.calculatedAmount > 0) {
|
||||||
|
if (side == OrderSide.Buy) {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
WETH.balanceOf(address(this)) - weth_balance
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
usdc_balance - USDC.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assertEq(
|
||||||
|
specifiedAmount,
|
||||||
|
usdc_balance - USDC.balanceOf(address(this))
|
||||||
|
);
|
||||||
|
assertEq(
|
||||||
|
trade.calculatedAmount,
|
||||||
|
WETH.balanceOf(address(this)) - weth_balance
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSwapSellIncreasing() public {
|
function testSwapSellIncreasing() public {
|
||||||
executeIncreasingSwaps(SwapSide.Sell);
|
executeIncreasingSwaps(OrderSide.Sell);
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeIncreasingSwaps(SwapSide side) internal {
|
function executeIncreasingSwaps(OrderSide side) internal {
|
||||||
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
||||||
|
|
||||||
uint256[] memory amounts = new uint256[](100);
|
uint256[] memory amounts = new uint256[](TEST_ITERATIONS);
|
||||||
for (uint256 i = 0; i < 100; i++) {
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
amounts[i] = 1000 * i * 10 ** 6;
|
amounts[i] = 1000 * i * 10 ** 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
Trade[] memory trades = new Trade [](100);
|
Trade[] memory trades = new Trade[](TEST_ITERATIONS);
|
||||||
uint256 beforeSwap;
|
uint256 beforeSwap;
|
||||||
for (uint256 i = 0; i < 100; i++) {
|
for (uint256 i = 0; i < TEST_ITERATIONS; i++) {
|
||||||
beforeSwap = vm.snapshot();
|
beforeSwap = vm.snapshot();
|
||||||
|
|
||||||
deal(address(USDC), address(this), amounts[i]);
|
deal(address(USDC), address(this), amounts[i]);
|
||||||
USDC.approve(address(pairFunctions), amounts[i]);
|
USDC.approve(address(adapter), amounts[i]);
|
||||||
trades[i] = pairFunctions.swap(pair, USDC, WETH, side, amounts[i]);
|
|
||||||
|
trades[i] = adapter.swap(pair, USDC, WETH, side, amounts[i]);
|
||||||
vm.revertTo(beforeSwap);
|
vm.revertTo(beforeSwap);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (uint256 i = 1; i < 99; i++) {
|
for (uint256 i = 1; i < TEST_ITERATIONS - 1; i++) {
|
||||||
assertLe(trades[i].receivedAmount, trades[i + 1].receivedAmount);
|
assertLe(trades[i].calculatedAmount, trades[i + 1].calculatedAmount);
|
||||||
assertLe(trades[i].gasUsed, trades[i + 1].gasUsed);
|
assertLe(trades[i].gasUsed, trades[i + 1].gasUsed);
|
||||||
assertEq(compareFractions(trades[i].price, trades[i + 1].price), 1);
|
assertEq(trades[i].price.compareFractions(trades[i + 1].price), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function testSwapBuyIncreasing() public {
|
function testSwapBuyIncreasing() public {
|
||||||
executeIncreasingSwaps(SwapSide.Buy);
|
executeIncreasingSwaps(OrderSide.Buy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGetCapabilities(bytes32 pair, address t0, address t1) public {
|
function testGetCapabilities(bytes32 pair, address t0, address t1) public {
|
||||||
Capabilities[] memory res =
|
Capability[] memory res =
|
||||||
pairFunctions.getCapabilities(pair, IERC20(t0), IERC20(t1));
|
adapter.getCapabilities(pair, IERC20(t0), IERC20(t1));
|
||||||
|
|
||||||
assertEq(res.length, 3);
|
assertEq(res.length, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testGetLimits() public {
|
function testGetLimits() public {
|
||||||
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
bytes32 pair = bytes32(bytes20(USDC_WETH_PAIR));
|
||||||
pairFunctions.getLimits(pair, SwapSide.Sell);
|
uint256[] memory limits = adapter.getLimits(pair, USDC, WETH);
|
||||||
|
|
||||||
|
assertEq(limits.length, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user