chore: Add BalancerSwapExecutor
This commit is contained in:
@@ -28,7 +28,8 @@ errors, such as invalid parameter lengths or unknown pool types in the swap logi
|
||||
manage these approvals within the implementation to ensure smooth execution of the swap.
|
||||
5. **Token Transfer Support**: Ensure that the implementation supports transferring received tokens to a designated
|
||||
receiver address, either within the swap function or through an additional transfer step.
|
||||
6. **Gas Efficiency**: Ensure the implementation is gas-efficient. Strive for optimal performance in the swap logic.
|
||||
6. **Gas Efficiency**: Ensure the implementation is gas-efficient. Strive for optimal performance in the swap logic.
|
||||
The usage of assembly is not necessary.
|
||||
7. **Security Considerations**: Follow common security best practices, such as validating inputs, ensuring proper
|
||||
access control, and safeguarding against reentrancy attacks.
|
||||
|
||||
@@ -36,4 +37,4 @@ access control, and safeguarding against reentrancy attacks.
|
||||
|
||||
## Example Implementation
|
||||
|
||||
See the example implementation of a `SwapExecutor` for Balancer here (TODO: paste link)
|
||||
See the example implementation of a `SwapExecutor` for Balancer [here](../../evm/src/balancer-v2/BalancerSwapExecutor.sol).
|
||||
@@ -6,10 +6,10 @@ description: Provide protocol logic using the ethereum virtual machine
|
||||
|
||||
## 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" %}
|
||||
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 adapter 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 %}
|
||||
|
||||
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.
|
||||
@@ -147,8 +147,3 @@ function getPoolIds(uint256 offset, uint256 limit)
|
||||
external
|
||||
returns (bytes32[] memory ids);
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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