chore: Add BalancerSwapExecutor
This commit is contained in:
132
evm/src/balancer-v2/BalancerSwapExecutor.sol
Normal file
132
evm/src/balancer-v2/BalancerSwapExecutor.sol
Normal file
@@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: UNLICENCED
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../interfaces/ISwapExecutor.sol";
|
||||
|
||||
contract BalancerSwapExecutor is ISwapExecutor {
|
||||
address private constant vaultAddress =
|
||||
0xBA12222222228d8Ba445958a75a0704d566BF2C8;
|
||||
bytes32 private constant swapSelector =
|
||||
0x52bbbe2900000000000000000000000000000000000000000000000000000000;
|
||||
bytes32 private constant maxUint256 =
|
||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF;
|
||||
|
||||
/**
|
||||
* @dev Executes a Balancer swap.
|
||||
* @param givenAmount how much of to swap, depending on exactOut either in-
|
||||
* or outAmount.
|
||||
* @param data the parameters of the swap. This data is roughly the packed
|
||||
* encoding of
|
||||
* the struct below:
|
||||
* ```
|
||||
* struct Params {
|
||||
* // the token that the caller is selling
|
||||
* IERC20 tokenIn;
|
||||
*
|
||||
* // the token that the caller is receiving in exchange
|
||||
* IERC20 tokenOut;
|
||||
*
|
||||
* // the target pool id
|
||||
* bytes32 poolId;
|
||||
*
|
||||
* // the receiver of `tokenOut`
|
||||
* address receiver;
|
||||
*
|
||||
* // whether we want exactOut semantics
|
||||
* bool exactOut;
|
||||
*
|
||||
* // whether we need to approve the pool to spend `tokenIn`
|
||||
* bool tokenApprovalNeeded;
|
||||
*
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
function swap(uint256 givenAmount, bytes calldata data)
|
||||
external
|
||||
payable
|
||||
returns (uint256 calculatedAmount)
|
||||
{
|
||||
IERC20 tokenIn;
|
||||
IERC20 tokenOut;
|
||||
bytes32 poolId;
|
||||
address receiver;
|
||||
bool exactOut;
|
||||
bool tokenApprovalNeeded;
|
||||
assembly {
|
||||
tokenIn := shr(96, calldataload(data.offset))
|
||||
tokenOut := shr(96, calldataload(add(data.offset, 20)))
|
||||
poolId := calldataload(add(data.offset, 40))
|
||||
let dataLoad := calldataload(add(data.offset, 72))
|
||||
receiver := shr(96, dataLoad)
|
||||
exactOut := and(shr(88, dataLoad), 0xff)
|
||||
tokenApprovalNeeded := and(shr(80, dataLoad), 0xff)
|
||||
|
||||
// Check if token approval is needed and perform the approval
|
||||
if tokenApprovalNeeded {
|
||||
// Prepare approve call
|
||||
let approveCalldata := mload(0x40)
|
||||
mstore(
|
||||
approveCalldata,
|
||||
0x095ea7b300000000000000000000000000000000000000000000000000000000
|
||||
) // approve selector
|
||||
mstore(add(approveCalldata, 4), vaultAddress) // spender
|
||||
mstore(add(approveCalldata, 36), maxUint256) // value
|
||||
// (maxUint256)
|
||||
|
||||
let success :=
|
||||
call(gas(), tokenIn, 0, approveCalldata, 68, 0, 0)
|
||||
if iszero(success) {
|
||||
returndatacopy(0, 0, returndatasize())
|
||||
revert(0, returndatasize())
|
||||
}
|
||||
}
|
||||
|
||||
let ptr := mload(0x40)
|
||||
mstore(ptr, swapSelector)
|
||||
//limit: as it is always recalculated during the swap, we use the
|
||||
// extremums 0 tokenOut or max(uint256) tokenIn.
|
||||
let limit := 0
|
||||
if exactOut { limit := maxUint256 }
|
||||
// as the singleSwap struct contains a bytes, it's considered as
|
||||
// dynamic.
|
||||
// dynamic values are encoded at the end of the calldata and have a
|
||||
// corresponding offset at the beginning
|
||||
// we first need to encode the offset of the singleSwap struct
|
||||
mstore(add(ptr, 4), 0xe0)
|
||||
// fundManagement.sender: is always address(this)
|
||||
mstore(add(ptr, 36), address())
|
||||
// fundManagement.fromInternalBalance
|
||||
mstore(add(ptr, 68), 0)
|
||||
// fundManagement.receiver
|
||||
mstore(add(ptr, 100), receiver)
|
||||
// fundManagement.toInternalBalance
|
||||
mstore(add(ptr, 132), 0)
|
||||
// limit
|
||||
mstore(add(ptr, 164), limit)
|
||||
// deadline
|
||||
mstore(add(ptr, 196), timestamp())
|
||||
// singleSwap.poolId
|
||||
mstore(add(ptr, 228), poolId)
|
||||
// singleSwap.exactOut
|
||||
mstore(add(ptr, 260), exactOut)
|
||||
// singleSwap.assetIn
|
||||
mstore(add(ptr, 292), tokenIn)
|
||||
// singleSwap.assetOut
|
||||
mstore(add(ptr, 324), tokenOut)
|
||||
// singleSwap.amount
|
||||
mstore(add(ptr, 356), givenAmount)
|
||||
// singleSwap.userData offset
|
||||
mstore(add(ptr, 388), 0xc0)
|
||||
// singleSwap.userData lenght
|
||||
mstore(add(ptr, 420), 0)
|
||||
|
||||
let success := call(gas(), vaultAddress, 0, ptr, 452, ptr, 32)
|
||||
switch success
|
||||
case 0 {
|
||||
returndatacopy(0, 0, returndatasize())
|
||||
revert(0, returndatasize())
|
||||
}
|
||||
default { calculatedAmount := mload(ptr) }
|
||||
}
|
||||
}
|
||||
}
|
||||
81
evm/test/BalancerSwapExecutor.t.sol
Normal file
81
evm/test/BalancerSwapExecutor.t.sol
Normal file
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "./SwapExecutorTest.sol";
|
||||
import "../src/balancer-v2/BalancerSwapExecutor.sol";
|
||||
|
||||
contract TestBalancerSwapExecutor is SwapExecutorTest {
|
||||
BalancerSwapExecutor balancer;
|
||||
IERC20 USDC = IERC20(USDC_ADDR);
|
||||
IERC20 USDT = IERC20(USDT_ADDR);
|
||||
|
||||
constructor() {}
|
||||
|
||||
function setUp() public {
|
||||
//Fork
|
||||
uint256 forkBlock = 16000000;
|
||||
vm.createSelectFork(vm.rpcUrl("mainnet"), forkBlock);
|
||||
|
||||
//Setup
|
||||
balancer = new BalancerSwapExecutor();
|
||||
}
|
||||
|
||||
function testBalancerSwap() public {
|
||||
//Set up
|
||||
uint256 sellAmount = 1000_000000;
|
||||
uint256 expectedAmount = 998_919380; //Swap 1k USDT for 998 USDC
|
||||
bool exactOut = false;
|
||||
// This is required because balancer does a transferFrom sender.
|
||||
// That also means we need to do this approval with our swapRouter.
|
||||
bool tokenApprovalNeeded = true;
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
USDT_ADDR,
|
||||
USDC_ADDR,
|
||||
DAI_USDC_USDT_balancer,
|
||||
bob,
|
||||
exactOut,
|
||||
tokenApprovalNeeded
|
||||
);
|
||||
|
||||
// Logic
|
||||
vm.prank(address(balancer));
|
||||
deal(USDT_ADDR, address(balancer), sellAmount);
|
||||
vm.prank(executor);
|
||||
uint256 responseAmount = balancer.swap(sellAmount, protocolData);
|
||||
|
||||
//Assertions
|
||||
assertEq(responseAmount, expectedAmount);
|
||||
assertEq(USDC.balanceOf(bob), expectedAmount);
|
||||
assertEq(USDT.balanceOf(address(balancer)), 0);
|
||||
}
|
||||
|
||||
function testBalancerExactOutSwap() public {
|
||||
//Set up
|
||||
uint256 buyAmount = 1000_979168;
|
||||
uint256 expectedSellAmount = 1000 * 10 ** 6;
|
||||
bool exactOut = true;
|
||||
bool tokenApprovalNeeded = true;
|
||||
bytes memory protocolData = abi.encodePacked(
|
||||
USDC_ADDR,
|
||||
USDT_ADDR,
|
||||
DAI_USDC_USDT_balancer,
|
||||
bob,
|
||||
exactOut,
|
||||
tokenApprovalNeeded
|
||||
);
|
||||
|
||||
//Logic
|
||||
// This is required because balancer does a transferFrom sender.
|
||||
// That also means we need to do this approval with our swapRouter.
|
||||
vm.prank(address(balancer));
|
||||
|
||||
deal(USDC_ADDR, address(balancer), expectedSellAmount);
|
||||
vm.prank(executor);
|
||||
uint256 responseAmount = balancer.swap(buyAmount, protocolData);
|
||||
|
||||
// //Assertions
|
||||
assertEq(responseAmount, expectedSellAmount);
|
||||
assertEq(USDT.balanceOf(bob), buyAmount);
|
||||
assertEq(USDC.balanceOf(address(balancer)), 0);
|
||||
}
|
||||
}
|
||||
23
evm/test/Constants.sol
Normal file
23
evm/test/Constants.sol
Normal file
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
contract Constants {
|
||||
address executor = address(1239501235123412541234); //executor=us || cowswap
|
||||
address admin = address(12395012351212343412541234); //admin=us
|
||||
address bob = address(123); //bob=someone!=us
|
||||
|
||||
// tokens
|
||||
address WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
|
||||
address USDT_ADDR = 0xdAC17F958D2ee523a2206206994597C13D831ec7;
|
||||
address USDC_ADDR = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
|
||||
address DAI_ADDR = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
|
||||
address WBTC_ADDR = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
|
||||
address LDO_ADDR = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32;
|
||||
address CRV_ADDR = 0xD533a949740bb3306d119CC777fa900bA034cd52;
|
||||
|
||||
// balancer
|
||||
address balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
|
||||
bytes32 DAI_USDC_USDT_balancer = bytes32(
|
||||
0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063
|
||||
);
|
||||
}
|
||||
18
evm/test/SwapExecutorTest.sol
Normal file
18
evm/test/SwapExecutorTest.sol
Normal file
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: UNLICENSED
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "forge-std/Test.sol";
|
||||
import "openzeppelin-contracts/contracts/interfaces/IERC20.sol";
|
||||
import "./Constants.sol";
|
||||
|
||||
contract SwapExecutorTest is Test, Constants {
|
||||
function twoTokens(address token0, address token1)
|
||||
internal
|
||||
pure
|
||||
returns (IERC20[] memory tokens)
|
||||
{
|
||||
tokens = new IERC20[](2);
|
||||
tokens[0] = IERC20(token0);
|
||||
tokens[1] = IERC20(token1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user