Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions src/contracts/adapters/BlackholeV1Adapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// ╟╗ ╔╬
// ╞╬╬ ╬╠╬
// ╔╣╬╬╬ ╠╠╠╠╦
// ╬╬╬╬╬╩ ╘╠╠╠╠╬
// ║╬╬╬╬╬ ╘╠╠╠╠╬
// ╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ╒╬╬╬╬╬╬╬╜ ╠╠╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╬╬╬╬╬╬╬╬╠╠╠╠╠╠╠╠
// ╙╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬╕ ╬╬╬╬╬╬╬╜ ╣╠╠╬╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╬╬╬╬╬╬╬╬╬╠╠╠╠╠╠╠╩
// ╙╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ╔╬╬╬╬╬╬╬ ╔╠╠╠╬╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╣╬╬╬╬╬╬╬╬╬╬╬╠╠╠╠╝╙
// ╘╣╬╬╬╬╬╬╬╬╬╬╬╬╬╬ ╒╠╠╠╬╠╬╩╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬╣╬╬╬╬╬╬╬╙
// ╣╬╬╬╬╬╬╬╬╬╬╠╣ ╣╬╠╠╠╬╩ ╚╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬
// ╣╬╬╬╬╬╬╬╬╬╣ ╣╬╠╠╠╬╬ ╣╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬╬╬╬╬╬╬╬
// ╟╬╬╬╬╬╬╬╩ ╬╬╠╠╠╠╬╬╬╬╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬╠╬╬╬╬╬╬╬
// ╬╬╬╬╬╬╬ ╒╬╬╠╠╬╠╠╬╬╬╬╬╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╣╬╬╬╬╬╬╬
// ╬╬╬╬╬╬╬ ╬╬╬╠╠╠╠╝╝╝╝╝╝╝╠╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╚╬╬╬╬╬╬╬╬
// ╬╬╬╬╬╬╬ ╣╬╬╬╬╠╠╩ ╘╬╬╬╬╬╬╬ ╠╬╬╬╬╬╬╬ ╙╬╬╬╬╬╬╬╬
//

// SPDX-License-Identifier: GPL-3.0-only
pragma solidity ^0.8.0;

import "../interface/IERC20.sol";
import "../lib/SafeERC20.sol";
import "../YakAdapter.sol";

interface IPairFactory {
function isPair(address) external view returns (bool);
function getFee(address _pairAddress, bool _stable) external view returns(uint256);
function pairCodeHash() external view returns (bytes32);
function isGenesis(address pair) external view returns (bool);
function getPair(address tokenA, address token, bool stable) external view returns (address);
}

interface IPair {
function getAmountOut(uint256, address) external view returns (uint256);
function metadata() external view returns
(uint dec0, uint dec1, uint r0, uint r1, bool st, address t0, address t1);

function swap(
uint256,
uint256,
address,
bytes calldata
) external;
}

contract BlackholeV1Adapter is YakAdapter {
using SafeERC20 for IERC20;
struct PairSwapMetadata {
uint decimals0;
uint decimals1;
uint reserve0;
uint reserve1;
bool stable;
address token0;
address token1;
uint balanceA;
uint balanceB;
uint reserveA;
uint reserveB;
uint decimalsA;
uint decimalsB;
}
bytes32 immutable PAIR_CODE_HASH;
address immutable FACTORY;

constructor(
string memory _name,
address _factory,
uint256 _swapGasEstimate
) YakAdapter(_name, _swapGasEstimate) {
FACTORY = _factory;
PAIR_CODE_HASH = getPairCodeHash(_factory);
}

function getPairCodeHash(address _factory) internal view returns (bytes32) {
return IPairFactory(_factory).pairCodeHash();
}

function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
(token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
}

// calculates the CREATE2 address for a pair without making any external calls
function pairFor(address tokenA, address tokenB, bool stable) public view returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
return IPairFactory(FACTORY).getPair(token0, token1, stable);
}

function _getAmoutOutSafe(address pair, uint amountIn, address tokenIn) internal view returns (uint) {
try IPair(pair).getAmountOut(amountIn, tokenIn) returns (uint amountOut) {
return amountOut;
} catch {
return 0;
}
}

function getQuoteAndPair(
uint256 _amountIn,
address _tokenIn,
address _tokenOut
) internal view returns (uint256 amountOut, address pair) {
address pairStable = pairFor(_tokenIn, _tokenOut, true);
address pairVolatile = pairFor(_tokenIn, _tokenOut, false);
uint amountStable;
uint amountVolatile;
if (IPairFactory(FACTORY).isPair(pairStable) && !IPairFactory(FACTORY).isGenesis(pairStable)) {
amountStable = _getAmoutOutSafe(pairStable, _amountIn, _tokenIn);
}
if (IPairFactory(FACTORY).isPair(pairVolatile) && !IPairFactory(FACTORY).isGenesis(pairVolatile)) {
amountVolatile = _getAmoutOutSafe(pairVolatile, _amountIn, _tokenIn);
}
(amountOut, pair) = amountStable > amountVolatile ? (amountStable, pairStable) :
(amountVolatile, pairVolatile);
if (pair == address(0)) {
return (0, address(0));
}
return (amountOut, pair);
}

function _query(
uint256 _amountIn,
address _tokenIn,
address _tokenOut
) internal view override returns (uint256 amountOut) {
if (_tokenIn != _tokenOut && _amountIn != 0) (amountOut, ) = getQuoteAndPair(_amountIn, _tokenIn, _tokenOut);
}

function _swap(
uint256 _amountIn,
uint256 _amountOut,
address _tokenIn,
address _tokenOut,
address to
) internal override {
(uint256 amountOut, address pair) = getQuoteAndPair(_amountIn, _tokenIn, _tokenOut);
require(amountOut >= _amountOut, "Insufficent amount out");
(uint256 amount0Out, uint256 amount1Out) = (_tokenIn < _tokenOut)
? (uint256(0), amountOut)
: (amountOut, uint256(0));
IERC20(_tokenIn).safeTransfer(pair, _amountIn);
IPair(pair).swap(amount0Out, amount1Out, to, new bytes(0));
}
}
11 changes: 11 additions & 0 deletions src/deploy/avalanche/adapters/blackhole/blackhole.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { deployAdapter, addresses } = require('../../../utils')
const { factory } = addresses.avalanche.blackhole

const networkName = 'avalanche'
const contractName = 'BlackholeV1Adapter'
const tags = [ 'blackhole' ]
const name = 'BlackholeV1Adapter'
const gasEstimate = 340_000
const args = [ name, factory, gasEstimate ]

module.exports = deployAdapter(networkName, tags, name, contractName, args)
6 changes: 5 additions & 1 deletion src/misc/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@
"BONER": "0x32f3Fb90112bA4CAb66c24aA75ACbe0182A5d50B",
"ARENA": "0xB8d7710f7d8349A506b75dD184F05777c82dAd0C",
"BOIL": "0x0A9a9e0A695F52502CdDF7A59c880f4bDf2f0548",
"ID": "0x34a528Da3b2EA5c6Ad1796Eba756445D1299a577"
"ID": "0x34a528Da3b2EA5c6Ad1796Eba756445D1299a577",
"BLACK": "0xcd94a87696fac69edae3a70fe5725307ae1c43f6"
},
"unilikeRouters": {
"zero": "0x85995d5f8ee9645cA855e92de16FA62D26398060",
Expand Down Expand Up @@ -451,6 +452,9 @@
"pharaoh": {
"quoter": "0xc7d4412aa74c655B2e6e71bB6790d24AC90E393C",
"factory": "0xAAA32926fcE6bE95ea2c51cB4Fcb60836D320C42"
},
"blackhole": {
"factory": "0xfE926062Fb99CA5653080d6C14fE945Ad68c265C"
}
},
"dogechain": {
Expand Down
74 changes: 74 additions & 0 deletions src/test/spec/avalanche/adapters/blackhole.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { setTestEnv, addresses } = require("../../../utils/test-env");
const { blackhole } = addresses.avalanche;

describe("YakAdapter - Blackhole", function () {
let testEnv;
let tkns;
let ate;

before(async () => {
const networkName = "avalanche";
testEnv = await setTestEnv(networkName);
tkns = testEnv.supportedTkns;

const contractName = "BlackholeV1Adapter";
const gasEstimate = 340_000;
const adapterArgs = [contractName, blackhole.factory, gasEstimate];
ate = await testEnv.setAdapterEnv(contractName, adapterArgs);
});

beforeEach(async () => {
testEnv.updateTrader();
});

describe("Swapping matches query", () => {
it("50 EURC -> USDC", async () => {
const amountIn = "50";
const tokenIn = tkns.EURC;
const tokenOut = tkns.USDC;

const rawQuote = await ate.Adapter.query(
ethers.utils.parseUnits(amountIn, 6),
tokenIn.address,
tokenOut.address
);

console.log(`💱 Quote for swapping ${amountIn} EURC to USDC: ${ethers.utils.formatUnits(rawQuote, 6)} USDC`);

expect(rawQuote).to.be.gt(0, "Expected a non-zero quote");

await ate.checkSwapMatchesQuery(amountIn, tokenIn, tokenOut);
});
});

it("Query returns zero if tokens not found", async () => {
await ate.checkQueryReturnsZeroForUnsupportedTkns(tkns.EURC);
});

it("Swapping too much returns zero", async () => {
const dy = await ate.Adapter.query(
ethers.utils.parseUnits("10000", 18),
tkns.EURC.address,
tkns.USDC.address
);
expect(dy).to.eq(0);
});

it("Adapter can only spend max-gas + buffer", async () => {
const gasBuffer = ethers.BigNumber.from("70000");
const gasLimit = ethers.BigNumber.from(await ate.Adapter.swapGasEstimate());
const gasUsed = await ate.Adapter.estimateGas.query(
ethers.utils.parseUnits("2000", 6),
tkns.EURC.address,
tkns.USDC.address
);
expect(gasUsed).to.lt(gasLimit.add(gasBuffer));
});

it("Gas-estimate is between max-gas-used and 110% max-gas-used", async () => {
const options = [["10", tkns.EURC, tkns.USDC]];
await ate.checkGasEstimateIsSensible(options);
});
});