chore: Add BalancerSwapExecutor

This commit is contained in:
Diana Carvalho
2024-08-23 17:58:00 +01:00
parent d1e21a8d63
commit e49af99b1f
6 changed files with 259 additions and 9 deletions

View File

@@ -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).

View File

@@ -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);
```

View 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) }
}
}
}

View 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
View 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
);
}

View 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);
}
}