Skip to content

Commit

Permalink
✨ swapper: swap ETH for EXA on velodrome
Browse files Browse the repository at this point in the history
Co-authored-by: Santiago Sanchez Avalos <santisanchezavalos@gmail.com>
  • Loading branch information
cruzdanilo and santichez committed Jul 26, 2023
1 parent 1189ce5 commit f5eadf5
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/eleven-dots-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

✨ swapper: swap `ETH` for `EXA` on velodrome
7 changes: 6 additions & 1 deletion .gas-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,9 @@ RewardsControllerTest:testUpdateWithTotalDebtZeroShouldUpdateLastUndistributed()
RewardsControllerTest:testUtilizationEqualZero() (gas: 622308)
RewardsControllerTest:testWithTwelveFixedPools() (gas: 3989456)
RewardsControllerTest:testWithdrawAllRewardBalance() (gas: 47146)
RewardsControllerTest:testWithdrawOnlyAdminRole() (gas: 84719)
RewardsControllerTest:testWithdrawOnlyAdminRole() (gas: 84719)
SwapperTest:testSwapBasic() (gas: 187376)
SwapperTest:testSwapWithInaccurateSlippageSendsETHToAccount() (gas: 207789)
SwapperTest:testSwapWithKeepAmount() (gas: 194032)
SwapperTest:testSwapWithKeepEqualToValue() (gas: 79362)
SwapperTest:testSwapWithKeepHigherThanValue() (gas: 34589)
57 changes: 57 additions & 0 deletions contracts/periphery/Swapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { WETH, ERC20 } from "solmate/src/tokens/WETH.sol";
import { SafeTransferLib } from "solmate/src/utils/SafeTransferLib.sol";

contract Swapper {
using SafeTransferLib for address payable;
using SafeTransferLib for WETH;

/// @notice The EXA asset.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ERC20 public immutable exa;
/// @notice The WETH asset.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
WETH public immutable weth;
/// @notice The liquidity pool.
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
IPool public immutable pool;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor(ERC20 exa_, WETH weth_, IPool pool_) {
exa = exa_;
weth = weth_;
pool = pool_;
}

/// @notice Swaps `msg.value` ETH for EXA and sends it to `account`.
/// @param account The account to send the EXA to.
/// @param minEXA The minimum amount of EXA to receive.
/// @param keepETH The amount of ETH to send to `account` (ex: for gas).
function swap(address payable account, uint256 minEXA, uint256 keepETH) external payable {
if (keepETH > msg.value) return account.safeTransferETH(msg.value);

uint256 inETH = msg.value - keepETH;
uint256 outEXA = pool.getAmountOut(inETH, weth);
if (outEXA < minEXA) return account.safeTransferETH(msg.value);

weth.deposit{ value: inETH }();
weth.safeTransfer(address(pool), inETH);

(uint256 amount0Out, uint256 amount1Out) = address(exa) < address(weth)
? (outEXA, uint256(0))
: (uint256(0), outEXA);
try pool.swap(amount0Out, amount1Out, account, "") {
account.safeTransferETH(keepETH);
} catch {
account.safeTransferETH(msg.value);
}
}
}

interface IPool {
function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external;

function getAmountOut(uint256 amountIn, WETH tokenIn) external view returns (uint256);
}
81 changes: 81 additions & 0 deletions test/solidity/Swapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.17;

import { ForkTest } from "./Fork.t.sol";
import { FixedPointMathLib } from "solmate/src/utils/FixedPointMathLib.sol";
import { Swapper, IPool, ERC20, WETH } from "../../contracts/periphery/Swapper.sol";

contract SwapperTest is ForkTest {
using FixedPointMathLib for uint256;

ERC20 internal exa;
WETH internal weth;
IPool internal pool;
Swapper internal swapper;

function setUp() external {
vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 107_385_046);

exa = ERC20(deployment("EXA"));
weth = WETH(payable(deployment("WETH")));
pool = IPool(deployment("EXAPool"));
swapper = new Swapper(exa, weth, pool);

deal(address(weth), address(this), 500 ether);
}

function testSwapBasic() external _checkBalance {
uint256 balanceETH = address(this).balance;
uint256 amountEXA = pool.getAmountOut(1 ether, weth);
swapper.swap{ value: 1 ether }(payable(address(this)), 0, 0);

assertEq(address(this).balance, balanceETH - 1 ether, "eth spent");
assertEq(exa.balanceOf(address(this)), amountEXA, "exa received");
}

function testSwapWithKeepAmount() external _checkBalance {
uint256 balanceETH = address(this).balance;
uint256 amountEXA = pool.getAmountOut(0.9 ether, weth);
swapper.swap{ value: 1 ether }(payable(address(this)), 0, 0.1 ether);

assertEq(address(this).balance, balanceETH - 0.9 ether, "eth spent");
assertEq(exa.balanceOf(address(this)), amountEXA, "exa received");
}

function testSwapWithKeepEqualToValue() external _checkBalance {
uint256 balanceETH = address(this).balance;
swapper.swap{ value: 1 ether }(payable(address(this)), 0, 1 ether);

assertEq(address(this).balance, balanceETH, "eth spent");
assertEq(exa.balanceOf(address(this)), 0, "exa received");
}

function testSwapWithKeepHigherThanValue() external _checkBalance {
uint256 balanceETH = address(this).balance;
swapper.swap{ value: 1 ether }(payable(address(this)), 0, 2 ether);

assertEq(address(this).balance, balanceETH, "eth spent");
assertEq(exa.balanceOf(address(this)), 0, "exa received");
}

function testSwapWithInaccurateSlippageSendsETHToAccount() external _checkBalance {
uint256 balanceETH = address(this).balance;
uint256 amountEXA = pool.getAmountOut(1 ether, weth);

swapper.swap{ value: 1 ether }(payable(address(this)), amountEXA * 5, 0);
assertEq(address(this).balance, balanceETH, "eth spent");
assertEq(exa.balanceOf(address(this)), 0, "exa received");

swapper.swap{ value: 1 ether }(payable(address(this)), amountEXA - 10e18, 0);
assertEq(address(this).balance, balanceETH - 1 ether, "eth spent");
assertEq(exa.balanceOf(address(this)), amountEXA, "exa received");
}

modifier _checkBalance() {
_;
assertEq(address(swapper).balance, 0);
}

// solhint-disable-next-line no-empty-blocks
receive() external payable {}
}

0 comments on commit f5eadf5

Please sign in to comment.