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

evm-contracts/deployment-audit-xhack #472

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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: 1 addition & 1 deletion .github/workflows/evm-contracts-verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Check Solidity lint
run: yarn lint:sol --max-warnings=0
- name: Check build
run: yarn build
run: yarn workspaces foreach --topological-dev --parallel --recursive --from @swim-io/evm-contracts run build
- name: Check TypeScript lint
run: yarn lint:ts --max-warnings=0
- name: "Create .env file for deployment"
Expand Down
75 changes: 75 additions & 0 deletions docs/poolmath/frontrunning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3

from swim_invariant import SwimPool, Decimal

token_count = 2
amp_factor = Decimal(10)
lp_fee = Decimal("0.0003")
gov_fee = Decimal("0.0001")
tolerance = Decimal("0.000001")
base = Decimal(1000000)
abort_search_factor = Decimal("0.00000001")
search_step = Decimal("0.01")
initial_guess = base * Decimal("0.001")

def setup():
balances = [Decimal(base) for _ in range(token_count)]
pool = SwimPool(token_count, amp_factor, lp_fee, gov_fee, tolerance)
pool.add(balances)
return pool

def swap(pool, amount, index):
input = [Decimal(0) for _ in range(token_count)]
input[index] = amount
return pool.swap_exact_input(input, 0 if index == 1 else 1)[0]

def frontrun(swap_amount, fr_amount, debug=False):
pool = setup()
fr_pre = swap(pool, fr_amount, 0)
user_output = swap(pool, swap_amount, 0)
fr_post = swap(pool, fr_pre, 1)
fr_profit = fr_post - fr_amount
if debug:
print(f" user input: {swap_amount:>10.5f}")
print(f" user output: {user_output:>10.5f}")
print(f"frontrun amount: {fr_amount:>10.5f}")
print(f"frontrun pre: {fr_pre:>10.5f}")
print(f"frontrun post: {fr_post:>10.5f}")
print(f"frontrun profit: {fr_profit:>10.5f}")
print("---------------------")
return [fr_profit, user_output]

def profitability_threshold(swap_amount, debug=False):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Love this whole setup

fr_amount = swap_amount
while True:
[fr_profit, user_output] = frontrun(swap_amount, fr_amount, debug)
if fr_profit > 0:
return fr_amount
if user_output < swap_amount * abort_search_factor:
return -1
fr_amount *= Decimal(1) + search_step

swap_amount = initial_guess
search_direction = -1 if profitability_threshold(swap_amount) > 0 else 1
while True:
fr_amount = profitability_threshold(swap_amount)
if search_direction == -1 and fr_amount == -1:
break
if search_direction == 1 and fr_amount > 0:
swap_amount *= 1 / (Decimal(1) + search_direction * search_step)
break
if fr_amount == -1:
print(f"{100*swap_amount/base:.4f} % is not exploitable")
else:
print(f"{100*swap_amount/base:.4f} % exploitable with {100*fr_amount/base:.2f} %")
swap_amount *= Decimal(1) + search_direction * search_step

fr_amount = profitability_threshold(swap_amount / (Decimal(1) + search_direction * search_step), True)
print()
print("given:")
print(f" token count: {token_count}")
print(f"pool balances: {base} each")
print(f" amp factor: {amp_factor}")
print(f" total fee: {int((lp_fee+gov_fee)*10000)} bips")
print(f"then a swap of {swap_amount:.2f} ({100*swap_amount/base:.3f} % of a pool balance) " +
"is unexploitable\n")
3 changes: 3 additions & 0 deletions packages/evm-contracts/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
MNEMONIC=test test test test test test test test test test test junk
FACTORY_MNEMONIC=try exercise column boring supreme corn fabric idea federal today hood equip
Copy link
Collaborator

Choose a reason for hiding this comment

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

"try exercise" :D

ETHERSCAN_API_KEY=
25 changes: 9 additions & 16 deletions packages/evm-contracts/contracts/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,16 @@ pragma solidity ^0.8.15;
bytes32 constant SWIM_USD_SOLANA_ADDRESS =
0x296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe26719;
bytes32 constant ROUTING_CONTRACT_SOLANA_ADDRESS =
0x0000000000000000000000000000000000000000000000000000000000000000; //TBD
0x857d8c691b9e9a1a1e98d010a36d6401a9099ce89d821751410623ad7c2a20d2;
address constant SWIM_FACTORY = address(0xDef312467D48bdDED813de11C3ee4c257e6eD7aD);
address constant ROUTING_CONTRACT = address(0x280999aB9aBfDe9DC5CE7aFB25497d6BB3e8bDD4);
address constant LP_TOKEN_LOGIC = address(0x357bb5061A015B898948B95Fb3422595E0Cf81CB);
uint constant PROPELLER_GAS_TIP = 1000000000; //=1 gwei;
uint16 constant WORMHOLE_SOLANA_CHAIN_ID = 1;
address constant SWIM_FACTORY = address(0x36E284788aaA29C16cc227E09477C8e73D96ffD3);
address constant ROUTING_CONTRACT = address(0xa33E4d9624608c468FE5466dd6CC39cE1Da4FF78);
address constant LP_TOKEN_LOGIC = address(0xc9752D59E6b66185156C0d8D9DC1b4661b1fA0C2);
//the following constants are "truly constant" in that the implementation depends on their
// particular values and hence changing them might break fundamental assumptions baked into the code
uint constant POOL_PRECISION = 6;
uint constant ROUTING_PRECISION = 18;
uint8 constant SWIM_USD_DECIMALS = 6;
//--------------------------------------------------------------------------------------------------

uint8 constant SWIM_USD_TOKEN_INDEX = 0;
uint16 constant SWIM_USD_TOKEN_NUMBER = 0;

uint constant FEE_DECIMALS = 6; //enough to represent 100th of a bip
uint constant FEE_MULTIPLIER = 10**FEE_DECIMALS;

//amp factor for internal respresentation (shifting is efficiently combined with other pool math)
uint constant AMP_SHIFT = 10; //number of bits ampFactor is shifted to the left
uint constant ONE_AMP_SHIFTED = 1 << AMP_SHIFT;

uint constant MARGINAL_PRICE_DECIMALS = 18;
uint constant MARGINAL_PRICE_MULTIPLIER = 10**MARGINAL_PRICE_DECIMALS;
11 changes: 11 additions & 0 deletions packages/evm-contracts/contracts/Invariant.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import "./Constants.sol";
import "./CenterAlignment.sol";
import "./Equalize.sol";

//amp factor for internal respresentation (shifting is efficiently combined with other pool math)
uint constant AMP_SHIFT = 10; //number of bits ampFactor is shifted to the left
uint constant MARGINAL_PRICE_DECIMALS = 18;

library Invariant {
error UnknownBalanceTooLarge(uint unknownBalance);

using CenterAlignment for uint;

uint private constant MARGINAL_PRICE_MULTIPLIER = 10**MARGINAL_PRICE_DECIMALS;
uint private constant ONE_AMP_SHIFTED = 1 << AMP_SHIFT;

// RESTRICTIONS:
// * Equalizeds use at most 61 bits (= ~18 digits).
// * MAX_TOKEN_COUNT = 6 so that:
Expand Down Expand Up @@ -202,6 +209,10 @@ library Invariant {
return Equalized.wrap(uint64(unknownBalance));
}}

//fails with division by zero if pool is empty which is fine for our purposes
// adding an additional check would be cleaner but since our pools will always been seeded
// immediately after deployment and nothing bad comes of it anyway, implementing said check would
// just be an unnecessary gas burden on users
function calculateDepth(
Equalized[] memory poolBalances,
uint32 ampFactor,
Expand Down
44 changes: 25 additions & 19 deletions packages/evm-contracts/contracts/Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
using SafeERC20 for IERC20;

uint private constant MAX_TOKEN_COUNT = 6;
int8 private constant POOL_PRECISION = 6;
int8 private constant SWIM_USD_EQUALIZER = POOL_PRECISION - int8(SWIM_USD_DECIMALS);
int8 private constant PRECISION = int8(int(POOL_PRECISION));
int8 private constant SWIM_USD_EQUALIZER = PRECISION - int8(SWIM_USD_DECIMALS);
//Min and max equalizers are somewhat arbitrary, though shifting down by more than 14 decimals
// will almost certainly be unintentional and shifting up by more than 4 digits will almost
// certainly result in too small of a usable value range (only 18 digits in total!).
Expand All @@ -53,7 +53,7 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
uint private constant MIN_AMP_ADJUSTMENT_WINDOW = 1 days;
uint private constant MAX_AMP_RELATIVE_ADJUSTMENT = 10;

//slot0 (28/32 bytes used)
//slot[0] (28/32 bytes used)
// We could cut down on gas costs further by implementing a method that reads the slot once
// and parses out the values manually instead of having Solidity generate inefficient, garbage
// bytecode as it does...
Expand All @@ -68,16 +68,17 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
uint32 private _ampTargetValue; //in internal, i.e. AMP_SHIFTED representation
uint32 private _ampTargetTimestamp;

//slot1
address public _governance;
//slot[1]
address private _governance;

//slot2
address public _governanceFeeRecipient;
//slot[2]
address private _governanceFeeRecipient;

//slot3
//slot[3]
TokenWithEqualizer private /*immutable*/ _lpTokenData;

//slots4-4+MAX_TOKEN_COUNT (use fixed size array to save gas by not having to keccak on access)
//slots[4 to 4+MAX_TOKEN_COUNT]
// (use fixed size array to save gas by not having to keccak on access)
TokenWithEqualizer[MAX_TOKEN_COUNT] private /*immutable*/ _poolTokensData;

modifier notPaused {
Expand Down Expand Up @@ -111,7 +112,7 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
//moved to a separate function to avoid stack too deep
deployLpToken(lpTokenName, lpTokenSymbol, lpTokenDecimals, lpTokenSalt),
//LpToken ensures decimals are sensible hence we don't have to worry about conversion here
POOL_PRECISION - int8(lpTokenDecimals)
PRECISION - int8(lpTokenDecimals)
);

uint tokenCount = poolTokenAddresses.length + 1;
Expand All @@ -122,8 +123,8 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
_tokenCount = uint8(tokenCount);

//swimUSD is always the first token
_poolTokensData[0].addr = IRouting(ROUTING_CONTRACT).swimUsdAddress();
_poolTokensData[0].equalizer = SWIM_USD_EQUALIZER;
_poolTokensData[SWIM_USD_TOKEN_INDEX].addr = IRouting(ROUTING_CONTRACT).swimUsdAddress();
_poolTokensData[SWIM_USD_TOKEN_INDEX].equalizer = SWIM_USD_EQUALIZER;

for (uint i = 0; i < poolTokenAddresses.length; ++i) {
//check that token contract exists and is (likely) ERC20 by calling balanceOf
Expand Down Expand Up @@ -539,13 +540,18 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
//We're limiting total fees to less than 50 % because:
// 1) Anything even close to approaching this is already entirely insane.
// 2) To avoid theoretical overflow/underflow issues when calculating the inverse fee,
// of 1/(1-fee)-1 would exceed 100 % if fee were to exceeds 50 %.
// of 1/(1-fee)-1 would exceed 100 % if fee were to exceed 50 %.
if (totalFee >= FEE_MULTIPLIER/2)
revert TotalFeeTooLarge(totalFee, uint32(FEE_MULTIPLIER/2 - 1));
if (governanceFee != 0 && _governanceFeeRecipient == address(0))
revert NonZeroGovernanceFeeButNoRecipient();
_totalFee = totalFee;
_governanceFee = governanceFee;

emit FeesChanged(
Decimal(lpFee, uint8(FEE_DECIMALS)),
Decimal(governanceFee, uint8(FEE_DECIMALS))
);
}

function adjustAmpFactor(uint32 targetValue, uint32 targetTimestamp) external onlyGovernance {
Expand All @@ -570,16 +576,16 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {
}
else {
uint threshold = ampTargetValue * MAX_AMP_RELATIVE_ADJUSTMENT;
if (ampTargetValue < threshold)
if (currentAmpFactor > threshold)
revert AmpFactorRelativeAdjustmentTooLarge(
toExternalAmpValue(uint32(currentAmpFactor)),
targetValue,
toExternalAmpValue(uint32(threshold))
);
}
// solhint-disable-next-line not-rely-on-time
_ampInitialValue = uint32(block.timestamp);
_ampInitialTimestamp = uint32(currentAmpFactor);
_ampInitialValue = uint32(currentAmpFactor);
// solhint-disable-next-line not-rely-on-time
_ampInitialTimestamp = uint32(block.timestamp);
_ampTargetValue = uint32(ampTargetValue);
_ampTargetTimestamp = targetTimestamp;
}
Expand All @@ -591,14 +597,14 @@ contract Pool is IPool, Initializable, UUPSUpgradeable {

function transferGovernance(address governance_) external onlyGovernance {
_governance = governance_;
emit TransferGovernance(msg.sender, governance_);
emit GovernanceChanged(msg.sender, governance_);
}

function changeGovernanceFeeRecipient(address governanceFeeRecipient_) external onlyGovernance {
if (_governanceFee != 0 && governanceFeeRecipient_ == address(0))
revert NonZeroGovernanceFeeButNoRecipient();
_governanceFeeRecipient = governanceFeeRecipient_;
emit ChangeGovernanceFeeRecipient(governanceFeeRecipient_);
emit GovernanceFeeRecipientChanged(governanceFeeRecipient_);
}

function upgradeLpToken(address newImplementation) external onlyGovernance {
Expand Down
5 changes: 4 additions & 1 deletion packages/evm-contracts/contracts/PoolMath.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ pragma solidity ^0.8.15;
import "./Equalize.sol";
import "./Invariant.sol";

uint constant FEE_DECIMALS = 6; //enough to represent 100th of a bip
uint constant FEE_MULTIPLIER = 10**FEE_DECIMALS;

//The code in here is less readable than I'd like because I had to inline a couple of variables to
// avoid solc's "Stack too deep, try removing local variables" error message
library PoolMath {
Expand Down Expand Up @@ -137,7 +140,7 @@ library PoolMath {
userTokenAmount = Equalized.wrap(userTokenAmount_);

if (pool.totalFee != 0) {
uint finalDepth = Invariant.calculateDepth(pool.balances, pool.ampFactor, initialDepth);
uint finalDepth = Invariant.calculateDepth(updatedBalances, pool.ampFactor, initialDepth);
uint totalFeeDepth = finalDepth - initialDepth;
uint governanceDepth = (totalFeeDepth * pool.governanceFee) / pool.totalFee; //rounding?
uint totalLpDepth = finalDepth - governanceDepth;
Expand Down
Loading