Skip to content

Commit

Permalink
Merge branch 'main' into change-mev-hook-permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
elshan-eth committed Feb 18, 2025
2 parents 89eda7b + f7a734c commit 4cb24ed
Show file tree
Hide file tree
Showing 15 changed files with 702 additions and 98 deletions.
46 changes: 46 additions & 0 deletions pkg/interfaces/contracts/vault/IAggregatorRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SwapKind } from "./VaultTypes.sol";

interface IAggregatorRouter {
/// @notice Thrown when the sender does not transfer the correct amount of tokens to the Vault.
error SwapInsufficientPayment();

/**
* @notice Executes a swap operation specifying an exact input token amount.
* @param pool Address of the liquidity pool
Expand All @@ -28,6 +31,30 @@ interface IAggregatorRouter {
bytes calldata userData
) external returns (uint256 amountOut);

/**
* @notice Executes a swap operation specifying an exact output token amount.
* @dev The sender should transfer the maxAmountIn to the Vault before calling this function, and the router will
* transfer any leftovers back to the sender after the swap is calculated.
*
* @param pool Address of the liquidity pool
* @param tokenIn Token to be swapped from
* @param tokenOut Token to be swapped to
* @param exactAmountOut Exact amounts of output tokens to receive
* @param maxAmountIn Maximum amount of input tokens to be sent
* @param deadline Deadline for the swap, after which it will revert
* @param userData Additional (optional) data sent with the swap request
* @return amountIn Calculated amount of input tokens to be sent in exchange for the given output tokens
*/
function swapSingleTokenExactOut(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountOut,
uint256 maxAmountIn,
uint256 deadline,
bytes calldata userData
) external returns (uint256 amountIn);

/**
* @notice Queries a swap operation specifying an exact input token amount without actually executing it.
* @param pool Address of the liquidity pool
Expand All @@ -46,4 +73,23 @@ interface IAggregatorRouter {
address sender,
bytes calldata userData
) external returns (uint256 amountOut);

/**
* @notice Queries a swap operation specifying an exact output token amount without actually executing it.
* @param pool Address of the liquidity pool
* @param tokenIn Token to be swapped from
* @param tokenOut Token to be swapped to
* @param exactAmountOut Exact amounts of output tokens to receive
* @param sender The sender passed to the operation. It can influence results (e.g., with user-dependent hooks)
* @param userData Additional (optional) data sent with the query request
* @return amountIn Calculated amount of input tokens to be sent in exchange for the given output tokens
*/
function querySwapSingleTokenExactOut(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountOut,
address sender,
bytes calldata userData
) external returns (uint256 amountIn);
}
16 changes: 4 additions & 12 deletions pkg/pool-hooks/contracts/MinimalRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { IWETH } from "@balancer-labs/v3-interfaces/contracts/solidity-utils/mis
import { IVault } from "@balancer-labs/v3-interfaces/contracts/vault/IVault.sol";
import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";

import { RouterWethLib } from "@balancer-labs/v3-vault/contracts/lib/RouterWethLib.sol";
import { RouterCommon } from "@balancer-labs/v3-vault/contracts/RouterCommon.sol";

abstract contract MinimalRouter is RouterCommon {
using Address for address payable;
using RouterWethLib for IWETH;
using SafeCast for *;

/**
Expand Down Expand Up @@ -145,13 +147,7 @@ abstract contract MinimalRouter is RouterCommon {

// There can be only one WETH token in the pool.
if (params.wethIsEth && address(token) == address(_weth)) {
if (address(this).balance < amountIn) {
revert InsufficientEth();
}

_weth.deposit{ value: amountIn }();
_weth.transfer(address(_vault), amountIn);
_vault.settle(_weth, amountIn);
_weth.wrapEthAndSettle(_vault, amountIn);
} else {
// Any value over MAX_UINT128 would revert above in `addLiquidity`, so this SafeCast shouldn't be
// necessary. Done out of an abundance of caution.
Expand Down Expand Up @@ -238,11 +234,7 @@ abstract contract MinimalRouter is RouterCommon {

// There can be only one WETH token in the pool.
if (params.wethIsEth && address(token) == address(_weth)) {
// Send WETH here and unwrap to native ETH.
_vault.sendTo(_weth, address(this), amountOut);
_weth.withdraw(amountOut);
// Send ETH to receiver.
payable(params.receiver).sendValue(amountOut);
_weth.unwrapWethAndTransferToSender(_vault, params.receiver, amountOut);
} else {
// Transfer the token to the receiver (amountOut).
_vault.sendTo(token, params.receiver, amountOut);
Expand Down
41 changes: 27 additions & 14 deletions pkg/pool-stable/contracts/StablePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@ contract StablePool is IStablePool, BalancerPoolToken, BasePoolAuthentication, P
using FixedPoint for uint256;
using SafeCast for *;

/**
* @notice Parameters used to deploy a new Stable Pool.
* @param name ERC20 token name
* @param symbol ERC20 token symbol
* @param amplificationParameter Controls the "flatness" of the invariant curve. higher values = lower slippage,
* and assumes prices are near parity. lower values = closer to the constant product curve (e.g., more like a
* weighted pool). This has higher slippage, and accommodates greater price volatility
* @param version The stable pool version
*/
struct NewPoolParams {
string name;
string symbol;
uint256 amplificationParameter;
string version;
}

// This contract uses timestamps to slowly update its Amplification parameter over time. These changes must occur
// over a minimum time period much larger than the block time, making timestamp manipulation a non-issue.
// solhint-disable not-rely-on-time
Expand Down Expand Up @@ -101,19 +117,16 @@ contract StablePool is IStablePool, BalancerPoolToken, BasePoolAuthentication, P
error AmpUpdateNotStarted();

/**
* @notice Parameters used to deploy a new Stable Pool.
* @param name ERC20 token name
* @param symbol ERC20 token symbol
* @param amplificationParameter Controls the "flatness" of the invariant curve. higher values = lower slippage,
* and assumes prices are near parity. lower values = closer to the constant product curve (e.g., more like a
* weighted pool). This has higher slippage, and accommodates greater price volatility
* @param version The stable pool version
* @notice Allow the swap manager to change the amplification parameter.
* @dev Unlike the swap fee percentage setting permission, this is non-exclusive.
*/
struct NewPoolParams {
string name;
string symbol;
uint256 amplificationParameter;
string version;
modifier authenticateByRole() {
// Allow if this is the swapFeeManager.
if (msg.sender != _vault.getPoolRoleAccounts(address(this)).swapFeeManager) {
// Otherwise, defer to governance.
_authenticateCaller();
}
_;
}

constructor(
Expand Down Expand Up @@ -196,7 +209,7 @@ contract StablePool is IStablePool, BalancerPoolToken, BasePoolAuthentication, P
}

/// @inheritdoc IStablePool
function startAmplificationParameterUpdate(uint256 rawEndValue, uint256 endTime) external authenticate {
function startAmplificationParameterUpdate(uint256 rawEndValue, uint256 endTime) external authenticateByRole {
if (rawEndValue < StableMath.MIN_AMP) {
revert AmplificationFactorTooLow();
}
Expand Down Expand Up @@ -246,7 +259,7 @@ contract StablePool is IStablePool, BalancerPoolToken, BasePoolAuthentication, P
}

/// @inheritdoc IStablePool
function stopAmplificationParameterUpdate() external authenticate {
function stopAmplificationParameterUpdate() external authenticateByRole {
(uint256 currentValue, bool isUpdating) = _getAmplificationParameter();

if (isUpdating == false) {
Expand Down
56 changes: 55 additions & 1 deletion pkg/pool-stable/test/foundry/StablePool.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ contract StablePoolTest is BasePoolTest, StablePoolContractsDeployer {
}

PoolRoleAccounts memory roleAccounts;
roleAccounts.swapFeeManager = alice;

// Allow pools created by `factory` to use poolHooksMock hooks
PoolHooksMock(poolHooksContract).allowFactory(poolFactory);

Expand Down Expand Up @@ -118,6 +120,58 @@ contract StablePoolTest is BasePoolTest, StablePoolContractsDeployer {
_testGetBptRate(invariantBefore, invariantAfter, amountsIn);
}

function testAmplificationUpdateByRole() public {
// Ensure the swap manager was set for the pool.
assertEq(vault.getPoolRoleAccounts(pool).swapFeeManager, alice, "Wrong swap fee manager");

// Ensure the swap manager doesn't have permission through governance.
assertFalse(
authorizer.hasRole(
IAuthentication(pool).getActionId(StablePool.startAmplificationParameterUpdate.selector),
alice
),
"Has governance-granted start permission"
);
assertFalse(
authorizer.hasRole(
IAuthentication(pool).getActionId(StablePool.stopAmplificationParameterUpdate.selector),
alice
),
"Has governance-granted stop permission"
);

// Ensure the swap manager account can start/stop anyway.
uint256 currentTime = block.timestamp;
uint256 updateInterval = 5000 days;

uint256 endTime = currentTime + updateInterval;
uint256 newAmplificationParameter = DEFAULT_AMP_FACTOR * 2;

vm.startPrank(alice);
IStablePool(pool).startAmplificationParameterUpdate(newAmplificationParameter, endTime);

(, bool isUpdating, ) = IStablePool(pool).getAmplificationParameter();
assertTrue(isUpdating, "Amplification update not started");

IStablePool(pool).stopAmplificationParameterUpdate();
vm.stopPrank();

(, isUpdating, ) = IStablePool(pool).getAmplificationParameter();
assertFalse(isUpdating, "Amplification update not stopped");

// Grant to Bob via governance.
authorizer.grantRole(
IAuthentication(pool).getActionId(StablePool.startAmplificationParameterUpdate.selector),
bob
);

vm.startPrank(bob);
IStablePool(pool).startAmplificationParameterUpdate(newAmplificationParameter, endTime);

(, isUpdating, ) = IStablePool(pool).getAmplificationParameter();
assertTrue(isUpdating, "Amplification update by Bob not started");
}

function testGetAmplificationState() public {
(AmplificationState memory ampState, uint256 precision) = IStablePool(pool).getAmplificationState();

Expand All @@ -139,7 +193,7 @@ contract StablePoolTest is BasePoolTest, StablePoolContractsDeployer {
uint256 newAmplificationParameter = DEFAULT_AMP_FACTOR * 2;

vm.prank(admin);
StablePool(pool).startAmplificationParameterUpdate(newAmplificationParameter, endTime);
IStablePool(pool).startAmplificationParameterUpdate(newAmplificationParameter, endTime);

vm.warp(currentTime + updateInterval + 1);

Expand Down
4 changes: 1 addition & 3 deletions pkg/solidity-utils/test/foundry/utils/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ pragma solidity ^0.8.24;

import "forge-std/Test.sol";

import { GasSnapshot } from "forge-gas-snapshot/GasSnapshot.sol";

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";

Expand All @@ -16,7 +14,7 @@ import { ERC4626TestToken } from "../../../contracts/test/ERC4626TestToken.sol";
import { ERC20TestToken } from "../../../contracts/test/ERC20TestToken.sol";
import { WETHTestToken } from "../../../contracts/test/WETHTestToken.sol";

abstract contract BaseTest is Test, GasSnapshot {
abstract contract BaseTest is Test {
using CastingHelpers for *;

uint256 internal constant DEFAULT_BALANCE = 1e9 * 1e18;
Expand Down
91 changes: 89 additions & 2 deletions pkg/vault/contracts/AggregatorRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { RouterCommon } from "./RouterCommon.sol";
* @notice Entrypoint for aggregators who want to swap without the standard permit2 payment logic.
* @dev The external API functions unlock the Vault, which calls back into the corresponding hook functions.
* These interact with the Vault and settle accounting. This is not a full-featured Router; it only implements
* `swapSingleTokenExactIn` and the associated query.
* `swapSingleTokenExactIn`, `swapSingleTokenExactOut`, and the associated queries.
*/
contract AggregatorRouter is IAggregatorRouter, RouterCommon {
constructor(
Expand Down Expand Up @@ -67,6 +67,39 @@ contract AggregatorRouter is IAggregatorRouter, RouterCommon {
);
}

/// @inheritdoc IAggregatorRouter
function swapSingleTokenExactOut(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountOut,
uint256 maxAmountIn,
uint256 deadline,
bytes calldata userData
) external saveSender(msg.sender) returns (uint256) {
return
abi.decode(
_vault.unlock(
abi.encodeCall(
AggregatorRouter.swapSingleTokenHook,
IRouter.SwapSingleTokenHookParams({
sender: msg.sender,
kind: SwapKind.EXACT_OUT,
pool: pool,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountGiven: exactAmountOut,
limit: maxAmountIn,
deadline: deadline,
wethIsEth: false,
userData: userData
})
)
),
(uint256)
);
}

/**
* @notice Hook for swaps.
* @dev Can only be called by the Vault. This router expects the caller to pay upfront by sending tokens to the
Expand All @@ -79,9 +112,31 @@ contract AggregatorRouter is IAggregatorRouter, RouterCommon {
function swapSingleTokenHook(
IRouter.SwapSingleTokenHookParams calldata params
) external nonReentrant onlyVault returns (uint256) {
// `amountInHint` represents the amount supposedly paid upfront by the sender.
uint256 amountInHint;
if (params.kind == SwapKind.EXACT_IN) {
amountInHint = params.amountGiven;
} else {
amountInHint = params.limit;
}

// Always settle the amount paid first to prevent potential underflows at the vault. `tokenInCredit`
// represents the amount actually paid by the sender, which can be at most `amountInHint`.
// If the user paid less than what was expected, revert early.
uint256 tokenInCredit = _vault.settle(params.tokenIn, amountInHint);
if (tokenInCredit < amountInHint) {
revert SwapInsufficientPayment();
}

(uint256 amountCalculated, uint256 amountIn, uint256 amountOut) = _swapHook(params);

_vault.settle(params.tokenIn, amountIn);
if (params.kind == SwapKind.EXACT_OUT) {
// Transfer any leftovers back to the sender (amount actually paid minus amount required for the swap).
// At this point, the Vault already validated that `tokenInCredit > amountIn`.
_sendTokenOut(params.sender, params.tokenIn, tokenInCredit - amountIn, false);
}

// Finally, settle the output token by sending the credited tokens to the sender.
_sendTokenOut(params.sender, params.tokenOut, amountOut, false);

return amountCalculated;
Expand Down Expand Up @@ -145,6 +200,38 @@ contract AggregatorRouter is IAggregatorRouter, RouterCommon {
);
}

/// @inheritdoc IAggregatorRouter
function querySwapSingleTokenExactOut(
address pool,
IERC20 tokenIn,
IERC20 tokenOut,
uint256 exactAmountOut,
address sender,
bytes memory userData
) external saveSender(sender) returns (uint256 amountCalculated) {
return
abi.decode(
_vault.quote(
abi.encodeCall(
AggregatorRouter.querySwapHook,
IRouter.SwapSingleTokenHookParams({
sender: msg.sender,
kind: SwapKind.EXACT_OUT,
pool: pool,
tokenIn: tokenIn,
tokenOut: tokenOut,
amountGiven: exactAmountOut,
limit: _MAX_AMOUNT,
deadline: _MAX_AMOUNT,
wethIsEth: false,
userData: userData
})
)
),
(uint256)
);
}

/**
* @notice Hook for swap queries.
* @dev Can only be called by the Vault.
Expand Down
Loading

0 comments on commit 4cb24ed

Please sign in to comment.