Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roundrip fee on proportional remove #1010

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/interfaces/contracts/test/IVaultMainMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ interface IVaultMainMock {

function manualFindTokenIndex(IERC20[] memory tokens, IERC20 token) external pure returns (uint256 index);

function manualSetAddLiquidityCalledFlag(address pool, bool flag) external;

function manualSetPoolCreator(address pool, address newPoolCreator) external;

function ensureValidTradeAmount(uint256 tradeAmount) external view;
Expand Down
6 changes: 6 additions & 0 deletions pkg/interfaces/contracts/vault/IVaultExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ interface IVaultExtension {
*/
function getReservesOf(IERC20 token) external view returns (uint256);

/**
* @notice Returns `true` if `addLiquidity` was called for the given pool in this transaction.
* @param pool Address of the pool to check.
Comment on lines +71 to +72
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @notice Returns `true` if `addLiquidity` was called for the given pool in this transaction.
* @param pool Address of the pool to check.
* @notice This flag is used to detect and tax "round trip" transactions (adding/removing liquidity in the same pool)
* @param pool Address of the pool to check
* @return liquidityAdded True if liquidity has been added to this pool in the current transaction

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also add a dev section here with more detail on why this is desirable / improves security, and documenting limitations.

*/
function getAddLiquidityCalledFlag(address pool) external view returns (bool);

/*******************************************************************************
Pool Registration
*******************************************************************************/
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
302.1k
302.0k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
333.0k
332.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
259.2k
259.1k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
196.9k
197.1k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
181.5k
181.7k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
195.5k
195.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
180.2k
180.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
172.5k
172.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
215.0k
215.2k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
168.2k
168.3k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
218.6k
218.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
203.2k
203.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
198.4k
198.3k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
235.6k
235.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
194.3k
194.5k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
231.4k
231.6k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
223.4k
223.5k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
178.9k
179.1k
3 changes: 3 additions & 0 deletions pkg/pool-weighted/test/foundry/WeightedPoolLimits.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ contract WeightedPoolLimitsTest is BaseVaultTest, WeightedPoolContractsDeployer
startingBalances[daiIdx] = dai.balanceOf(bob);
startingBalances[usdcIdx] = usdc.balanceOf(bob);

// Prevent roundtrip fee
vault.manualSetAddLiquidityCalledFlag(pool, false);

uint256[] memory amountsOut = router.removeLiquidityProportional(
address(weightedPool),
bptAmountIn,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
297.0k
296.9k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
319.5k
319.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
190.6k
190.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
168.2k
168.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
180.2k
180.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
159.3k
159.5k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
213.5k
213.7k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
168.2k
168.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
169.2k
169.1k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
152.1k
152.0k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
212.5k
212.7k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
190.0k
190.2k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
218.5k
218.4k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
184.3k
184.2k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
235.6k
235.8k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
181.0k
181.3k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
230.3k
230.5k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
223.4k
223.6k
Original file line number Diff line number Diff line change
@@ -1 +1 @@
178.9k
179.2k
27 changes: 18 additions & 9 deletions pkg/solidity-utils/contracts/helpers/TransientStorageHelpers.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { SlotDerivation } from "../openzeppelin/SlotDerivation.sol";
import { StorageSlotExtension } from "../openzeppelin/StorageSlotExtension.sol";

type TokenDeltaMappingSlotType is bytes32;
type AddressMappingSlot is bytes32;
type AddressToUintMappingSlot is bytes32;
type AddressToBooleanMappingSlot is bytes32;
type AddressArraySlotType is bytes32;

/**
Expand Down Expand Up @@ -46,21 +47,29 @@ library TransientStorageHelpers {
TokenDeltaMappingSlotType.unwrap(slot).deriveMapping(address(k1)).asInt256().tstore(value);
}

function tGet(AddressMappingSlot slot, address key) internal view returns (uint256) {
return AddressMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tload();
function tGet(AddressToUintMappingSlot slot, address key) internal view returns (uint256) {
return AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tload();
}

function tSet(AddressMappingSlot slot, address key, uint256 value) internal {
AddressMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(value);
function tSet(AddressToUintMappingSlot slot, address key, uint256 value) internal {
AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(value);
}

function tGet(AddressToBooleanMappingSlot slot, address key) internal view returns (bool) {
return AddressToBooleanMappingSlot.unwrap(slot).deriveMapping(key).asBoolean().tload();
}

function tSet(AddressToBooleanMappingSlot slot, address key, bool value) internal {
AddressToBooleanMappingSlot.unwrap(slot).deriveMapping(key).asBoolean().tstore(value);
}

// Implement the common "+=" operation: map[key] += value.
function tAdd(AddressMappingSlot slot, address key, uint256 value) internal {
AddressMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) + value);
function tAdd(AddressToUintMappingSlot slot, address key, uint256 value) internal {
AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) + value);
}

function tSub(AddressMappingSlot slot, address key, uint256 value) internal {
AddressMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) - value);
function tSub(AddressToUintMappingSlot slot, address key, uint256 value) internal {
AddressToUintMappingSlot.unwrap(slot).deriveMapping(key).asUint256().tstore(tGet(slot, key) - value);
}

/***************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.24;
import { StorageSlotExtension } from "./StorageSlotExtension.sol";
import {
AddressArraySlotType,
AddressMappingSlot,
AddressToUintMappingSlot,
TransientStorageHelpers
} from "../helpers/TransientStorageHelpers.sol";

Expand Down Expand Up @@ -212,7 +212,7 @@ library TransientEnumerableSet {
}
}

function _indexes(AddressSet storage set) private view returns (AddressMappingSlot slot) {
function _indexes(AddressSet storage set) private view returns (AddressToUintMappingSlot slot) {
mapping(address addressKey => uint256 indexValue) storage indexes = set.__indexes;

assembly ("memory-safe") {
Expand Down
14 changes: 7 additions & 7 deletions pkg/vault/contracts/BatchRouterCommon.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/TransientEnumerableSet.sol";
import {
TransientStorageHelpers,
AddressMappingSlot
AddressToUintMappingSlot
} from "@balancer-labs/v3-solidity-utils/contracts/helpers/TransientStorageHelpers.sol";

import { RouterCommon } from "./RouterCommon.sol";
Expand Down Expand Up @@ -61,21 +61,21 @@ contract BatchRouterCommon is RouterCommon {
}

// token in -> amount: tracks token in amounts within a batch swap.
function _currentSwapTokenInAmounts() internal view returns (AddressMappingSlot slot) {
return AddressMappingSlot.wrap(_CURRENT_SWAP_TOKEN_IN_AMOUNTS_SLOT);
function _currentSwapTokenInAmounts() internal view returns (AddressToUintMappingSlot slot) {
return AddressToUintMappingSlot.wrap(_CURRENT_SWAP_TOKEN_IN_AMOUNTS_SLOT);
}

// token out -> amount: tracks token out amounts within a batch swap.
function _currentSwapTokenOutAmounts() internal view returns (AddressMappingSlot slot) {
return AddressMappingSlot.wrap(_CURRENT_SWAP_TOKEN_OUT_AMOUNTS_SLOT);
function _currentSwapTokenOutAmounts() internal view returns (AddressToUintMappingSlot slot) {
return AddressToUintMappingSlot.wrap(_CURRENT_SWAP_TOKEN_OUT_AMOUNTS_SLOT);
}

// token -> amount that is part of the current input / output amounts, but is settled preemptively.
// This situation happens whenever there is BPT involved in the operation, which is minted and burned instantly.
// Since those amounts are not tracked in the inputs / outputs to settle, we need to track them elsewhere
// to return the correct total amounts in and out for each token involved in the operation.
function _settledTokenAmounts() internal view returns (AddressMappingSlot slot) {
return AddressMappingSlot.wrap(_SETTLED_TOKEN_AMOUNTS_SLOT);
function _settledTokenAmounts() internal view returns (AddressToUintMappingSlot slot) {
return AddressToUintMappingSlot.wrap(_SETTLED_TOKEN_AMOUNTS_SLOT);
}

function _calculateBatchRouterStorageSlot(string memory key) internal pure returns (bytes32) {
Expand Down
10 changes: 10 additions & 0 deletions pkg/vault/contracts/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ contract Vault is IVaultMain, VaultCommon, Proxy {
// bptOut = supply * (ratio - 1), so lower ratio = less bptOut, favoring the pool.

_ensureUnpaused(params.pool);
_addLiquidityCalled().tSet(params.pool, true);

// `_loadPoolDataUpdatingBalancesAndYieldFees` is non-reentrant, as it updates storage as well
// as filling in poolData in memory. Since the add liquidity hooks are reentrant and could do anything,
Expand Down Expand Up @@ -878,6 +879,15 @@ contract Vault is IVaultMain, VaultCommon, Proxy {
_totalSupply(params.pool),
bptAmountIn
);

// Charge roundtrip fee.
if (_addLiquidityCalled().tGet(params.pool)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: if we clear this flag here, it's easy to bypass.

You add a huge amount, make a decoy proportional exit, and then a big exit.
Whenever the flag is set, it'll be set for the rest of the tx. A more correct approach would be to clean it when transient modifier ends, but we would need a transient set for that and I really don't think it's worth the added complexity.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that's probably not worth it for an edge case. Although... maybe aggregators might bundle lots of transactions and come across that, since it just checks the pool, not the pool + user, so this could also get triggered on bundled transactions with multiple unrelated users doing liquidity operations on a popular pool. Maybe worth investigating if that's possible? If one person added and another person removed, the second one would get unexpectedly taxed.

Though even storing the user wouldn't be foolproof, as someone could craft a transaction where the add and remove steps were done from separate addresses controlled by the same account.

uint256 swapFeePercentage = poolData.poolConfigBits.getStaticSwapFeePercentage();
for (uint256 i = 0; i < locals.numTokens; ++i) {
swapFeeAmounts[i] = amountsOutScaled18[i].mulUp(swapFeePercentage);
amountsOutScaled18[i] -= swapFeeAmounts[i];
}
}
} else if (params.kind == RemoveLiquidityKind.SINGLE_TOKEN_EXACT_IN) {
poolData.poolConfigBits.requireUnbalancedLiquidityEnabled();
bptAmountIn = params.maxBptAmountIn;
Expand Down
5 changes: 5 additions & 0 deletions pkg/vault/contracts/VaultExtension.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ contract VaultExtension is IVaultExtension, VaultCommon, Proxy {
return _reservesOf[token];
}

/// @inheritdoc IVaultExtension
function getAddLiquidityCalledFlag(address pool) external view onlyVaultDelegateCall returns (bool) {
return _addLiquidityCalled().tGet(pool);
}

/*******************************************************************************
Pool Registration
*******************************************************************************/
Expand Down
9 changes: 7 additions & 2 deletions pkg/vault/contracts/VaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import "@balancer-labs/v3-interfaces/contracts/vault/VaultTypes.sol";
import { StorageSlotExtension } from "@balancer-labs/v3-solidity-utils/contracts/openzeppelin/StorageSlotExtension.sol";
import {
TransientStorageHelpers,
AddressArraySlotType,
TokenDeltaMappingSlotType
TokenDeltaMappingSlotType,
AddressToBooleanMappingSlot
} from "@balancer-labs/v3-solidity-utils/contracts/helpers/TransientStorageHelpers.sol";

import { VaultStateBits } from "./lib/VaultStateLib.sol";
Expand Down Expand Up @@ -66,6 +66,7 @@ contract VaultStorage {
bytes32 private immutable _IS_UNLOCKED_SLOT = _calculateVaultStorageSlot("isUnlocked");
bytes32 private immutable _NON_ZERO_DELTA_COUNT_SLOT = _calculateVaultStorageSlot("nonZeroDeltaCount");
bytes32 private immutable _TOKEN_DELTAS_SLOT = _calculateVaultStorageSlot("tokenDeltas");
bytes32 private immutable _ADD_LIQUIDITY_CALLED_SLOT = _calculateVaultStorageSlot("addLiquidityCalled");
// solhint-enable var-name-mixedcase

/***************************************************************************
Expand Down Expand Up @@ -173,6 +174,10 @@ contract VaultStorage {
return TokenDeltaMappingSlotType.wrap(_TOKEN_DELTAS_SLOT);
}

function _addLiquidityCalled() internal view returns (AddressToBooleanMappingSlot slot) {
return AddressToBooleanMappingSlot.wrap(_ADD_LIQUIDITY_CALLED_SLOT);
}

function _calculateVaultStorageSlot(string memory key) private pure returns (bytes32) {
return TransientStorageHelpers.calculateSlot(type(VaultStorage).name, key);
}
Expand Down
Loading
Loading