Skip to content

Commit

Permalink
Merge pull request #43 from FastLane-Labs/v4-swap-example
Browse files Browse the repository at this point in the history
V4 swap example
  • Loading branch information
thogard785 authored Dec 6, 2023
2 parents 58c9d1a + 066ae8f commit 395d223
Show file tree
Hide file tree
Showing 8 changed files with 574 additions and 5 deletions.
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,21 @@ jobs:

- name: Check contract sizes
run: |
forge build --sizes && \
forge build && \
for file in out/*.sol/*.json; do
# Skip test files and script files
if [[ "$file" == *".t.sol"* || "$file" == *".s.sol"* ]]; then
continue
fi
# Extract contract name from the file path
contract_name=$(basename $(dirname $file))
# Check if the source file for the contract exists anywhere within src/contracts/
if ! find src/contracts/ -type f -name "${contract_name}.sol" | grep -q .; then
continue
fi
# Calculate the size of the deployed bytecode
size=$(cat "$file" | jq '.deployedBytecode.object' | wc -c | awk '{print int(($1-2)/2)}')
# Check if size exceeds the limit
if [ $size -gt 24576 ]; then
echo "Contract in $file exceeds max size of 24576 bytes (Size: $size bytes)"
exit 1
Expand Down
1 change: 1 addition & 0 deletions lib/v4-core
Submodule v4-core added at ab5239
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ hardhat/=node_modules/hardhat/
openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/
openzeppelin-contracts/=lib/openzeppelin-contracts/
solmate/=lib/solmate/src/
v4-core/=lib/v4-core/contracts
4 changes: 2 additions & 2 deletions src/contracts/atlas/ExecutionEnvironment.sol
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ contract ExecutionEnvironment is Base {

// Handle any solver preOps, if necessary
if (_config().needsPreSolver()) {
bytes memory data = abi.encode(solverOp.solver, dAppReturnData);
bytes memory data = abi.encode(solverOp.solver, solverOp.bidAmount, dAppReturnData);

data = abi.encodeWithSelector(IDAppControl.preSolverCall.selector, data);

Expand Down Expand Up @@ -223,7 +223,7 @@ contract ExecutionEnvironment is Base {
if (_config().needsSolverPostCall()) {
bytes memory data = dAppReturnData;

data = abi.encode(solverOp.solver, data);
data = abi.encode(solverOp.solver, solverOp.bidAmount, data);

data = abi.encodeWithSelector(IDAppControl.postSolverCall.selector, data);

Expand Down
1 change: 1 addition & 0 deletions src/contracts/common/ExecutionBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ contract ExecutionBase is Base {

if (source == user) {
if (shiftedPhase & SAFE_USER_TRANSFER == 0) {
address(0).staticcall("");
return false;
}
if (ERC20(token).allowance(user, atlas) < amount) {
Expand Down
4 changes: 2 additions & 2 deletions src/contracts/examples/intents-example/SwapIntent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ contract SwapIntentController is DAppControl {
//////////////////////////////////

function _preSolverCall(bytes calldata data) internal override returns (bool) {
(address solverTo, bytes memory returnData) = abi.decode(data, (address, bytes));
(address solverTo,, bytes memory returnData) = abi.decode(data, (address, uint256, bytes));
if (solverTo == address(this) || solverTo == _control() || solverTo == escrow) {
return false;
}
Expand All @@ -184,7 +184,7 @@ contract SwapIntentController is DAppControl {

// Checking intent was fulfilled, and user has received their tokens, happens here
function _postSolverCall(bytes calldata data) internal override returns (bool) {
(, bytes memory returnData) = abi.decode(data, (address, bytes));
(,, bytes memory returnData) = abi.decode(data, (address, uint256, bytes));

SwapData memory swapData = abi.decode(returnData, (SwapData));

Expand Down
267 changes: 267 additions & 0 deletions src/contracts/examples/intents-example/V4SwapIntent.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
//SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.16;

// Base Imports
import { SafeTransferLib, ERC20 } from "solmate/utils/SafeTransferLib.sol";

// Atlas Base Imports
import { IEscrow } from "../../interfaces/IEscrow.sol";

import { CallConfig } from "../../types/DAppApprovalTypes.sol";
import "../../types/UserCallTypes.sol";
import "../../types/SolverCallTypes.sol";
import "../../types/LockTypes.sol";

// Atlas DApp-Control Imports
import { DAppControl } from "../../dapp/DAppControl.sol";

import "forge-std/Test.sol";

// This struct is for passing around data internally
struct SwapData {
address tokenIn;
address tokenOut;
int256 requestedAmount; // positive for exact in, negative for exact out
uint256 limitAmount; // if exact in, min amount out. if exact out, max amount in
address recipient;
}

contract V4SwapIntentController is DAppControl {
using SafeTransferLib for ERC20;

address immutable V4_POOL; // TODO: set for test v4 pool

uint256 startingBalance; // Balance tracked for the v4 pool

constructor(
address _escrow,
address poolManager
)
DAppControl(
_escrow,
msg.sender,
CallConfig({
sequenced: false,
requirePreOps: false,
trackPreOpsReturnData: false,
trackUserReturnData: true,
localUser: false,
delegateUser: true,
preSolver: true,
postSolver: true,
requirePostOps: false,
zeroSolvers: false,
reuseUserOp: true,
userBundler: true,
solverBundler: true,
verifySolverBundlerCallChainHash: true,
unknownBundler: true,
forwardReturnData: false,
requireFulfillment: true
})
)
{
V4_POOL = poolManager;
}

//////////////////////////////////
// CONTRACT-SPECIFIC FUNCTIONS //
//////////////////////////////////

modifier verifyCall(address tokenIn, address tokenOut, uint256 amount) {
require(msg.sender == escrow, "ERR-PI002 InvalidSender");
require(_approvedCaller() == control, "ERR-PI003 InvalidLockState");
require(address(this) != control, "ERR-PI004 MustBeDelegated");

address user = _user();

// TODO: Could maintain a balance of "1" of each token to allow the user to save gas over multiple uses
uint256 tokenInBalance = ERC20(tokenIn).balanceOf(address(this));
if (tokenInBalance > 0) {
ERC20(tokenIn).safeTransfer(user, tokenInBalance);
}

uint256 tokenOutBalance = ERC20(tokenOut).balanceOf(address(this));
if (tokenOutBalance > 0) {
ERC20(tokenOut).safeTransfer(user, tokenOutBalance);
}

require(_availableFundsERC20(tokenIn, user, amount, ExecutionPhase.PreSolver), "ERR-PI059 SellFundsUnavailable");
_;
}

struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint256 maxFee;
address recipient;
uint256 amountIn;
uint256 amountOutMinimum;
uint256 sqrtPriceLimitX96;
}

// selector 0x04e45aaf
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
verifyCall(params.tokenIn, params.tokenOut, params.amountIn)
returns (SwapData memory)
{
SwapData memory swapData = SwapData({
tokenIn: params.tokenIn,
tokenOut: params.tokenOut,
requestedAmount: int256(params.amountIn),
limitAmount: params.amountOutMinimum,
recipient: params.recipient
});

return swapData;
}

struct ExactOutputSingleParams {
address tokenIn;
address tokenOut;
uint256 maxFee;
address recipient;
uint256 amountInMaximum;
uint256 amountOut;
uint256 sqrtPriceLimitX96;
}

// selector 0x5023b4df
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
verifyCall(params.tokenIn, params.tokenOut, params.amountInMaximum)
returns (SwapData memory)
{
SwapData memory swapData = SwapData({
tokenIn: params.tokenIn,
tokenOut: params.tokenOut,
requestedAmount: -int256(params.amountOut),
limitAmount: params.amountInMaximum,
recipient: params.recipient
});

return swapData;
}

//////////////////////////////////
// ATLAS OVERRIDE FUNCTIONS //
//////////////////////////////////

function _preSolverCall(bytes calldata data) internal override returns (bool) {
(address solverTo, uint256 solverBid, bytes memory returnData) = abi.decode(data, (address, uint256, bytes));
if (solverTo == address(this) || solverTo == _control() || solverTo == escrow) {
return false;
}

SwapData memory swapData = abi.decode(returnData, (SwapData));

// Record balance and transfer to the solver
if (swapData.requestedAmount > 0) {
// exact input
startingBalance = ERC20(swapData.tokenIn).balanceOf(V4_POOL);
_transferUserERC20(swapData.tokenIn, solverTo, uint256(swapData.requestedAmount));
} else {
// exact output
startingBalance = ERC20(swapData.tokenOut).balanceOf(V4_POOL);
_transferUserERC20(swapData.tokenIn, solverTo, swapData.limitAmount - solverBid);
// For exact output swaps, the solver solvers compete and bid on how much tokens they can
// return to the user in excess of their specified limit input. We only transfer what they
// require to make the swap in this step.
}

// TODO: Permit69 is currently enabled during solver phase, but there is low conviction that this
// does not enable an attack vector. Consider enabling to save gas on a transfer?
return true;
}

// Checking intent was fulfilled, and user has received their tokens, happens here
function _postSolverCall(bytes calldata data) internal override returns (bool) {
(, uint256 solverBid, bytes memory returnData) = abi.decode(data, (address, uint256, bytes));

SwapData memory swapData = abi.decode(returnData, (SwapData));

uint256 buyTokenBalance = ERC20(swapData.tokenOut).balanceOf(address(this));
uint256 amountUserBuys =
swapData.requestedAmount > 0 ? swapData.limitAmount : uint256(-swapData.requestedAmount);

// If it was an exact input swap, we need to verify that
// a) We have enough tokens to meet the user's minimum amount out
// b) The output amount matches (or is greater than) the solver's bid
// c) PoolManager's balances increased by the provided input amount
if (swapData.requestedAmount > 0) {
if (buyTokenBalance < swapData.limitAmount) {
return false; // insufficient amount out
}
if (buyTokenBalance < solverBid) {
return false; // does not meet solver bid
}
uint256 endingBalance = ERC20(swapData.tokenIn).balanceOf(V4_POOL);
if ((endingBalance - startingBalance) < uint256(swapData.requestedAmount)) {
return false; // pool manager balances did not increase by the provided input amount
}
} else {
// Exact output swap - check the output amount was transferred out by pool
uint256 endingBalance = ERC20(swapData.tokenOut).balanceOf(V4_POOL);
if ((startingBalance - endingBalance) < amountUserBuys) {
return false; // pool manager balances did not decrease by the provided output amount
}
}
// no need to check for exact output, since the max is whatever the user transferred

if (buyTokenBalance >= amountUserBuys) {
// Make sure not to transfer any extra 'auctionBaseCurrency' token, since that will be used
// for the auction measurements
address auctionBaseCurrency = swapData.requestedAmount > 0 ? swapData.tokenOut : swapData.tokenIn;

if (swapData.tokenOut != auctionBaseCurrency) {
ERC20(swapData.tokenOut).safeTransfer(swapData.recipient, buyTokenBalance);
} else {
ERC20(swapData.tokenOut).safeTransfer(swapData.recipient, amountUserBuys);
}
return true;
} else {
return false;
}
}

// This occurs after a Solver has successfully paid their bid, which is
// held in ExecutionEnvironment.
function _allocateValueCall(address bidToken, uint256 bidAmount, bytes calldata) internal override {
// This function is delegatecalled
// address(this) = ExecutionEnvironment
// msg.sender = Escrow
if (bidToken != address(0)) {
ERC20(bidToken).safeTransfer(_user(), bidAmount);
} else {
SafeTransferLib.safeTransferETH(_user(), address(this).balance);
}
}

/////////////////////////////////////////////////////////
///////////////// GETTERS & HELPERS // //////////////////
/////////////////////////////////////////////////////////
// NOTE: These are not delegatecalled

function getBidFormat(UserOperation calldata userOp) public pure override returns (address bidToken) {
// This is a helper function called by solvers
// so that they can get the proper format for
// submitting their bids to the hook.

if (bytes4(userOp.data[:4]) == this.exactInputSingle.selector) {
// exact input swap, the bidding is done in output token
(, bidToken) = abi.decode(userOp.data[4:], (address, address));
} else if (bytes4(userOp.data[:4]) == this.exactOutputSingle.selector) {
// exact output, bidding done in input token
bidToken = abi.decode(userOp.data[4:], (address));
}

// should we return an error here if the function is wrong?
}

function getBidValue(SolverOperation calldata solverOp) public pure override returns (uint256) {
return solverOp.bidAmount;
}
}
Loading

0 comments on commit 395d223

Please sign in to comment.