From 07a1f546a3aa73bf1457cb027581ea345e865b84 Mon Sep 17 00:00:00 2001 From: swimivan Date: Mon, 24 Oct 2022 23:27:12 -0700 Subject: [PATCH 01/14] fix: Use updated balances when calculating govMintAmount --- packages/evm-contracts/contracts/PoolMath.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm-contracts/contracts/PoolMath.sol b/packages/evm-contracts/contracts/PoolMath.sol index 9384ad7f2..f333162af 100644 --- a/packages/evm-contracts/contracts/PoolMath.sol +++ b/packages/evm-contracts/contracts/PoolMath.sol @@ -137,7 +137,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; From fdce66afe34fd33f55d38183b06dbf24de63aea3 Mon Sep 17 00:00:00 2001 From: swimivan Date: Mon, 24 Oct 2022 23:38:28 -0700 Subject: [PATCH 02/14] feat: SwimFactory now also catches Panic during proxy construction --- packages/evm-contracts/contracts/SwimFactory.sol | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/evm-contracts/contracts/SwimFactory.sol b/packages/evm-contracts/contracts/SwimFactory.sol index 2e54085b3..de5d9ddae 100644 --- a/packages/evm-contracts/contracts/SwimFactory.sol +++ b/packages/evm-contracts/contracts/SwimFactory.sol @@ -136,6 +136,9 @@ contract SwimFactory is ISwimFactory { bytes memory code = proxyDeploymentCode(); address proxy = create2(code, salt); try IUUPSUpgradeable(proxy).upgradeToAndCall(logic, call) {} + catch Panic(uint errorCode) { + revert ProxyConstructorFailed(abi.encode(errorCode)); + } catch (bytes memory lowLevelData) { revert ProxyConstructorFailed(lowLevelData); } From 498bcd233000bc13e88d407e0a8f09e8708b4725 Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 03:14:21 -0700 Subject: [PATCH 03/14] feat: Impl audit feedback, refactor Propeller gas fees, cleanup, redeployment * audit: add missing whenNotPaused modifier * audit: fix amp factor logic in Pool * audit: emit additional events in Routing and Pool * fix: throw in propellerComplete() when not a Propeller transaction * fix: cap remunerated gas price by tx.gasprice * feat: make remunerated Propeller gas price depend on specific chain * feat: implement Decimal interface for fixedSwimUsdPerGasToken * feat: expose Wormhole nonce in function signatures for upcoming BatchVAAs * feat: catch Panic thrown by failed Wormhole interactions * feat: compile contracts using viaIR optimization * chore: improve gas estimation for Propeller gas fee remuneration * chore: have registerToken() clean up old registrations and check inputs * chore: clean-up and relocate contract structs, enums, events, and constants * chore: update constants after deployment --- .../evm-contracts/contracts/Constants.sol | 25 +-- .../evm-contracts/contracts/Invariant.sol | 11 + packages/evm-contracts/contracts/Pool.sol | 42 ++-- packages/evm-contracts/contracts/PoolMath.sol | 3 + packages/evm-contracts/contracts/Routing.sol | 210 ++++++++++++++---- .../contracts/interfaces/Decimal.sol | 7 + .../contracts/interfaces/IPool.sol | 26 +-- .../contracts/interfaces/IRouting.sol | 79 ++++++- .../contracts/interfaces/PoolState.sol | 18 ++ packages/evm-contracts/hardhat.config.ts | 27 ++- packages/evm-contracts/src/config.ts | 77 ++++++- packages/evm-contracts/src/presigned.ts | 2 +- 12 files changed, 413 insertions(+), 114 deletions(-) create mode 100644 packages/evm-contracts/contracts/interfaces/Decimal.sol create mode 100644 packages/evm-contracts/contracts/interfaces/PoolState.sol diff --git a/packages/evm-contracts/contracts/Constants.sol b/packages/evm-contracts/contracts/Constants.sol index 6a06370ae..a582cbae8 100644 --- a/packages/evm-contracts/contracts/Constants.sol +++ b/packages/evm-contracts/contracts/Constants.sol @@ -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; diff --git a/packages/evm-contracts/contracts/Invariant.sol b/packages/evm-contracts/contracts/Invariant.sol index 553ed6769..9c731927c 100644 --- a/packages/evm-contracts/contracts/Invariant.sol +++ b/packages/evm-contracts/contracts/Invariant.sol @@ -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 constant MARGINAL_PRICE_MULTIPLIER = 10**MARGINAL_PRICE_DECIMALS; + uint constant ONE_AMP_SHIFTED = 1 << AMP_SHIFT; + // RESTRICTIONS: // * Equalizeds use at most 61 bits (= ~18 digits). // * MAX_TOKEN_COUNT = 6 so that: @@ -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, diff --git a/packages/evm-contracts/contracts/Pool.sol b/packages/evm-contracts/contracts/Pool.sol index 90b76cdbb..176bb4009 100644 --- a/packages/evm-contracts/contracts/Pool.sol +++ b/packages/evm-contracts/contracts/Pool.sol @@ -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!). @@ -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... @@ -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 { @@ -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; @@ -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 @@ -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 { @@ -570,7 +576,7 @@ 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, @@ -578,8 +584,8 @@ contract Pool is IPool, Initializable, UUPSUpgradeable { ); } // solhint-disable-next-line not-rely-on-time - _ampInitialValue = uint32(block.timestamp); - _ampInitialTimestamp = uint32(currentAmpFactor); + _ampInitialValue = uint32(currentAmpFactor); + _ampInitialTimestamp = uint32(block.timestamp); _ampTargetValue = uint32(ampTargetValue); _ampTargetTimestamp = targetTimestamp; } @@ -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 { diff --git a/packages/evm-contracts/contracts/PoolMath.sol b/packages/evm-contracts/contracts/PoolMath.sol index f333162af..ef47bd819 100644 --- a/packages/evm-contracts/contracts/PoolMath.sol +++ b/packages/evm-contracts/contracts/PoolMath.sol @@ -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 { diff --git a/packages/evm-contracts/contracts/Routing.sol b/packages/evm-contracts/contracts/Routing.sol index 9bb795076..2394ac26a 100644 --- a/packages/evm-contracts/contracts/Routing.sol +++ b/packages/evm-contracts/contracts/Routing.sol @@ -23,17 +23,7 @@ contract Routing is OwnableUpgradeable, UUPSUpgradeable { - struct TokenInfo { - uint16 tokenNumber; - address tokenAddress; - address poolAddress; - uint8 tokenIndexInPool; - } - - enum GasTokenPriceMethod { - FixedPrice, - UniswapOracle - } + using SafeERC20 for IERC20; //uses two slots struct UniswapOracleParams { @@ -46,28 +36,34 @@ contract Routing is struct PropellerFeeConfig { GasTokenPriceMethod method; - uint64 serviceFee; //specified in swimUSD + //service fee specified in swimUSD + uint64 serviceFee; + //specified in atomic with 18 decimals, i.e. how many atomic swimUSD per 1 wei? + // assuming a gas token price of 1 (human) swimUSD / 1 (human) gas token where swimUSD has + // 6 decimals and gas token has 18 decimals means 10^-12 atomic swimUSD per 1 wei gas token + // and thus taking 18 decimals into account fixedSwimUsdPerGasToken would equal 10^6 uint fixedSwimUsdPerGasToken; UniswapOracleParams uniswap; } - using SafeERC20 for IERC20; - - uint private constant PRECISION = 18; - uint private constant GAS_COST_BASE = 85000; - uint private constant GAS_COST_POOL_SWAP = 125000; + uint private constant PRECISION = ROUTING_PRECISION; + uint private constant PRECISION_MULTIPLIER = 10 ** PRECISION; + uint private constant GAS_COST_BASE = 79900; + uint private constant GAS_COST_POOL_SWAP = 80000; uint private constant GAS_KICKSTART_AMOUNT = 0.05 ether; - uint private constant PROPELLER_GAS_TIP = 1 gwei; + + uint private constant BNB_MAINNET_CHAINID = 56; + uint private constant BNB_TESTNET_CHAINID = 97; + uint private constant BNB_GAS_PRICE = 5 gwei; uint private constant BIT224 = 1 << 224; uint private constant BIT128 = 1 << 128; uint private constant BIT96 = 1 << 96; - //slot0 + //slot[0] address public /*immutable*/ swimUsdAddress; - //slot1 + //slot[1] ITokenBridge public /*immutable*/ tokenBridge; - uint32 public wormholeNonce; //remaining slots PropellerFeeConfig public propellerFeeConfig; uint256[20] private reservedSlotsForAdditionalPropellerFeeRemunerationMethodConfigs; @@ -83,7 +79,6 @@ contract Routing is __Pausable_init(); __Ownable_init(); _transferOwnership(owner_); - wormholeNonce = 0; tokenBridge = ITokenBridge(tokenBridgeAddress); swimUsdAddress = tokenBridge.wrappedAsset(WORMHOLE_SOLANA_CHAIN_ID, SWIM_USD_SOLANA_ADDRESS); if (swimUsdAddress == address(0)) @@ -181,7 +176,45 @@ contract Routing is inputAmount, firstMinimumOutputAmount, wormholeRecipientChain, - toOwner + toOwner, + 0 //wormholeNonce + ); + } + + function crossChainInitiate( + address fromToken, + uint inputAmount, + uint firstMinimumOutputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + bytes16 memo + ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { + (swimUsdAmount, wormholeSequence) = _crossChainInitiate( + fromToken, + inputAmount, + firstMinimumOutputAmount, + wormholeRecipientChain, + toOwner, + 0 //wormholeNonce + ); + emit MemoInteraction(memo); + } + + function crossChainInitiate( + address fromToken, + uint inputAmount, + uint firstMinimumOutputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + uint32 wormholeNonce + ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { + (swimUsdAmount, wormholeSequence) = _crossChainInitiate( + fromToken, + inputAmount, + firstMinimumOutputAmount, + wormholeRecipientChain, + toOwner, + wormholeNonce ); } @@ -191,6 +224,7 @@ contract Routing is uint firstMinimumOutputAmount, uint16 wormholeRecipientChain, bytes32 toOwner, + uint32 wormholeNonce, bytes16 memo ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { (swimUsdAmount, wormholeSequence) = _crossChainInitiate( @@ -198,7 +232,8 @@ contract Routing is inputAmount, firstMinimumOutputAmount, wormholeRecipientChain, - toOwner + toOwner, + wormholeNonce ); emit MemoInteraction(memo); } @@ -208,7 +243,8 @@ contract Routing is uint inputAmount, uint firstMinimumOutputAmount, uint16 wormholeRecipientChain, - bytes32 toOwner + bytes32 toOwner, + uint32 wormholeNonce ) internal returns (uint swimUsdAmount, uint64 wormholeSequence) { address swimUsdAddress_ = swimUsdAddress; swimUsdAmount = acquireAndMaybeSwap( @@ -243,6 +279,7 @@ contract Routing is swimUsdAmount, wormholeRecipientChain, SwimPayloadConversion.encode(toOwner), + wormholeNonce, swimUsdAddress_ ); // } @@ -256,7 +293,54 @@ contract Routing is bool gasKickstart, uint64 maxPropellerFee, uint16 toTokenNumber - ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence) { + ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { + (swimUsdAmount, wormholeSequence) = _propellerInitiate( + fromToken, + inputAmount, + wormholeRecipientChain, + toOwner, + gasKickstart, + maxPropellerFee, + toTokenNumber, + 0, //wormholeNonce + bytes16(0) + ); + } + + function propellerInitiate( + address fromToken, + uint inputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + bool gasKickstart, + uint64 maxPropellerFee, + uint16 toTokenNumber, + bytes16 memo + ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { + (swimUsdAmount, wormholeSequence) = _propellerInitiate( + fromToken, + inputAmount, + wormholeRecipientChain, + toOwner, + gasKickstart, + maxPropellerFee, + toTokenNumber, + 0, //wormholeNonce + memo + ); + emit MemoInteraction(memo); + } + + function propellerInitiate( + address fromToken, + uint inputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + bool gasKickstart, + uint64 maxPropellerFee, + uint16 toTokenNumber, + uint32 wormholeNonce + ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { (swimUsdAmount, wormholeSequence) = _propellerInitiate( fromToken, inputAmount, @@ -265,6 +349,7 @@ contract Routing is gasKickstart, maxPropellerFee, toTokenNumber, + wormholeNonce, bytes16(0) ); } @@ -277,6 +362,7 @@ contract Routing is bool gasKickstart, uint64 maxPropellerFee, uint16 toTokenNumber, + uint32 wormholeNonce, bytes16 memo ) external payable whenNotPaused returns (uint swimUsdAmount, uint64 wormholeSequence) { (swimUsdAmount, wormholeSequence) = _propellerInitiate( @@ -287,6 +373,7 @@ contract Routing is gasKickstart, maxPropellerFee, toTokenNumber, + wormholeNonce, memo ); emit MemoInteraction(memo); @@ -300,6 +387,7 @@ contract Routing is bool gasKickstart, uint64 maxPropellerFee, uint16 toTokenNumber, + uint32 wormholeNonce, bytes16 memo ) internal returns (uint swimUsdAmount, uint64 wormholeSequence) { address swimUsdAddress_ = swimUsdAddress; @@ -313,6 +401,7 @@ contract Routing is swimUsdAmount, wormholeRecipientChain, encodedPayload, + wormholeNonce, swimUsdAddress_ ); } @@ -376,6 +465,9 @@ contract Routing is (uint swimUsdAmount, SwimPayload memory swimPayload) = wormholeCompleteTransfer(encodedVm); address swimUsdAddress_ = swimUsdAddress; + if (!swimPayload.propellerEnabled) + revert NotAPropellerTransaction(); + if (swimPayload.gasKickstart) { if (msg.value != GAS_KICKSTART_AMOUNT) revert IncorrectMessageValue(msg.value, GAS_KICKSTART_AMOUNT); @@ -393,12 +485,16 @@ contract Routing is toTokenInfo = tokenNumberMapping[swimPayload.toTokenNumber]; if (toTokenInfo.tokenNumber != 0) { //if toTokenNumber in swimPayload was invalid for whatever reason then just return - //swimUsd to prevent propeller transaction from getting stuck + // swimUsd to prevent propeller transaction from getting stuck toTokenNumber = toTokenInfo.tokenNumber; //same as swimPayload.toTokenNumber outputToken = toTokenInfo.tokenAddress; } } + //emit here instead of at the end to have it included in dynamic gas cost calculation + if (swimPayload.memo != bytes16(0)) + emit MemoInteraction(swimPayload.memo); + uint swimUsdPerGasToken = propellerFeeConfig.method == GasTokenPriceMethod.UniswapOracle ? swimUsdPerGasTokenUniswap() : propellerFeeConfig.fixedSwimUsdPerGasToken; @@ -433,14 +529,10 @@ contract Routing is 0 //no slippage for propeller ) : swimUsdAmount - feeAmount; - IERC20(outputToken).safeTransfer(swimPayload.toOwner, outputAmount); } else outputAmount = 0; - - if (swimPayload.memo != bytes16(0)) - emit MemoInteraction(swimPayload.memo); }} // ----------------------------- ENGINE ---------------------------------------------------------- @@ -458,6 +550,9 @@ contract Routing is address tokenAddress, address poolAddress ) external onlyOwner { + if (tokenNumber == 0 || tokenAddress == address(0) || poolAddress == address(0)) + revert InvalidZeroValue(); + uint8 tokenIndexInPool = 0; PoolState memory state = IPool(poolAddress).getState(); //skip first token because it's always swimUSD @@ -476,6 +571,13 @@ contract Routing is token.poolAddress = poolAddress; token.tokenIndexInPool = tokenIndexInPool; + address oldTokenAddress = tokenNumberMapping[tokenNumber].tokenAddress; + uint16 oldTokenNumber = tokenAddressMapping[tokenAddress].tokenNumber; + if (oldTokenAddress != address(0) && oldTokenAddress != tokenAddress) + delete tokenAddressMapping[oldTokenAddress]; + if (oldTokenNumber != 0 && oldTokenNumber != tokenNumber) + delete tokenNumberMapping[oldTokenNumber]; + tokenNumberMapping[tokenNumber] = token; tokenAddressMapping[tokenAddress] = token; @@ -484,13 +586,19 @@ contract Routing is function adjustPropellerServiceFee(uint64 serviceFee) external onlyOwner { propellerFeeConfig.serviceFee = serviceFee; + + emit PropellerServiceFeeChanged(serviceFee); } function usePropellerFixedGasTokenPrice( - uint fixedSwimUsdPerGasToken + Decimal calldata fixedSwimUsdPerGasToken //atomic swimUsd per wei ETH ) external onlyOwner { - propellerFeeConfig.method = GasTokenPriceMethod.FixedPrice; - propellerFeeConfig.fixedSwimUsdPerGasToken = fixedSwimUsdPerGasToken; + updatePropellerGasTokenPriceMethod(GasTokenPriceMethod.FixedPrice); + propellerFeeConfig.fixedSwimUsdPerGasToken = fixedSwimUsdPerGasToken.decimals < PRECISION + ? fixedSwimUsdPerGasToken.value * 10 ** (PRECISION - fixedSwimUsdPerGasToken.decimals) + : fixedSwimUsdPerGasToken.value / 10 ** (fixedSwimUsdPerGasToken.decimals - PRECISION); + + emit PropellerFixedSwimUsdPerGasTokenChanged(fixedSwimUsdPerGasToken); } function usePropellerUniswapOracle( @@ -498,7 +606,6 @@ contract Routing is address uniswapPoolAddress ) external onlyOwner { (IPool swimPool, uint8 swimIntermediateIndex) = getPoolAndIndex(intermediateToken); - IUniswapV3Pool uniswapPool = IUniswapV3Pool(uniswapPoolAddress); bool uniswapIntermediateIsFirst; @@ -509,11 +616,13 @@ contract Routing is else revert TokenNotInPool(intermediateToken, uniswapPoolAddress); - propellerFeeConfig.method = GasTokenPriceMethod.UniswapOracle; + updatePropellerGasTokenPriceMethod(GasTokenPriceMethod.UniswapOracle); propellerFeeConfig.uniswap.swimPool = swimPool; propellerFeeConfig.uniswap.swimIntermediateIndex = swimIntermediateIndex; propellerFeeConfig.uniswap.uniswapPool = uniswapPool; propellerFeeConfig.uniswap.uniswapIntermediateIsFirst = uniswapIntermediateIsFirst; + + emit PropellerUniswapFeeConfigChanged(intermediateToken, uniswapPoolAddress); } function pause() public onlyOwner { @@ -565,6 +674,7 @@ contract Routing is uint swimUsdAmount, uint16 wormholeRecipientChain, bytes memory swimPayload, + uint32 wormholeNonce, address swimUsdAddress_ ) internal returns (uint64 wormholeSequence) { IERC20(swimUsdAddress_).safeApprove(address(tokenBridge), swimUsdAmount); @@ -585,10 +695,12 @@ contract Routing is returns (uint64 wormholeSequence_) { wormholeSequence = wormholeSequence_; } + catch Panic(uint errorCode) { + revert WormholeInteractionFailed(abi.encode(errorCode)); + } catch (bytes memory lowLevelData) { revert WormholeInteractionFailed(lowLevelData); } - ++wormholeNonce; } function wormholeCompleteTransfer( @@ -612,14 +724,24 @@ contract Routing is WORMHOLE_SOLANA_CHAIN_ID ); } + catch Panic(uint errorCode) { + revert WormholeInteractionFailed(abi.encode(errorCode)); + } catch (bytes memory lowLevelData) { revert WormholeInteractionFailed(lowLevelData); } } + function updatePropellerGasTokenPriceMethod(GasTokenPriceMethod newMethod) internal { + if (propellerFeeConfig.method != newMethod) { + propellerFeeConfig.method = newMethod; + emit PropellerGasTokenPriceMethodChanged(newMethod); + } + } + // ----------------------------- INTERNAL VIEW/PURE ---------------------------------------------- - // @return swimUsdPerGasToken with 18 decimals + // @return swimUsdPerGasToken with PRECISION decimals function swimUsdPerGasTokenUniswap() internal view returns (uint swimUsdPerGasToken) { unchecked { //this function is a bit of a clusterfuck due to the wide range of prices that can // _theoretically_ be returned by our oracles (swimPool and Uniswap) and because uniswap pools @@ -696,7 +818,14 @@ contract Routing is uint consumedGas = startGas; unchecked { - uint remuneratedGasPrice = block.basefee + PROPELLER_GAS_TIP; + uint remuneratedGasPrice = + block.chainid == BNB_MAINNET_CHAINID || block.chainid == BNB_TESTNET_CHAINID + ? BNB_GAS_PRICE + : block.basefee + PROPELLER_GAS_TIP; + + //gas is not an intended source of profit for engines + if (remuneratedGasPrice > tx.gasprice) + remuneratedGasPrice = tx.gasprice; consumedGas += GAS_COST_BASE; if (swimPayload.toTokenNumber != SWIM_USD_TOKEN_NUMBER) @@ -707,8 +836,7 @@ contract Routing is consumedGas -= gasleft(); gasTokenCost += remuneratedGasPrice * consumedGas; } - - swimUsdGasFee = (gasTokenCost * swimUsdPerGasToken) / MARGINAL_PRICE_MULTIPLIER; //SafeMath! + swimUsdGasFee = (gasTokenCost * swimUsdPerGasToken) / PRECISION_MULTIPLIER; //SafeMath! } function getPoolAndIndex(address token) internal view returns (IPool, uint8) { diff --git a/packages/evm-contracts/contracts/interfaces/Decimal.sol b/packages/evm-contracts/contracts/interfaces/Decimal.sol new file mode 100644 index 000000000..4c29a8014 --- /dev/null +++ b/packages/evm-contracts/contracts/interfaces/Decimal.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.15; + +struct Decimal { + uint value; + uint8 decimals; +} diff --git a/packages/evm-contracts/contracts/interfaces/IPool.sol b/packages/evm-contracts/contracts/interfaces/IPool.sol index 0a1943d11..a4d6c1f1f 100644 --- a/packages/evm-contracts/contracts/interfaces/IPool.sol +++ b/packages/evm-contracts/contracts/interfaces/IPool.sol @@ -1,31 +1,15 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.15; +import "./Decimal.sol"; +import "./PoolState.sol"; import "./IMemoInteractor.sol"; -struct TokenBalance { - address tokenAddress; - uint balance; -} - -struct Decimal { - uint value; - uint8 decimals; -} - -struct PoolState { - bool paused; - TokenBalance[] balances; - TokenBalance totalLpSupply; - Decimal ampFactor; - Decimal lpFee; - Decimal governanceFee; -} - interface IPool is IMemoInteractor { event Paused(bool paused); - event TransferGovernance(address indexed from, address indexed to); - event ChangeGovernanceFeeRecipient(address indexed governanceFeeRecepient); + event GovernanceChanged(address indexed from, address indexed to); + event GovernanceFeeRecipientChanged(address indexed governanceFeeRecepient); + event FeesChanged(Decimal lpFee, Decimal governanceFee); //governance errors: error LpTokenInitializationFailed(bytes lowLevelData); diff --git a/packages/evm-contracts/contracts/interfaces/IRouting.sol b/packages/evm-contracts/contracts/interfaces/IRouting.sol index f0c4eea3a..a1cc30cc9 100644 --- a/packages/evm-contracts/contracts/interfaces/IRouting.sol +++ b/packages/evm-contracts/contracts/interfaces/IRouting.sol @@ -1,27 +1,38 @@ //SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.15; +import "./Decimal.sol"; +import "./PoolState.sol"; import "./IMemoInteractor.sol"; -import "./IPool.sol"; interface IRouting is IMemoInteractor { - event TokenRegistered(uint16 indexed tokenNumber, address indexed token, address pool); + struct TokenInfo { + uint16 tokenNumber; + address tokenAddress; + address poolAddress; + uint8 tokenIndexInPool; + } + + enum GasTokenPriceMethod { + FixedPrice, + UniswapOracle + } enum CodeLocation { DetermineGasCostViaUniswap1, DetermineGasCostViaUniswap2 } + error NumericError(CodeLocation location, bytes data); + error NotAPropellerTransaction(); error SwimUsdNotAttested(); + error WormholeInteractionFailed(bytes lowLevelData); + error GasKickstartFailed(address owner); error TokenNotInPool(address passedToken, address pool); error SenderIsNotOwner(address sender, address owner); - error TokenNotRegistered(bytes20 addressOrTokenNumber); - error WormholeInteractionFailed(bytes lowLevelData); - error TooSmallForPropeller(uint swimUsdAmount, uint propellerMinimumThreshold); error IncorrectMessageValue(uint value, uint expected); - error NumericError(CodeLocation location, bytes data); - error GasKickstartFailed(address owner); - error ExcessivePropellerFee(uint propellerFee); + error TokenNotRegistered(bytes20 addressOrTokenNumber); + error InvalidZeroValue(); error InvalidWormholeToken( bytes32 originAddress, uint16 originChain, @@ -29,7 +40,14 @@ interface IRouting is IMemoInteractor { uint16 expectedChain ); + event TokenRegistered(uint16 indexed tokenNumber, address indexed token, address pool); + event PropellerServiceFeeChanged(uint serviceFee); + event PropellerGasTokenPriceMethodChanged(GasTokenPriceMethod latest); + event PropellerFixedSwimUsdPerGasTokenChanged(Decimal fixedSwimUsdPerGasToken); + event PropellerUniswapFeeConfigChanged(address intermediateToken, address uniswapPool); + function swimUsdAddress() external view returns (address); + function engineFees(address engine) external view returns (uint); function getPoolStates(address[] memory poolAddresses) external view returns (PoolState[] memory); @@ -67,6 +85,25 @@ interface IRouting is IMemoInteractor { bytes16 memo ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + function crossChainInitiate( + address fromToken, + uint inputAmount, + uint firstMinimumOutputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + uint32 wormholeNonce + ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + + function crossChainInitiate( + address fromToken, + uint inputAmount, + uint firstMinimumOutputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + uint32 wormholeNonce, + bytes16 memo + ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + function propellerInitiate( address fromToken, uint inputAmount, @@ -88,6 +125,29 @@ interface IRouting is IMemoInteractor { bytes16 memo ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + function propellerInitiate( + address fromToken, + uint inputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + bool gasKickstart, + uint64 maxPropellerFee, + uint16 toTokenNumber, + uint32 wormholeNonce + ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + + function propellerInitiate( + address fromToken, + uint inputAmount, + uint16 wormholeRecipientChain, + bytes32 toOwner, + bool gasKickstart, + uint64 maxPropellerFee, + uint16 toTokenNumber, + uint32 wormholeNonce, + bytes16 memo + ) external payable returns (uint swimUsdAmount, uint64 wormholeSequence); + function crossChainComplete( bytes memory encodedVm, address toToken, @@ -116,8 +176,7 @@ interface IRouting is IMemoInteractor { function adjustPropellerServiceFee(uint64 serviceFee) external; - //swimUsdPerGasToken in 18 decimals - function usePropellerFixedGasTokenPrice(uint fixedSwimUsdPerGasToken) external; + function usePropellerFixedGasTokenPrice(Decimal calldata fixedSwimUsdPerGasToken) external; function usePropellerUniswapOracle( address intermediateToken, diff --git a/packages/evm-contracts/contracts/interfaces/PoolState.sol b/packages/evm-contracts/contracts/interfaces/PoolState.sol new file mode 100644 index 000000000..a9c7a194a --- /dev/null +++ b/packages/evm-contracts/contracts/interfaces/PoolState.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.15; + +import "./Decimal.sol"; + +struct TokenBalance { + address tokenAddress; + uint balance; +} + +struct PoolState { + bool paused; + TokenBalance[] balances; + TokenBalance totalLpSupply; + Decimal ampFactor; + Decimal lpFee; + Decimal governanceFee; +} diff --git a/packages/evm-contracts/hardhat.config.ts b/packages/evm-contracts/hardhat.config.ts index 9cd7e8b71..f84adf439 100644 --- a/packages/evm-contracts/hardhat.config.ts +++ b/packages/evm-contracts/hardhat.config.ts @@ -12,10 +12,11 @@ import { task } from "hardhat/config"; import type { HardhatUserConfig, HttpNetworkUserConfig } from "hardhat/types"; dotenv.config(); +//update .env.examples if you add additional environment variables! const { FACTORY_MNEMONIC, MNEMONIC, ETHERSCAN_API_KEY } = process.env; task( - "factoryAddress", + "factory-address", "Prints the address the SwimFactory will be deployed to given the FACTORY_MNEMONIC", // eslint-disable-next-line @typescript-eslint/require-await async (_, { ethers }) => { @@ -38,6 +39,7 @@ task( async ({ proxy, logic, owner }, hre) => { const { ethers } = hre; const _owner = owner ? await ethers.getSigner(owner as string) : (await ethers.getSigners())[0]; + //TODO check that proxy and logic exist const _proxy = (await ethers.getContractAt("BlankLogic", proxy as string)).connect(_owner); await (await _proxy.upgradeTo(logic as string)).wait(); } @@ -49,7 +51,7 @@ task( ) .addOptionalPositionalParam("owner", "owner who's authorized to execute the upgrade", ""); -task("poolState", "Print state of given pool", async ({ pool }, { ethers }) => { +task("pool-state", "Print state of given pool", async ({ pool }, { ethers }) => { const [isPaused, balances, lpSupply, ampFactorDec, lpFeeDec, govFeeDec] = await ( await ethers.getContractAt("Pool", pool as string) ).getState(); @@ -117,6 +119,7 @@ const config: HardhatUserConfig = { solidity: { version: "0.8.15", settings: { + viaIR: true, optimizer: { enabled: true, runs: 1000, // Optimize heavily for runtime gas cost rather than deployment gas cost @@ -128,7 +131,7 @@ const config: HardhatUserConfig = { "evm.bytecode", "evm.bytecode.sourceMap", //"ir", - //"irOptimized", + "irOptimized", "evm.assembly", ], // "": ["ast"], @@ -159,6 +162,24 @@ const config: HardhatUserConfig = { chainId: 97, ...sharedNetworkConfig, }, + avalanchetestnet: { + //https://avalanche--fuji--rpc.datahub.figment.io/apikey/fa3f07b34f4acd63c50e8a965ae62e1c + //url: "https://morning-proud-borough.avalanche-testnet.quiknode.pro/5d786c70bfeb06e9d120aedc93bbe02f7d2fbcd6/ext/bc/C/rpc", + url: "https://api.avax-test.network/ext/bc/C/rpc", + chainId: 43113, + ...sharedNetworkConfig, + }, + polygontestnet: { + //mumbai + url: "https://apis.ankr.com/93e2796ab57a416c955d169d2468ddaa/40368bdfe11e91019e93b8797c65a1f3/polygon/full/test", + chainId: 80001, + ...sharedNetworkConfig, + }, + fantomtestnet: { + url: "https://rpc.testnet.fantom.network/", + chainId: 4002, + ...sharedNetworkConfig, + }, }, gasReporter: { enabled: true, diff --git a/packages/evm-contracts/src/config.ts b/packages/evm-contracts/src/config.ts index 902e8aeb7..6e285a3d6 100644 --- a/packages/evm-contracts/src/config.ts +++ b/packages/evm-contracts/src/config.ts @@ -51,13 +51,23 @@ export type ChainConfig = { readonly pools?: readonly PoolConfig[]; }; -export const SWIM_FACTORY_ADDRESS = "0x36E284788aaA29C16cc227E09477C8e73D96ffD3"; -export const SWIM_USD_SOLANA_ADDRESS = - "0x296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe26719"; -export const ROUTING_CONTRACT_SOLANA_ADDRESS = "0x" + "00".repeat(32); //TBD +//"fixed" constants (either can't be changed by us or implementation critically relies on them): export const WORMHOLE_SOLANA_CHAIN_ID = 1; export const POOL_PRECISION = 6; +export const ROUTING_PRECISION = 18; export const SWIM_USD_DECIMALS = 6; +export const SWIM_USD_TOKEN_INDEX = 0; +export const SWIM_USD_TOKEN_NUMBER = 0; + +//"variable" constants: +export const SWIM_FACTORY_ADDRESS = "0xDef312467D48bdDED813de11C3ee4c257e6eD7aD"; +export const ROUTING_CONTRACT_SOLANA_ADDRESS = + "0x857d8c691b9e9a1a1e98d010a36d6401a9099ce89d821751410623ad7c2a20d2"; +export const SWIM_USD_SOLANA_ADDRESS = + "0x296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe26719"; +export const PROPELLER_GAS_TIP = "10000000000"; //i.e. 1e9, i.e. 1 gwei + +export const GAS_TOKEN_DECIMALS = 18; //1e18 wei in 1 ETH or BNB, or AVAX, ... export const TOKEN_NUMBERS: Record = { swimUSD: 0, @@ -156,8 +166,67 @@ const BNB_TESTNET = { ], }; +const AVALANCHE_TESTNET = { + name: "Avalanche Fuji Testnet", + routing: { + wormholeTokenBridge: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756", + }, + pools: [ + { + salt: "0x" + "00".repeat(31) + "01", + lpSalt: "0x" + "00".repeat(31) + "11", + lpName: "Test Pool LP", + lpSymbol: "LP", + tokens: [ + { symbol: "USDC" as const, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, + { symbol: "USDT" as const, address: "0x489dDcd070b6c4e0373FBB5d529Cc06328E048c3" }, + ], + }, + ], +}; + +const POLYGON_TESTNET = { + name: "Polygon Mumbai Testnet", + routing: { + wormholeTokenBridge: "0x377D55a7928c046E18eEbb61977e714d2a76472a", + }, + pools: [ + { + salt: "0x" + "00".repeat(31) + "01", + lpSalt: "0x" + "00".repeat(31) + "11", + lpName: "Test Pool LP", + lpSymbol: "LP", + tokens: [ + { symbol: "USDC" as const, address: "0x0a0d7cEA57faCBf5DBD0D3b5169Ab00AC8Cf7dd1" }, + { symbol: "USDT" as const, address: "0x2Ac9183EC64F71AfB73909c7C028Db14d35FAD2F" }, + ], + }, + ], +}; + +const FANTOM_TESTNET = { + name: "Fantom Testnet", + routing: { + wormholeTokenBridge: "0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8", + }, + pools: [ + { + salt: "0x" + "00".repeat(31) + "01", + lpSalt: "0x" + "00".repeat(31) + "11", + lpName: "Test Pool LP", + lpSymbol: "LP", + tokens: [ + { symbol: "USDC" as const, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, + ], + }, + ], +}; + export const CHAINS: { readonly [chainId: number]: ChainConfig | undefined } = { 5: GOERLI, 97: BNB_TESTNET, 31337: LOCAL, + 43113: AVALANCHE_TESTNET, + 80001: POLYGON_TESTNET, + 4002: FANTOM_TESTNET, }; diff --git a/packages/evm-contracts/src/presigned.ts b/packages/evm-contracts/src/presigned.ts index f563bca66..2c19d6002 100644 --- a/packages/evm-contracts/src/presigned.ts +++ b/packages/evm-contracts/src/presigned.ts @@ -1,2 +1,2 @@ export const HARDHAT_FACTORY_PRESIGNED = - "0x02f91b02827a69808459682f0084d09dc3008317530e8080b91aa7608060405234801561001057600080fd5b50604051611a87380380611a8783398101604081905261002f916100a6565b600080546001600160a01b0319166001600160a01b03831617905560405161005690610099565b604051809103906000f080158015610072573d6000803e3d6000fd5b50600280546001600160a01b0319166001600160a01b0392909216919091179055506100d6565b610ad780610fb083390190565b6000602082840312156100b857600080fd5b81516001600160a01b03811681146100cf57600080fd5b9392505050565b610ecb806100e56000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80638da5cb5b1161005b5780638da5cb5b1461014f578063ae7f00de14610162578063dda39c0814610175578063f2fde38b1461018857600080fd5b80630492e4931461008257806330575892146100b157806383dfd8951461013c575b600080fd5b610095610090366004610b89565b61019d565b6040516001600160a01b03909116815260200160405180910390f35b6100956100bf366004610c45565b8151602092830120604080517fff00000000000000000000000000000000000000000000000000000000000000818601526bffffffffffffffffffffffff193060601b166021820152603581019390935260558084019290925280518084039092018252607590920190915280519101206001600160a01b031690565b61009561014a366004610c8a565b610229565b600054610095906001600160a01b031681565b610095610170366004610c45565b6103a1565b610095610183366004610d13565b610476565b61019b610196366004610d60565b610628565b005b60006102236101aa6106d6565b8051602091820120604080517fff00000000000000000000000000000000000000000000000000000000000000818501523060601b6bffffffffffffffffffffffff191660218201526035810187905260558082019390935281518082039093018352607501905280519101206001600160a01b031690565b92915050565b600080546001600160a01b031633148061024557506000600154115b6102875760405162461bcd60e51b815260206004820152600e60248201526d139bdd08105d5d1a1bdc9a5e995960921b60448201526064015b60405180910390fd5b60016000815461029690610d98565b9091555060006102a68585610b1e565b9050600080826001600160a01b0316856040516102c39190610de1565b6000604051808303816000865af19150503d8060008114610300576040519150601f19603f3d011682016040523d82523d6000602084013e610305565b606091505b50915091508161034357806040517f1f292e7200000000000000000000000000000000000000000000000000000000815260040161027e9190610e29565b604051600081526001600160a01b038416907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a2509091505060016000815461039690610e3c565b909155509392505050565b600080546001600160a01b03163314806103bd57506000600154115b6103fa5760405162461bcd60e51b815260206004820152600e60248201526d139bdd08105d5d1a1bdc9a5e995960921b604482015260640161027e565b60016000815461040990610d98565b9091555060006104198484610b1e565b604051600081529091506001600160a01b038216907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a2905060016000815461046c90610e3c565b9091555092915050565b600080546001600160a01b031633148061049257506000600154115b6104cf5760405162461bcd60e51b815260206004820152600e60248201526d139bdd08105d5d1a1bdc9a5e995960921b604482015260640161027e565b6001600081546104de90610d98565b9091555060006104ec6106d6565b905060006104fa8286610b1e565b6040517f4f1ef2860000000000000000000000000000000000000000000000000000000081529091506001600160a01b03821690634f1ef286906105449089908890600401610e53565b600060405180830381600087803b15801561055e57600080fd5b505af192505050801561056f575060015b6105d7573d80801561059d576040519150601f19603f3d011682016040523d82523d6000602084013e6105a2565b606091505b50806040517f43adf0b800000000000000000000000000000000000000000000000000000000815260040161027e9190610e29565b604051600181526001600160a01b038216907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a291505060016000815461039690610e3c565b6000546001600160a01b031633146106735760405162461bcd60e51b815260206004820152600e60248201526d139bdd08105d5d1a1bdc9a5e995960921b604482015260640161027e565b600080547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b0383169081178255604051909133917f5c486528ec3e3f0ea91181cff8116f02bfa350e03b8b6f12e00765adbb5af85c9190a350565b6002546060906044906102fa907f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc906001600160a01b0316600061071a8486610e7d565b67ffffffffffffffff81111561073257610732610ba2565b6040519080825280601f01601f19166020018201604052801561075c576020820181803683370190505b5060589290921b7f7300000000000000000000000000000000000000007f0000000000000000000001602083015250603681019190915260c89290921b60e09190911b017f556100008060006000396000f3000000000000000000000000000000000000000160568201527f60806040523661001357610011610017565b005b6100115b610027610022610060648201527f74565b6100b9565b565b606061004e838360405180606001604052806027815260848201527f6020016102fb602791396100dd565b9392505050565b73ffffffffffffffffff60a48201527fffffffffffffffffffffff163b151590565b90565b60006100b47f360894a13b60c48201527fa1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5473ffffff60e48201526ee96fa9a46faf6fa9a4c99fff7fc89f196101048201527e80366000845af43d6000803e8080156100d8573d6000f35b3d6000fd5b60606101248201527f73ffffffffffffffffffffffffffffffffffffffff84163b610188576040517f61014482015262461bcd60e51b6101648201527f815260206004820152602660248201527f416464726573733a2064656c6567616101848201527f74652063616c6c20746f206e6f6e2d636f60448201527f6e74726163740000006101a48201526860648201526084015b6101c48201527f60405180910390fd5b6000808573ffffffffffffffffffffffffffffffffffff6101e48201527fffff16856040516101b0919061028d565b600060405180830381855af49150506102048201527f3d80600081146101eb576040519150601f19603f3d011682016040523d82523d6102248201527f6000602084013e6101f0565b606091505b509150915061020082828661020a566102448201527f5b9695505050505050565b6060831561021957508161004e565b8251156102296102648201527f5782518084602001fd5b816040517f08c379a00000000000000000000000000061028482015270815260040161017f91906102a9565b60006102a48201527f5b83811015610278578181015183820152602001610260565b838111156102876102c48201527f576000848401525b50505050565b6000825161029f81846020870161025d565b6102e48201527f9190910192915050565b60208152600082518060208401526102c881604085016103048201527f6020870161025d565b601f017fffffffffffffffffffffffffffffffffffffff6103248201527fffffffffffffffffffffffffe016919091016040019291505056000000000000610344820152919050565b6000808390506000839050600080828451602086016000f5915050803b158015610b7f576040517fc56443730000000000000000000000000000000000000000000000000000000081526001600160a01b038316600482015260240161027e565b5095945050505050565b600060208284031215610b9b57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b600082601f830112610bc957600080fd5b813567ffffffffffffffff80821115610be457610be4610ba2565b604051601f8301601f19908116603f01168101908282118183101715610c0c57610c0c610ba2565b81604052838152866020858801011115610c2557600080fd5b836020870160208301376000602085830101528094505050505092915050565b60008060408385031215610c5857600080fd5b823567ffffffffffffffff811115610c6f57600080fd5b610c7b85828601610bb8565b95602094909401359450505050565b600080600060608486031215610c9f57600080fd5b833567ffffffffffffffff80821115610cb757600080fd5b610cc387838801610bb8565b9450602086013593506040860135915080821115610ce057600080fd5b50610ced86828701610bb8565b9150509250925092565b80356001600160a01b0381168114610d0e57600080fd5b919050565b600080600060608486031215610d2857600080fd5b610d3184610cf7565b925060208401359150604084013567ffffffffffffffff811115610d5457600080fd5b610ced86828701610bb8565b600060208284031215610d7257600080fd5b610d7b82610cf7565b9392505050565b634e487b7160e01b600052601160045260246000fd5b600060018201610daa57610daa610d82565b5060010190565b60005b83811015610dcc578181015183820152602001610db4565b83811115610ddb576000848401525b50505050565b60008251610df3818460208701610db1565b9190910192915050565b60008151808452610e15816020860160208601610db1565b601f01601f19169290920160200192915050565b602081526000610d7b6020830184610dfd565b600081610e4b57610e4b610d82565b506000190190565b6001600160a01b0383168152604060208201526000610e756040830184610dfd565b949350505050565b60008219821115610e9057610e90610d82565b50019056fea26469706673582212209e377f6a3d9bd526f1c6d1b78a0a243fe6626152dda805d48fc318ecf46b88ee64736f6c634300080f003360a06040523060805234801561001457600080fd5b50608051610a8c61004b60003960008181609f01528181610129015281816102160152818161029b015261037c0152610a8c6000f3fe6080604052600436106100345760003560e01c80633659cfe6146100395780634f1ef2861461005b57806352d1902d1461006e575b600080fd5b34801561004557600080fd5b5061005961005436600461088f565b610095565b005b6100596100693660046108d9565b61020c565b34801561007a57600080fd5b5061008361036f565b60405190815260200160405180910390f35b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001630036101275760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b19195b1959d85d1958d85b1b60a21b60648201526084015b60405180910390fd5b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166101827f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b6001600160a01b0316146101ed5760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b6163746976652070726f787960a01b606482015260840161011e565b6040805160008082526020820190925261020991839190610434565b50565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001630036102995760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b19195b1959d85d1958d85b1b60a21b606482015260840161011e565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166102f47f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b6001600160a01b03161461035f5760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b6163746976652070726f787960a01b606482015260840161011e565b61036b82826001610434565b5050565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461040f5760405162461bcd60e51b815260206004820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c0000000000000000606482015260840161011e565b507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc90565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff161561046c57610467836105d9565b505050565b826001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156104c6575060408051601f3d908101601f191682019092526104c39181019061099b565b60015b6105385760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f742055555053000000000000000000000000000000000000606482015260840161011e565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81146105cd5760405162461bcd60e51b815260206004820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c65555549440000000000000000000000000000000000000000000000606482015260840161011e565b506104678383836106af565b6001600160a01b0381163b6106565760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e747261637400000000000000000000000000000000000000606482015260840161011e565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b0392909216919091179055565b6106b8836106da565b6000825111806106c55750805b15610467576106d4838361071a565b50505050565b6106e3816105d9565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606061073f8383604051806060016040528060278152602001610a3060279139610746565b9392505050565b60606001600160a01b0384163b6107c55760405162461bcd60e51b815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e74726163740000000000000000000000000000000000000000000000000000606482015260840161011e565b600080856001600160a01b0316856040516107e091906109e0565b600060405180830381855af49150503d806000811461081b576040519150601f19603f3d011682016040523d82523d6000602084013e610820565b606091505b509150915061083082828661083a565b9695505050505050565b6060831561084957508161073f565b8251156108595782518084602001fd5b8160405162461bcd60e51b815260040161011e91906109fc565b80356001600160a01b038116811461088a57600080fd5b919050565b6000602082840312156108a157600080fd5b61073f82610873565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600080604083850312156108ec57600080fd5b6108f583610873565b9150602083013567ffffffffffffffff8082111561091257600080fd5b818501915085601f83011261092657600080fd5b813581811115610938576109386108aa565b604051601f8201601f19908116603f01168101908382118183101715610960576109606108aa565b8160405282815288602084870101111561097957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b6000602082840312156109ad57600080fd5b5051919050565b60005b838110156109cf5781810151838201526020016109b7565b838111156106d45750506000910152565b600082516109f28184602087016109b4565b9190910192915050565b6020815260008251806020840152610a1b8160408501602087016109b4565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220f65dfc1e543852973e209c16dccbcb8fb791caf1d019ba3ed04c669254c784ec64736f6c634300080f0033000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266c001a02aa9425990f0df66862ff58146870158ef96156e49e32bf1b26a67ecf3254cb2a0659c9603ade4f06a6ab4b0170658d098c9cd9aa93ed36be6f93a866d112ad9d7"; + "0x02f91bbd827a69808459682f0084d09dc30083179db68080b91b6260a060405234801561001057600080fd5b50604051611b42380380611b4283398101604081905261002f91610092565b600080546001600160a01b0319166001600160a01b03831617905560405161005690610085565b604051809103906000f080158015610072573d6000803e3d6000fd5b506001600160a01b0316608052506100c2565b610ad78061106b83390190565b6000602082840312156100a457600080fd5b81516001600160a01b03811681146100bb57600080fd5b9392505050565b608051610f8e6100dd60003960006107160152610f8e6000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c80638da5cb5b1161005b5780638da5cb5b1461014f578063ae7f00de14610162578063dda39c0814610175578063f2fde38b1461018857600080fd5b80630492e4931461008257806330575892146100b157806383dfd8951461013c575b600080fd5b610095610090366004610bcb565b61019d565b6040516001600160a01b03909116815260200160405180910390f35b6100956100bf366004610bfa565b8151602092830120604080517fff00000000000000000000000000000000000000000000000000000000000000818601526bffffffffffffffffffffffff193060601b166021820152603581019390935260558084019290925280518084039092018252607590920190915280519101206001600160a01b031690565b61009561014a366004610cf8565b610229565b600054610095906001600160a01b031681565b610095610170366004610d72565b6103c4565b610095610183366004610dda565b6104b5565b61019b610196366004610e34565b610656565b005b60006102236101aa6106e4565b8051602091820120604080517fff00000000000000000000000000000000000000000000000000000000000000818501523060601b6bffffffffffffffffffffffff191660218201526035810187905260558082019390935281518082039093018352607501905280519101206001600160a01b031690565b92915050565b600080546001600160a01b031633148015906102455750600154155b15610263576040516379d1e58f60e01b815260040160405180910390fd5b60016000815461027290610e6c565b9190508190555060006102bc87878080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250899250610b60915050565b9050600080826001600160a01b031686866040516102db929190610e85565b6000604051808303816000865af19150503d8060008114610318576040519150601f19603f3d011682016040523d82523d6000602084013e61031d565b606091505b50915091508161036457806040517f1f292e7200000000000000000000000000000000000000000000000000000000815260040161035b9190610e95565b60405180910390fd5b604051600081526001600160a01b038416907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a250909150506001600081546103b790610eea565b9091555095945050505050565b600080546001600160a01b031633148015906103e05750600154155b156103fe576040516379d1e58f60e01b815260040160405180910390fd5b60016000815461040d90610e6c565b91905081905550600061045785858080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250879250610b60915050565b604051600081529091506001600160a01b038216907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a290506001600081546104aa90610eea565b909155509392505050565b600080546001600160a01b031633148015906104d15750600154155b156104ef576040516379d1e58f60e01b815260040160405180910390fd5b6001600081546104fe90610e6c565b90915550600061050c6106e4565b9050600061051a8287610b60565b6040517f4f1ef2860000000000000000000000000000000000000000000000000000000081529091506001600160a01b03821690634f1ef28690610566908a9089908990600401610f01565b600060405180830381600087803b15801561058057600080fd5b505af1925050508015610591575060015b6105f9573d8080156105bf576040519150601f19603f3d011682016040523d82523d6000602084013e6105c4565b606091505b50806040517f43adf0b800000000000000000000000000000000000000000000000000000000815260040161035b9190610e95565b604051600181526001600160a01b038216907f8bc3e5adcf79834694ea9a3bc347edb046015ae83ad0c26c4008921aed0ee31d9060200160405180910390a291505060016000815461064a90610eea565b90915550949350505050565b6000546001600160a01b03163314610681576040516379d1e58f60e01b815260040160405180910390fd5b600080547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b0383169081178255604051909133917f5c486528ec3e3f0ea91181cff8116f02bfa350e03b8b6f12e00765adbb5af85c9190a350565b606060446102fa7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001660006107428486610f40565b67ffffffffffffffff81111561075a5761075a610be4565b6040519080825280601f01601f191660200182016040528015610784576020820181803683370190505b5060589290921b7f7300000000000000000000000000000000000000007f0000000000000000000001602083015250603681019190915260c89290921b60e09190911b017f556100008060006000396000f3000000000000000000000000000000000000000160568201527f60806040523661001357610011610017565b005b6100115b610027610022610060648201527f74565b6100b9565b565b606061004e838360405180606001604052806027815260848201527f6020016102fb602791396100dd565b9392505050565b73ffffffffffffffffff60a48201527fffffffffffffffffffffff163b151590565b90565b60006100b47f360894a13b60c48201527fa1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc5473ffffff60e48201526ee96fa9a46faf6fa9a4c99fff7fc89f196101048201527e80366000845af43d6000803e8080156100d8573d6000f35b3d6000fd5b60606101248201527f73ffffffffffffffffffffffffffffffffffffffff84163b610188576040517f6101448201527f08c379a0000000000000000000000000000000000000000000000000000000006101648201527f815260206004820152602660248201527f416464726573733a2064656c6567616101848201527f74652063616c6c20746f206e6f6e2d636f60448201527f6e74726163740000006101a48201526860648201526084015b6101c48201527f60405180910390fd5b6000808573ffffffffffffffffffffffffffffffffffff6101e48201527fffff16856040516101b0919061028d565b600060405180830381855af49150506102048201527f3d80600081146101eb576040519150601f19603f3d011682016040523d82523d6102248201527f6000602084013e6101f0565b606091505b509150915061020082828661020a566102448201527f5b9695505050505050565b6060831561021957508161004e565b8251156102296102648201527f5782518084602001fd5b816040517f08c379a00000000000000000000000000061028482015270815260040161017f91906102a9565b60006102a48201527f5b83811015610278578181015183820152602001610260565b838111156102876102c48201527f576000848401525b50505050565b6000825161029f81846020870161025d565b6102e48201527f9190910192915050565b60208152600082518060208401526102c881604085016103048201527f6020870161025d565b601f017fffffffffffffffffffffffffffffffffffffff6103248201527fffffffffffffffffffffffffe016919091016040019291505056000000000000610344820152919050565b6000808390506000839050600080828451602086016000f5915050803b158015610bc1576040517fc56443730000000000000000000000000000000000000000000000000000000081526001600160a01b038316600482015260240161035b565b5095945050505050565b600060208284031215610bdd57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b60008060408385031215610c0d57600080fd5b823567ffffffffffffffff80821115610c2557600080fd5b818501915085601f830112610c3957600080fd5b813581811115610c4b57610c4b610be4565b604051601f8201601f19908116603f01168101908382118183101715610c7357610c73610be4565b81604052828152886020848701011115610c8c57600080fd5b826020860160208301376000602093820184015298969091013596505050505050565b60008083601f840112610cc157600080fd5b50813567ffffffffffffffff811115610cd957600080fd5b602083019150836020828501011115610cf157600080fd5b9250929050565b600080600080600060608688031215610d1057600080fd5b853567ffffffffffffffff80821115610d2857600080fd5b610d3489838a01610caf565b9097509550602088013594506040880135915080821115610d5457600080fd5b50610d6188828901610caf565b969995985093965092949392505050565b600080600060408486031215610d8757600080fd5b833567ffffffffffffffff811115610d9e57600080fd5b610daa86828701610caf565b909790965060209590950135949350505050565b80356001600160a01b0381168114610dd557600080fd5b919050565b60008060008060608587031215610df057600080fd5b610df985610dbe565b935060208501359250604085013567ffffffffffffffff811115610e1c57600080fd5b610e2887828801610caf565b95989497509550505050565b600060208284031215610e4657600080fd5b610e4f82610dbe565b9392505050565b634e487b7160e01b600052601160045260246000fd5b600060018201610e7e57610e7e610e56565b5060010190565b8183823760009101908152919050565b600060208083528351808285015260005b81811015610ec257858101830151858201604001528201610ea6565b81811115610ed4576000604083870101525b50601f01601f1916929092016040019392505050565b600081610ef957610ef9610e56565b506000190190565b6001600160a01b038416815260406020820152816040820152818360608301376000818301606090810191909152601f909201601f1916010192915050565b60008219821115610f5357610f53610e56565b50019056fea26469706673582212202f4493162beaddf880ec3cb5732ce90756ae630214323ed56d2aa8bf7158c7c664736f6c634300080f003360a06040523060805234801561001457600080fd5b50608051610a8c61004b60003960008181609f01528181610129015281816102160152818161029b015261037c0152610a8c6000f3fe6080604052600436106100345760003560e01c80633659cfe6146100395780634f1ef2861461005b57806352d1902d1461006e575b600080fd5b34801561004557600080fd5b5061005961005436600461088f565b610095565b005b6100596100693660046108d9565b61020c565b34801561007a57600080fd5b5061008361036f565b60405190815260200160405180910390f35b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001630036101275760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b19195b1959d85d1958d85b1b60a21b60648201526084015b60405180910390fd5b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166101827f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b6001600160a01b0316146101ed5760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b6163746976652070726f787960a01b606482015260840161011e565b6040805160008082526020820190925261020991839190610434565b50565b6001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001630036102995760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b19195b1959d85d1958d85b1b60a21b606482015260840161011e565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03166102f47f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b6001600160a01b03161461035f5760405162461bcd60e51b815260206004820152602c60248201527f46756e6374696f6e206d7573742062652063616c6c6564207468726f7567682060448201526b6163746976652070726f787960a01b606482015260840161011e565b61036b82826001610434565b5050565b6000306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161461040f5760405162461bcd60e51b815260206004820152603860248201527f555550535570677261646561626c653a206d757374206e6f742062652063616c60448201527f6c6564207468726f7567682064656c656761746563616c6c0000000000000000606482015260840161011e565b507f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc90565b7f4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd91435460ff161561046c57610467836105d9565b505050565b826001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156104c6575060408051601f3d908101601f191682019092526104c39181019061099b565b60015b6105385760405162461bcd60e51b815260206004820152602e60248201527f45524331393637557067726164653a206e657720696d706c656d656e7461746960448201527f6f6e206973206e6f742055555053000000000000000000000000000000000000606482015260840161011e565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc81146105cd5760405162461bcd60e51b815260206004820152602960248201527f45524331393637557067726164653a20756e737570706f727465642070726f7860448201527f6961626c65555549440000000000000000000000000000000000000000000000606482015260840161011e565b506104678383836106af565b6001600160a01b0381163b6106565760405162461bcd60e51b815260206004820152602d60248201527f455243313936373a206e657720696d706c656d656e746174696f6e206973206e60448201527f6f74206120636f6e747261637400000000000000000000000000000000000000606482015260840161011e565b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80547fffffffffffffffffffffffff0000000000000000000000000000000000000000166001600160a01b0392909216919091179055565b6106b8836106da565b6000825111806106c55750805b15610467576106d4838361071a565b50505050565b6106e3816105d9565b6040516001600160a01b038216907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a250565b606061073f8383604051806060016040528060278152602001610a3060279139610746565b9392505050565b60606001600160a01b0384163b6107c55760405162461bcd60e51b815260206004820152602660248201527f416464726573733a2064656c65676174652063616c6c20746f206e6f6e2d636f60448201527f6e74726163740000000000000000000000000000000000000000000000000000606482015260840161011e565b600080856001600160a01b0316856040516107e091906109e0565b600060405180830381855af49150503d806000811461081b576040519150601f19603f3d011682016040523d82523d6000602084013e610820565b606091505b509150915061083082828661083a565b9695505050505050565b6060831561084957508161073f565b8251156108595782518084602001fd5b8160405162461bcd60e51b815260040161011e91906109fc565b80356001600160a01b038116811461088a57600080fd5b919050565b6000602082840312156108a157600080fd5b61073f82610873565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600080604083850312156108ec57600080fd5b6108f583610873565b9150602083013567ffffffffffffffff8082111561091257600080fd5b818501915085601f83011261092657600080fd5b813581811115610938576109386108aa565b604051601f8201601f19908116603f01168101908382118183101715610960576109606108aa565b8160405282815288602084870101111561097957600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b6000602082840312156109ad57600080fd5b5051919050565b60005b838110156109cf5781810151838201526020016109b7565b838111156106d45750506000910152565b600082516109f28184602087016109b4565b9190910192915050565b6020815260008251806020840152610a1b8160408501602087016109b4565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220f65dfc1e543852973e209c16dccbcb8fb791caf1d019ba3ed04c669254c784ec64736f6c634300080f0033000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266c080a0a6a14c2711d322a50be5842c0f033e9c449a510c11d19728ae66622ffebfb993a06f44ca85a05b5efb5cde56f6967c57cad2b33f986fb0416847e698c2215c00c8"; From 7634617c48812ae77b9da5e0f9faa3cbd69417ae Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 04:38:25 -0700 Subject: [PATCH 04/14] feat: Use Swim packages to further improve code * feat: use token-projects package to determine token numbers * feat: use pool-math package for testing to replace hardcoded values * feat: include attestation of SwimUSD in deployment process when possible * feat: implement MOCK for Routing contract itself to isolate pool tests * refactor: replace BigNumber with more suited Decimal in test code * refactor: further improve test code and Wrapper classes --- .../contracts/interfaces/ITokenBridge.sol | 9 +- .../test/MockRoutingForPoolTests.sol | 18 +- .../contracts/test/MockTokenBridge.sol | 13 + packages/evm-contracts/package.json | 5 +- packages/evm-contracts/scripts/playground.ts | 336 +++++++++---- packages/evm-contracts/src/config.ts | 74 ++- packages/evm-contracts/src/deploy.ts | 189 +++++-- packages/evm-contracts/src/deployment.ts | 22 +- packages/evm-contracts/src/testUtils.ts | 269 +++++++--- packages/evm-contracts/test/pool.ts | 348 ++++++------- packages/evm-contracts/test/routing.ts | 471 +++++++++++------- 11 files changed, 1102 insertions(+), 652 deletions(-) mode change 100755 => 100644 packages/evm-contracts/scripts/playground.ts diff --git a/packages/evm-contracts/contracts/interfaces/ITokenBridge.sol b/packages/evm-contracts/contracts/interfaces/ITokenBridge.sol index 25a55d3aa..3efcb39ab 100644 --- a/packages/evm-contracts/contracts/interfaces/ITokenBridge.sol +++ b/packages/evm-contracts/contracts/interfaces/ITokenBridge.sol @@ -24,9 +24,14 @@ interface ITokenBridge { ) external payable returns (uint64 sequence); function completeTransfer(bytes memory encodedVm) external; - function completeTransferWithPayload(bytes memory encodedVm) external returns (bytes memory); - function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) external view returns (address); + function completeTransferWithPayload(bytes memory encodedVm) + external returns (bytes memory tokenbridgePayload); + + function createWrapped(bytes memory encodedVm) external returns (address token); + + function wrappedAsset(uint16 tokenChainId, bytes32 tokenAddress) + external view returns (address token); function wormhole() external view returns (IWormhole); } diff --git a/packages/evm-contracts/contracts/test/MockRoutingForPoolTests.sol b/packages/evm-contracts/contracts/test/MockRoutingForPoolTests.sol index f041638f4..ea9e603a1 100644 --- a/packages/evm-contracts/contracts/test/MockRoutingForPoolTests.sol +++ b/packages/evm-contracts/contracts/test/MockRoutingForPoolTests.sol @@ -6,10 +6,17 @@ import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; contract MockRoutingForPoolTests is Initializable, UUPSUpgradeable { - event TokenRegistered(uint16 indexed tokenNumber, address indexed token, address pool); - address public /*immutable*/ swimUsdAddress; + struct TokenInfo { + uint16 tokenNumber; + address tokenAddress; + address poolAddress; + uint8 tokenIndexInPool; + } + + mapping(uint16 => TokenInfo) public tokenNumberMapping; + function initialize(address _swimUsdAddress) public initializer { swimUsdAddress = _swimUsdAddress; } @@ -17,7 +24,10 @@ contract MockRoutingForPoolTests is Initializable, UUPSUpgradeable { function _authorizeUpgrade(address newImplementation) internal override {} function registerToken(uint16 tokenNumber, address tokenAddress, address poolAddress) external { - emit TokenRegistered(tokenNumber, tokenAddress, poolAddress); + TokenInfo storage token = tokenNumberMapping[tokenNumber]; + token.tokenNumber = tokenNumber; + token.tokenAddress = tokenAddress; + token.poolAddress = poolAddress; + token.tokenIndexInPool = 1; } - } diff --git a/packages/evm-contracts/contracts/test/MockTokenBridge.sol b/packages/evm-contracts/contracts/test/MockTokenBridge.sol index 777c967f0..d4eecb0cf 100644 --- a/packages/evm-contracts/contracts/test/MockTokenBridge.sol +++ b/packages/evm-contracts/contracts/test/MockTokenBridge.sol @@ -114,6 +114,19 @@ contract MockTokenBridge is ITokenBridge { return _completeTransfer(encodedVm, PAYLOAD_ID_WITH_PAYLOAD); } + function createWrapped(bytes memory encodedVm) external view returns (address) { + for (uint i = 1; i < encodedVm.length-34; ++i) { + uint offset = i; + bytes32 tokenAddress; + uint16 tokenChainId; + (tokenAddress, offset) = encodedVm.asBytes32(offset); + (tokenChainId, offset) = encodedVm.asUint16(offset); + if (tokenAddress == SWIM_USD_SOLANA_ADDRESS && tokenChainId == WORMHOLE_SOLANA_CHAIN_ID) + return address(swimUsd); + } + return address(0); + } + function wrappedAsset( uint16 tokenChainId, bytes32 tokenAddress diff --git a/packages/evm-contracts/package.json b/packages/evm-contracts/package.json index a84333f4c..54fc6a774 100644 --- a/packages/evm-contracts/package.json +++ b/packages/evm-contracts/package.json @@ -19,7 +19,7 @@ "compile": "hardhat compile", "clean": "hardhat clean", "test": "hardhat test", - "test:staging": "hardhat test --network rinkeby", + "playground": "hardhat run scripts/playground.ts", "test:fixtures": "hardhat test --deploy-fixture --network localhost", "transfer-ownership": "hardhat run scripts/transfer_ownership.ts", "format": "prettier --write \"./{src,test}/**/*.ts\"", @@ -45,6 +45,8 @@ "@openzeppelin/contracts-upgradeable": "^4.7.3", "@openzeppelin/hardhat-upgrades": "^1.20.0", "@swim-io/eslint-config": "workspace:^", + "@swim-io/pool-math": "workspace:^", + "@swim-io/token-projects": "workspace:^", "@swim-io/tsconfig": "workspace:^", "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.2", @@ -55,6 +57,7 @@ "@typescript-eslint/parser": "^5.38.1", "chai": "^4.3.6", "chai-bn": "^0.3.1", + "decimal.js": "^10.3.1", "dotenv": "^16.0.1", "eslint": "^8.22.0", "eslint-config-prettier": "^8.5.0", diff --git a/packages/evm-contracts/scripts/playground.ts b/packages/evm-contracts/scripts/playground.ts old mode 100755 new mode 100644 index 8efd8fd07..9c3ba9bcf --- a/packages/evm-contracts/scripts/playground.ts +++ b/packages/evm-contracts/scripts/playground.ts @@ -1,20 +1,43 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-type-assertion */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-console */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable functional/immutable-data */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { readFile, readdir } from "fs/promises"; + +import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { BigNumber, utils } from "ethers"; import { ethers } from "hardhat"; -import { confirm, getProxy, getLogic, getSwimFactory } from "../src/deploy"; -import { decodeVaa } from "../src/payloads"; -import { CHAINS } from "../src/config"; -import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { + ChainConfig, + RoutingConfig, + SWIM_USD_SOLANA_ADDRESS, + WORMHOLE_SOLANA_CHAIN_ID, +} from "../src/config"; +import { CHAINS } from "../src/config"; +import { + confirm, + deployRegular, + getLogic, + getProxy, + getRoutingProxy, + getSwimFactory, + getToken, +} from "../src/deploy"; +import { decodeVaa } from "../src/payloads"; +import { PoolWrapper, RoutingWrapper, TokenWrapper } from "../src/testUtils"; import type { Pool } from "../typechain-types/contracts/Pool"; +import { ITokenBridge } from "../typechain-types/contracts/interfaces/ITokenBridge"; const toHexBytes = (val: number, bytes: number) => { const hex = ethers.utils.hexlify(val).substring(2); const requiredBytes = hex.length / 2; if (requiredBytes > 2 * bytes) - throw Error( - "Could not convert " + val + "(= 0x" + hex + " ) does not fit in " + bytes + "bytes" - ); + throw Error(`Could not convert ${val} (= 0x${hex}) does not fit in ${bytes} bytes`); return "00".repeat(bytes - requiredBytes) + hex; }; @@ -71,6 +94,31 @@ async function poolDeploymentDebugging() { } } +async function compareBytecode() { + const files = await Promise.all( + ["before", "after", "latest"].map(async (f) => + JSON.parse(await readFile("./" + f + ".json", "utf-8")) + ) + ); + + const sizes = new Map(); + for (const file of files) { + const contracts = file.output.contracts; + for (const key of Object.keys(contracts)) { + if (key.startsWith("contracts/") && key.indexOf("/", "contracts/".length) == -1) { + const name = key.slice("contracts/".length, -4); + if (contracts[key][name]) { + const bytecode = contracts[key][name].evm.bytecode.object; + if (sizes.has(name)) sizes.get(name).push(bytecode.length / 2); + else sizes.set(name, [bytecode.length / 2]); + } + } + } + } + + console.log(sizes); +} + function decodePacked(args: readonly string[], encoded: string): any { let offset = 2; const readBytes = (size: number): string => { @@ -96,57 +144,6 @@ function decodePacked(args: readonly string[], encoded: string): any { return args.map(convert); } -function printEncodedVM() { - const encodedVm = - "01000000000100be0dcdfa049489c81b8fe3f4452335df013aa64ae7fb949c19" + - "bc8d1262a013723235018850b15a33033a330fa96f441162c8a3383cec447b7e" + - "7524eb68ca6f1900630e1d68000000120002000000000000000000000000f890" + - "982f9310df57d00f659cf4fd87e65aded8d7000000000000070c0f0300000000" + - "000000000000000000000000000000000000000000000000000a07f8296b21c9" + - "a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe2671900010000" + - "00000000000000000000a33e4d9624608c468fe5466dd6cc39ce1da4ff780004" + - "000000000000000000000000a33e4d9624608c468fe5466dd6cc39ce1da4ff78" + - "01000000000000000000000000b0a05611328d1068c91f58e2c83ab4048de8cd" + - "7f"; - console.log(decodeVaa(encodedVm)); -} - -async function manualTesting() { - const walletSeed = ""; - const wallet = ethers.Wallet.fromMnemonic(walletSeed).connect(ethers.provider!); - const routingProxy = ( - await ethers.getContractAt("Routing", "0xa33E4d9624608c468FE5466dd6CC39cE1Da4FF78") - ).connect(wallet); - - const encodedVm = - "0x" + - "0100000000010003cc44dc2672fcb3881443e91e9c3b4507d70c8f1c72b204301" + - "369bd1a078eae68d91463ac9f6cee56639852387e63d3f55de164dd20191cb512" + - "5024b24a158700630ef148000000150002000000000000000000000000f890982" + - "f9310df57d00f659cf4fd87e65aded8d700000000000007160f03000000000000" + - "0000000000000000000000000000000000000000000000989680296b21c9a4722" + - "da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe2671900010000000000" + - "00000000000000a33e4d9624608c468fe5466dd6cc39ce1da4ff7800040000000" + - "00000000000000000a33e4d9624608c468fe5466dd6cc39ce1da4ff7801000000" + - "000000000000000000b0a05611328d1068c91f58e2c83ab4048de8cd7f"; - const memo = "0x5662387c418a2f09b3f688b3c7fa3a5b"; - const toToken = "0x4C15919A4354b4416e7aFcB9A27a118bc45818C0"; - const minimumOutputAmount = 0; - - //console.log(routingProxy.estimateGas); - - console.log( - await confirm( - routingProxy["crossChainComplete(bytes,address,uint256,bytes16)"]( - encodedVm, - toToken, - minimumOutputAmount, - memo - ) - ) - ); -} - async function printAssembly(contract: string) { const buildDir = "artifacts/build-info/"; const [buildInfoFile] = await readdir(buildDir); @@ -154,58 +151,210 @@ async function printAssembly(contract: string) { console.log(buildInfo.output.contracts["contracts/" + contract + ".sol"][contract].evm.assembly); } -const getDefaultPool = async () => { +const transferEth = (from: SignerWithAddress, to: string, etherAmount: string) => + //e.g. etherAmount = "0.1" for .1 eth + confirm(from.sendTransaction({ to, value: ethers.utils.parseEther(etherAmount) })); + +async function printAttestedSwimUsd() { + const chainConfig = await getChainConfig(); + const tokenBridge = (await ethers.getContractAt( + "ITokenBridge", + (chainConfig.routing as RoutingConfig).wormholeTokenBridge! + )) as ITokenBridge; + const swimUsdAddress = await tokenBridge.wrappedAsset( + WORMHOLE_SOLANA_CHAIN_ID, + SWIM_USD_SOLANA_ADDRESS + ); + console.log(swimUsdAddress); +} + +async function transferAllEth(to: string) { + const [deployer] = await ethers.getSigners(); + const balance = await deployer.getBalance(); + const refundCost = (await ethers.provider.getFeeData()).gasPrice!.mul(21000); + const fmt = ethers.utils.formatEther; + if (balance.gt(refundCost)) { + const value = balance.sub(refundCost); + console.log( + `transferring ${ethers.utils.formatEther(value)} (= ${fmt(balance)} balance - ${fmt( + refundCost + )} refundCost) from ${deployer.address} to ${to}` + ); + await confirm(deployer.sendTransaction({ to, value })); + } else + console.log( + `nothing to transfer - ${deployer.address} has balance of ${fmt( + balance + )}$ which is not sufficient to cover gas costs of refund of ${fmt(refundCost)}` + ); +} + +async function getChainConfig() { const chainId = (await ethers.provider.detectNetwork()).chainId; const chainConfig = CHAINS[chainId]; if (!chainConfig) throw Error(`Network with chainId ${chainId} not implemented yet`); + return chainConfig; +} - return getProxy("Pool", chainConfig.pools![0].salt) as Promise; -}; +const getDefaultPool = async () => + getProxy("Pool", (await getChainConfig()).pools![0].salt) as Promise; -async function printTokenBalances(address: string) { - const pool = await getDefaultPool(); - const state = await pool.getState(); - const tokenAddresses = state.balances.map((struct: any[]) => struct[0]) as string[]; - console.log("token balances of address:", address); - for (const token of tokenAddresses) { - const ct = await ethers.getContractAt("ERC20", token); - console.log(token, await ct.balanceOf(address)); - } +async function setupWrappers(chainConfig: ChainConfig) { + const pool = await PoolWrapper.create(chainConfig.pools![0].salt); + + const routing = await RoutingWrapper.create(); + const { swimUsd } = routing; + + return { pool, routing, swimUsd }; } -async function upgradePool() { - const governance = (await ethers.getSigners())[1]; - const logic = (await getLogic("Pool")).address; - const pool = await getDefaultPool(); - console.log(`updating pool ${pool.address} to logic ${logic}`); - await confirm(pool.connect(governance).upgradeTo(logic)); +async function printBalances() { + const chainConfig = await getChainConfig(); + const { pool } = await setupWrappers(chainConfig); + const [deployer] = await ethers.getSigners(); + console.log(`balances of ${deployer.address} on ${chainConfig.name}`); + console.table({ + gasToken: { tokenAddress: "", balance: ethers.utils.formatEther(await deployer.getBalance()) }, + ...Object.fromEntries( + await Promise.all( + pool.tokens.map(async (token) => [ + token.symbol, + { + tokenAddress: token.address, + balance: await token.balanceOf(deployer), + totalSupply: await token.totalSupply(), + }, + ]) + ) + ), + }); + // console.log(`gas token:`, ethers.utils.formatEther(await deployer.getBalance())); + // for (const token of pool.tokens) + // console.log(`${token.symbol} (${token.address}):`, await token.balanceOf(deployer)); } -const transferEth = (from: SignerWithAddress, to: string, etherAmount: string) => - //e.g. etherAmount = "0.1" for .1 eth - confirm(from.sendTransaction({ to, value: ethers.utils.parseEther(etherAmount) })); +async function wormholeSwimUsd() { + const recipient = "0x866450d3256310D51Ff3aac388608e30d03d7841"; + const [deployer] = await ethers.getSigners(); + const { routing, swimUsd } = await setupWrappers(await getChainConfig()); + await routing.propellerInitiate( + deployer, + swimUsd, + 6000, + 10, + "0x" + "00".repeat(12) + recipient.slice(2), + false, + 1000, + 0 + ); +} + +const getFuncSelector = (functionSignature: string) => + ethers.utils.keccak256(Buffer.from(functionSignature)).slice(0, 10); + +async function printWormholeEmitters() { + const chainConfig = await getChainConfig(); + const tokenBridge = (chainConfig.routing as RoutingConfig).wormholeTokenBridge; + console.log(`registered emitters for Token Bridge (${tokenBridge}) on ${chainConfig.name}`); + const chainIds = [1, 2, 4, 5, 6, 10]; + console.log("chainId", "emitter"); + for (const chainId of chainIds) + console.log( + chainId.toString().padStart(7), + await ethers.provider.call({ + to: tokenBridge, + data: getFuncSelector("bridgeContracts(uint16)") + chainId.toString().padStart(64, "0"), + }) + ); +} async function main() { + const { pool } = await setupWrappers(await getChainConfig()); + //await printWormholeEmitters(); + //await printBalances(); + const [deployer] = await ethers.getSigners(); + // const faucet = { address: "0x790e1590023754b1554fcc3bde8ee90340f82ac5" }; + const faucet = await deployRegular("Faucet", [deployer.address]); + console.log("faucetAddress", faucet.address); + await confirm( + faucet.setup( + pool.tokens.map((t) => t.address), + pool.tokens.map((t) => t.toAtomic(1000)) + ) + ); + for (const token of pool.tokens) { + console.log("transferring", token.symbol, token.address); + await token.transfer(deployer, faucet, 1000000); + } + console.log("faucetBalances:"); + for (const token of pool.tokens) console.log(await token.balanceOf(faucet)); + + // const faucet = { address: "0x790e1590023754b1554fcc3bde8ee90340f82ac5" }; + // await confirm(deployer.sendTransaction({ to: faucet.address })); + // await printBalances(); + + // await deployer.sendTransaction({ + // to: "0x280999ab9abfde9dc5ce7afb25497d6bb3e8bdd4", + // data: "0x4a0cfc6b0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012d01000000000100e47c4b37ed8e168a7be81c39d8eae4d1e7e557b520a30788641c612f248c5946664aa021b38c7c92bc4b3f0417389010132100b7edd6b5e140fcd5e5c90ee80100633d21140000001f0002000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7000000000000086101030000000000000000000000000000000000000000000000000000000165a0bc00296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe267190001000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd4000a000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd401000000000000000000000000866450d3256310d51ff3aac388608e30d03d7841010000000000000003e8000000000000000000000000000000000000000000", + // }); + // const { pool } = await setupWrappers(await getChainConfig()); + //await pool.add(deployer, [995000, 995000, 995000], 0); + //for (const token of pool.tokens) console.log(token.symbol, await token.balanceOf(pool)); + // await ethers.provider.estimateGas({ + // from: "0x2fd34874480371d80904d2822e58aeade3aa1c74", + // to: "0x280999ab9abfde9dc5ce7afb25497d6bb3e8bdd4", + // data: "0x4a0cfc6b0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012d01000000000100e47c4b37ed8e168a7be81c39d8eae4d1e7e557b520a30788641c612f248c5946664aa021b38c7c92bc4b3f0417389010132100b7edd6b5e140fcd5e5c90ee80100633d21140000001f0002000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7000000000000086101030000000000000000000000000000000000000000000000000000000165a0bc00296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe267190001000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd4000a000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd401000000000000000000000000866450d3256310d51ff3aac388608e30d03d7841010000000000000003e8000000000000000000000000000000000000000000", + // }); + // const encodedVm = + // "01000000000100e47c4b37ed8e168a7be81c39d8eae4d1e7e557b520a30788641c612f248c5946664aa021b38c7c92bc4b3f0417389010132100b7edd6b5e140fcd5e5c90ee80100633d21140000001f0002000000000000000000000000f890982f9310df57d00f659cf4fd87e65aded8d7000000000000086101030000000000000000000000000000000000000000000000000000000165a0bc00296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe267190001000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd4000a000000000000000000000000280999ab9abfde9dc5ce7afb25497d6bb3e8bdd401000000000000000000000000866450d3256310d51ff3aac388608e30d03d7841010000000000000003e80000"; + // console.log(decodeVaa(encodedVm)); + //console.log(await routingProxy.contract.estimateGas.propellerComplete("0x" + encodedVm)); + // console.log( + // await routingProxy.contract.estimateGas[ + // "propellerInitiate(address,uint256,uint16,bytes32,bool,uint64)" + // ]( + // "0x92934a8b10ddf85e81b65be1d6810544744700dc", + // 1, + // 4, + // "0x00000000000000000000000092934a8b10ddf85e81b65be1d6810544744700dc", + // false, + // 1, + // { from: "0x866450d3256310d51ff3aac388608e30d03d7841" } + // ) + // ); + //printEncodedVM(); + //console.log("latest block:", await ethers.provider.getBlockNumber()); + //await transferAllEth("0x866450d3256310D51Ff3aac388608e30d03d7841"); + //await printAttestedSwimUsd(); + //await printTestnetState(); + //await compareBytecode(); //const [deployer, governance] = await ethers.getSigners(); - // const walletSeed = "fetch office dry buyer funny often cheese hurt buffalo carpet pole lady"; - // const wallet = ethers.Wallet.fromMnemonic(walletSeed).connect(ethers.provider!); + // const { pool, swimUsd, token1, token2 } = await setupWrappers(); + // const [chiu] = await ethers.getSigners(); + // const tokens = [swimUsd, token1, token2]; + // console.log("wallet funds:", await Promise.all(tokens.map((t) => t.balanceOf(chiu)))); + // await pool.add(chiu, pool.toAtomicAmounts("5000"), 0); + // console.log("after", await pool.contract.getState()); //await printTokenBalances(deployer.address); // const pool = await getDefaultPool(); - // console.log(pool.address, await pool.getState()); + // const pool = await ethers.getContractAt("Pool", "0x944fd8212c855e82e654ce70cd54566edf90f532"); + //console.log(pool.address, await pool.getState()); + // const lpToken = await ethers.getContractAt( + // "ERC20Token", + // "0x57FCF9B276d3E7D698112D9b87e6f410B1B5d78d" + // ); + // const lpAmount = "90000000000"; + // console.log(await lpToken.balanceOf(chiu.address)); + // await confirm(lpToken.approve(pool.address, lpAmount)); + // await confirm(pool["removeUniform(uint256,uint256[])"](lpAmount, ["0", "0", "0"])); + // console.log(await lpToken.balanceOf(chiu.address)); // console.log((await pool.getMarginalPrices()).map((p: BigNumber) => p.toString())); - //console.log(await pool.estimateGas["add(uint256[],uint256)"]([2000000000, 0, 0], 1146708034)); - printEncodedVM(); //await manualTesting(); // console.log(await deployer.getBalance()); //console.log(await tokenBridge.wrappedAsset(1, "0x296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe26719")); //const uniswap = await ethers.getContractAt("IUniswapV3PoolState", "0x9dcF9D205C9De35334D646BeE44b2D2859712A09"); // const sender = "0xb0a05611328d1068c91f58e2c83ab4048de8cd7f"; // const memo = "0x32a3624e63482375429f829eaca4db2e" + "00".repeat(16); - // //const filter = routingProxy.filters.SwimInteraction(sender); - // const filter = routingProxy.filters.SwimInteraction(null, memo); - // const events = await routingProxy.queryFilter(filter); - // console.log(events); - //console.log(routingProxy.interface.events["SwimInteraction(address,bytes16,uint8,bytes,bytes)"]); // const usdc = await ethers.getContractAt("ERC20Token", "0x45b167cf5b14007ca0490dcfb7c4b870ec0c0aa6"); // console.log(await usdc.allowance( // "0xb0A05611328d1068c91F58e2c83Ab4048De8CD7f", @@ -233,7 +382,7 @@ async function main() { // console.log(await lpToken.name()); // console.log(await lpToken.symbol()); // console.log(await lpToken.decimals()); - // const swimFactory = await getSwimFactory(); + //const swimFactory = await getSwimFactory(); // const filter = swimFactory.filters.ContractCreated(routing.address); // const [deployEvent] = await swimFactory.queryFilter(filter); // console.log(await deployEvent.getTransaction()); @@ -250,4 +399,9 @@ async function main() { // console.log(JSON.stringify(await ethers.provider.getTransactionReceipt("0x26c407c4bb570adb57026d60d3235377eecebb94c1b5732e1c0c592391ec3ada"), null, 2)); } -main(); +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/packages/evm-contracts/src/config.ts b/packages/evm-contracts/src/config.ts index 6e285a3d6..e51cb31da 100644 --- a/packages/evm-contracts/src/config.ts +++ b/packages/evm-contracts/src/config.ts @@ -1,17 +1,16 @@ -import { HARDHAT_FACTORY_PRESIGNED } from "./presigned"; +import { TokenProjectId } from "@swim-io/token-projects"; -export type TokenSymbol = "swimUSD" | "USDC" | "USDT" | "BUSD"; +import { HARDHAT_FACTORY_PRESIGNED } from "./presigned"; export type TestToken = { //will be dynamically deployed - readonly symbol: TokenSymbol; - readonly name: string; + readonly id: TokenProjectId; readonly decimals: number; }; export type DeployedToken = { //must already exist on-chain - readonly symbol: TokenSymbol; + readonly id: TokenProjectId; readonly address: string; }; @@ -34,7 +33,7 @@ export type RoutingFixedGasPrice = { }; export type RoutingUniswapOracle = { - readonly intermediateToken: TokenSymbol; + readonly intermediateTokenId: TokenProjectId; readonly uniswapPoolAddress: string; }; @@ -65,33 +64,32 @@ export const ROUTING_CONTRACT_SOLANA_ADDRESS = "0x857d8c691b9e9a1a1e98d010a36d6401a9099ce89d821751410623ad7c2a20d2"; export const SWIM_USD_SOLANA_ADDRESS = "0x296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe26719"; +//recovered from here: +// https://goerli.etherscan.io/tx/0x8e4eaaeb7ba6c2c158d5e8f1bb32d43e0fc42d96134a044e9ebdae5c864c8dc4 +export const SWIM_USD_ATTESTATION_ENCODEDVM = + "0x" + + "0100000000010032dc21539dc1ff9c16cd2ee2cf27db0954c95041dff907309c" + + "1e215a8f90f02c33fd45184631d1b9ad1a253b83bbda21583596336b05cf2b9a" + + "f6bc69bdbe4d310063094295000126ae00013b26409f8aaded3f5ddca184695a" + + "a6a0fa829b0c85caf84856324896d214ca980000000000000c702002296b21c9" + + "a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe2671900010600" + + "0000000000000000000000000000000000000000000000000000000000000000" + + "00000000000000000000000000000000000000000000000000000000000000"; export const PROPELLER_GAS_TIP = "10000000000"; //i.e. 1e9, i.e. 1 gwei export const GAS_TOKEN_DECIMALS = 18; //1e18 wei in 1 ETH or BNB, or AVAX, ... -export const TOKEN_NUMBERS: Record = { - swimUSD: 0, - USDC: 1, - USDT: 2, - BUSD: 3, -}; - export const DEFAULTS = { salt: "0x" + "00".repeat(32), lpDecimals: SWIM_USD_DECIMALS, amp: 1_000, //3 decimals lpFee: 300, //fee as 100th of a bip (6 decimals, 1000000 = 100 % fee) governanceFee: 100, - swimUsd: { - symbol: "swimUSD" as const, - name: "SwimUSD", - decimals: SWIM_USD_DECIMALS, - }, serviceFee: 10 ** (SWIM_USD_DECIMALS - 2), gasPriceMethod: { //set price of 1 human gas token (18 decimals) = of 1 human swimUSD (6 decimals) - //so if 10^18 wei = 10^6 swimUSD and we specify price with 18 decimals the 10^18 cancel out: - fixedSwimUsdPerGasToken: 10 ** SWIM_USD_DECIMALS, + //(i.e. a value of 1 means 1 swimUSD = 1 [ETH, BNB, AVAX, MATIC, what have you]) + fixedSwimUsdPerGasToken: 1, }, }; @@ -117,11 +115,9 @@ export const LOCAL = { { salt: "0x" + "00".repeat(31) + "01", lpSalt: "0x" + "00".repeat(31) + "11", - lpName: "Test Pool LP", - lpSymbol: "LP", tokens: [ - { symbol: "USDC" as const, name: "USD Coin", decimals: 6 }, - { symbol: "USDT" as const, name: "Tether", decimals: 6 }, + { id: TokenProjectId.Usdc, decimals: 6 }, + { id: TokenProjectId.Usdt, decimals: 6 }, ], }, ], @@ -136,12 +132,10 @@ const GOERLI = { { salt: "0x" + "00".repeat(31) + "01", lpSalt: "0x" + "00".repeat(31) + "11", - lpName: "Test Pool LP", - lpSymbol: "LP", tokens: [ //usdc and usdt both already have 6 decimals on Goerli - { symbol: "USDC" as const, address: "0x45B167CF5b14007Ca0490dCfB7C4B870Ec0C0Aa6" }, - { symbol: "USDT" as const, address: "0x996f42BdB0CB71F831C2eFB05Ac6d0d226979e5B" }, + { id: TokenProjectId.Usdc, address: "0x45B167CF5b14007Ca0490dCfB7C4B870Ec0C0Aa6" }, + { id: TokenProjectId.Usdt, address: "0x996f42BdB0CB71F831C2eFB05Ac6d0d226979e5B" }, ], }, ], @@ -156,11 +150,9 @@ const BNB_TESTNET = { { salt: "0x" + "00".repeat(31) + "03", lpSalt: "0x" + "00".repeat(31) + "13", - lpName: "Test Pool LP", - lpSymbol: "LP", tokens: [ - { symbol: "BUSD" as const, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, - { symbol: "USDT" as const, address: "0x98529E942FD121d9C470c3d4431A008257E0E714" }, + { id: TokenProjectId.Busd, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, + { id: TokenProjectId.Usdt, address: "0x98529E942FD121d9C470c3d4431A008257E0E714" }, ], }, ], @@ -175,11 +167,9 @@ const AVALANCHE_TESTNET = { { salt: "0x" + "00".repeat(31) + "01", lpSalt: "0x" + "00".repeat(31) + "11", - lpName: "Test Pool LP", - lpSymbol: "LP", tokens: [ - { symbol: "USDC" as const, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, - { symbol: "USDT" as const, address: "0x489dDcd070b6c4e0373FBB5d529Cc06328E048c3" }, + { id: TokenProjectId.Usdc, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, + { id: TokenProjectId.Usdt, address: "0x489dDcd070b6c4e0373FBB5d529Cc06328E048c3" }, ], }, ], @@ -194,11 +184,9 @@ const POLYGON_TESTNET = { { salt: "0x" + "00".repeat(31) + "01", lpSalt: "0x" + "00".repeat(31) + "11", - lpName: "Test Pool LP", - lpSymbol: "LP", tokens: [ - { symbol: "USDC" as const, address: "0x0a0d7cEA57faCBf5DBD0D3b5169Ab00AC8Cf7dd1" }, - { symbol: "USDT" as const, address: "0x2Ac9183EC64F71AfB73909c7C028Db14d35FAD2F" }, + { id: TokenProjectId.Usdc, address: "0x0a0d7cEA57faCBf5DBD0D3b5169Ab00AC8Cf7dd1" }, + { id: TokenProjectId.Usdt, address: "0x2Ac9183EC64F71AfB73909c7C028Db14d35FAD2F" }, ], }, ], @@ -213,11 +201,7 @@ const FANTOM_TESTNET = { { salt: "0x" + "00".repeat(31) + "01", lpSalt: "0x" + "00".repeat(31) + "11", - lpName: "Test Pool LP", - lpSymbol: "LP", - tokens: [ - { symbol: "USDC" as const, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }, - ], + tokens: [{ id: TokenProjectId.Usdc, address: "0x92934a8b10DDF85e81B65Be1D6810544744700dC" }], }, ], }; diff --git a/packages/evm-contracts/src/deploy.ts b/packages/evm-contracts/src/deploy.ts index 176bd6306..f58e916a5 100644 --- a/packages/evm-contracts/src/deploy.ts +++ b/packages/evm-contracts/src/deploy.ts @@ -2,8 +2,8 @@ import type { TransactionResponse } from "@ethersproject/abstract-provider"; import { getContractAddress } from "@ethersproject/address"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import type { BigNumber } from "ethers"; -import { Contract } from "ethers"; +import { TOKEN_PROJECTS_BY_ID } from "@swim-io/token-projects"; +import { BigNumber, Contract } from "ethers"; import { ethers } from "hardhat"; import type { Pool } from "../typechain-types/contracts/Pool"; @@ -12,11 +12,28 @@ import type { SwimFactory } from "../typechain-types/contracts/SwimFactory.sol/S import type { ERC20Token } from "../typechain-types/contracts/test/ERC20Token"; import type { DeployedToken, PoolConfig, RoutingConfig, TestToken } from "./config"; -import { DEFAULTS, POOL_PRECISION, SALTS, SWIM_FACTORY_ADDRESS, TOKEN_NUMBERS } from "./config"; +import { + DEFAULTS, + GAS_TOKEN_DECIMALS, + POOL_PRECISION, + ROUTING_PRECISION, + SALTS, + SWIM_FACTORY_ADDRESS, + SWIM_USD_ATTESTATION_ENCODEDVM, + SWIM_USD_DECIMALS, + SWIM_USD_SOLANA_ADDRESS, + WORMHOLE_SOLANA_CHAIN_ID, +} from "./config"; const ERC1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; +//the following should be part of ../typechain-types/contracts/Routing but isn't for whatever reason +enum RoutingGasTokenPriceMethod { + FixedPrice = 0, + UniswapOracle = 1, +} + export const confirm = async (tx: Promise) => (await tx).wait(); export const isDeployed = async (address: string) => @@ -48,8 +65,14 @@ export const getRegularAddress = async (name: string, constructorArgs: readonly return swimFactory.determineAddress(bytecodeWithConstructorArgs, DEFAULTS.salt); }; +const testTokenToConstructorArgs = (token: TestToken) => [ + TOKEN_PROJECTS_BY_ID[token.id].displayName, + TOKEN_PROJECTS_BY_ID[token.id].symbol, + token.decimals, +]; + export const getTokenAddress = (token: TestToken) => - getRegularAddress("ERC20Token", [token.name, token.symbol, token.decimals]); + getRegularAddress("ERC20Token", testTokenToConstructorArgs(token)); const getDeployedContract = async (name: string, address: string) => { if (!(await isDeployed(address))) @@ -85,22 +108,71 @@ export interface PoolConfigDeployedTokens extends PoolConfig { readonly tokens: readonly DeployedToken[]; } +export async function completeSwimUsdAttestation(tokenBridgeAddress: string) { + const tokenBridge = await ethers.getContractAt("ITokenBridge", tokenBridgeAddress); + const attestationIsComplete = async () => { + const swimUsdAddress = await tokenBridge.wrappedAsset( + WORMHOLE_SOLANA_CHAIN_ID, + SWIM_USD_SOLANA_ADDRESS + ); + return swimUsdAddress && swimUsdAddress !== ethers.constants.AddressZero; + }; + + if (!(await attestationIsComplete())) + await confirm(tokenBridge.createWrapped(SWIM_USD_ATTESTATION_ENCODEDVM)); + if (!(await attestationIsComplete())) + throw Error(`Could not attest swimUSD using encoded attestation`); +} + export async function setupPropellerFees(routingConfig: RoutingConfig) { const routingProxy = await getRoutingProxy(); const serviceFee = routingConfig.serviceFee ?? DEFAULTS.serviceFee; + const currentFeeConfig = await routingProxy.propellerFeeConfig(); + if (!currentFeeConfig.serviceFee.eq(serviceFee)) + await confirm(routingProxy.adjustPropellerServiceFee(serviceFee)); + const gasPriceMethod = routingConfig.gasPriceMethod ?? DEFAULTS.gasPriceMethod; - if (serviceFee !== 0) await confirm(routingProxy.adjustPropellerServiceFee(serviceFee)); - if ("uniswapPoolAddress" in gasPriceMethod) - await confirm( - routingProxy.usePropellerUniswapOracle( - gasPriceMethod.intermediateToken, - gasPriceMethod.uniswapPoolAddress - ) + + if ("uniswapPoolAddress" in gasPriceMethod) { + const tokenNumber = TOKEN_PROJECTS_BY_ID[gasPriceMethod.intermediateTokenId].tokenNumber; + if (tokenNumber === null) + throw Error( + `Token ${gasPriceMethod.intermediateTokenId} has not been assigned a tokenNumber yet` + ); + const uniswapConfig = currentFeeConfig.uniswap; + const tokenInfo = await routingProxy.tokenNumberMapping(tokenNumber); + if ( + currentFeeConfig.method !== RoutingGasTokenPriceMethod.UniswapOracle || + uniswapConfig.swimPool !== tokenInfo.poolAddress || + uniswapConfig.swimIntermediateIndex !== tokenInfo.tokenIndexInPool || + uniswapConfig.uniswapPool !== gasPriceMethod.uniswapPoolAddress + ) + await confirm( + routingProxy.usePropellerUniswapOracle( + tokenInfo.tokenAddress, + gasPriceMethod.uniswapPoolAddress + ) + ); + } else { + //same x swimUSD / gastoken + //if same decimals, then x * 10^18 + //for every decimal that swimUSD has price goes up by 10x because with e.g. 2 decimals 100 + // means 1 swimUSD whereas with 0 decimals 1 means 1 swimUSD + //for every decimal gas token has, price goes down by 10x + const fixedSwimUsdPerGasToken = BigNumber.from(gasPriceMethod.fixedSwimUsdPerGasToken).mul( + BigNumber.from(10).pow(ROUTING_PRECISION + SWIM_USD_DECIMALS - GAS_TOKEN_DECIMALS) ); - else { - const fixedSwimUsdPerGasToken = gasPriceMethod.fixedSwimUsdPerGasToken; - if (fixedSwimUsdPerGasToken !== 0) - await confirm(routingProxy.usePropellerFixedGasTokenPrice(fixedSwimUsdPerGasToken)); + + if ( + currentFeeConfig.method !== RoutingGasTokenPriceMethod.FixedPrice || + !currentFeeConfig.fixedSwimUsdPerGasToken.eq(fixedSwimUsdPerGasToken) + ) + await confirm( + routingProxy.usePropellerFixedGasTokenPrice({ + value: fixedSwimUsdPerGasToken, + decimals: ROUTING_PRECISION, + }) + ); } } @@ -138,14 +210,11 @@ export async function deployPoolAndRegister( const routingProxy = await getRoutingProxy(); for (let i = 0; i < pool.tokens.length; ++i) { const poolToken = pool.tokens[i]; - const tokenNumber = TOKEN_NUMBERS[poolToken.symbol]; - const filter = routingProxy.filters.TokenRegistered(tokenNumber); - const tokenReregisteredEvents = await routingProxy.queryFilter(filter); - const currentlyRegisteredPool = - tokenReregisteredEvents.length > 0 - ? tokenReregisteredEvents[tokenReregisteredEvents.length - 1] - : ""; - if (currentlyRegisteredPool !== poolProxy.address) + const tokenNumber = TOKEN_PROJECTS_BY_ID[poolToken.id].tokenNumber; + if (tokenNumber === null) + throw Error(`Token ${poolToken.id} has not been assigned a tokenNumber yet`); + + if ((await routingProxy.tokenNumberMapping(tokenNumber)).poolAddress !== poolProxy.address) await confirm(routingProxy.registerToken(tokenNumber, poolToken.address, poolProxy.address)); } @@ -172,25 +241,31 @@ export async function deployProxy( `expected: ${logic.address} but found ${actualLogic}` ); - const filter = swimFactory.filters.ContractCreated(proxyAddress); - const [deployEvent] = await swimFactory.queryFilter(filter); - const deployData = (await deployEvent.getTransaction()).data; - const index = deployData.lastIndexOf(initializeEncoded.slice(2)); - const suffixIndex = index === -1 ? -1 : index + initializeEncoded.length - 2; - if ( - index === -1 || - deployData.slice(suffixIndex) !== "0".repeat(deployData.length - suffixIndex) - ) + try { + //this tends to fail on various networks for various reasons, e.g. on BNB testnet you get + const filter = swimFactory.filters.ContractCreated(proxyAddress); + const deployEvents = await swimFactory.queryFilter(filter); + if (deployEvents.length === 0) throw Error(`RPC Provider failed to return deploy event`); + const [deployEvent] = deployEvents; + const deployData = (await deployEvent.getTransaction()).data; + const index = deployData.lastIndexOf(initializeEncoded.slice(2)); + const suffixIndex = index === -1 ? -1 : index + initializeEncoded.length - 2; + if ( + index === -1 || + deployData.slice(suffixIndex) !== "0".repeat(deployData.length - suffixIndex) + ) + console.warn( + `Warning: Deployment transaction of proxy ${proxyAddress} for logic contract` + + `${logic.address}$ was already deployed with different initialize arguments\n` + + `expected:\n${initializeEncoded}\nbut full original calldata was:\n${deployData}` + ); + } catch (e: any) { console.warn( - "Deployment transaction of proxy", - proxyAddress, - "for logic contract", - logic.address, - "was already deployed with different initialize arguments - expected:", - initializeEncoded, - "but full original calldata was:", - deployData + `Warning: Couldn't compare initialize arguments from previous deploy transaction:\n` + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions + `${e.message ?? e}` ); + } } else await confirm(swimFactory.createProxy(logic.address, salt, initializeEncoded)); return new Contract(proxyAddress, logic.interface, logic.provider); @@ -247,7 +322,7 @@ export async function deployRegular( } export const deployToken = async (token: TestToken): Promise => - deployRegular("ERC20Token", [token.name, token.symbol, token.decimals]) as Promise; + deployRegular("ERC20Token", testTokenToConstructorArgs(token)) as Promise; export async function deploySwimFactory( owner: SignerWithAddress, @@ -295,24 +370,34 @@ export async function deploySwimFactory( `expected: ${SWIM_FACTORY_ADDRESS} but got: ${precalculatedAddress}` ); - const swimFactoryFactory = (await ethers.getContractFactory("SwimFactory")).connect( - factoryDeployer - ); + const swimFactoryDeployTx = (await ethers.getContractFactory("SwimFactory")) + .connect(factoryDeployer) + .getDeployTransaction(owner.address); - const gasEstimate = await factoryDeployer.estimateGas( - swimFactoryFactory.getDeployTransaction(owner.address) - ); - - const { maxFeePerGas } = await ethers.getDefaultProvider().getFeeData(); + const gasEstimate = await factoryDeployer.estimateGas(swimFactoryDeployTx); + const { maxFeePerGas } = await ethers.provider.getFeeData(); const maxCost = gasEstimate.mul(maxFeePerGas!); + await topUpGasOfFactoryDeployer(factoryDeployer.address, maxCost); - const swimFactory = await (await swimFactoryFactory.deploy(owner.address)).deployed(); + const receipt = await confirm( + factoryDeployer.sendTransaction({ ...swimFactoryDeployTx, gasLimit: gasEstimate }) + ); - if (swimFactory.address !== SWIM_FACTORY_ADDRESS) + if (receipt.contractAddress !== SWIM_FACTORY_ADDRESS) throw Error( `Unexpected deployed SwimFactory address - ` + - `expected: ${SWIM_FACTORY_ADDRESS} but got: ${swimFactory.address}` + `expected: ${SWIM_FACTORY_ADDRESS} but got: ${receipt.contractAddress}` + ); + + const leftoverBalance = await ethers.provider.getBalance(factoryDeployer.address); + const refundCost = maxFeePerGas!.mul(21000); + if (leftoverBalance.gt(refundCost)) + await confirm( + factoryDeployer.sendTransaction({ + to: owner.address, + value: leftoverBalance.sub(refundCost), + }) ); } else if (presigned) { //deploy SwimFactory via presigned tx diff --git a/packages/evm-contracts/src/deployment.ts b/packages/evm-contracts/src/deployment.ts index 147251f74..a1bfa5385 100644 --- a/packages/evm-contracts/src/deployment.ts +++ b/packages/evm-contracts/src/deployment.ts @@ -1,18 +1,21 @@ import { readFile } from "fs/promises"; +import { TokenProjectId } from "@swim-io/token-projects"; import type { Contract } from "ethers"; import { ethers } from "hardhat"; import type { ChainConfig } from "./config"; import { - DEFAULTS, + POOL_PRECISION, ROUTING_CONTRACT_SOLANA_ADDRESS, + ROUTING_PRECISION, SWIM_FACTORY_ADDRESS, SWIM_USD_DECIMALS, SWIM_USD_SOLANA_ADDRESS, WORMHOLE_SOLANA_CHAIN_ID, } from "./config"; import { + completeSwimUsdAttestation, deployLogic, deployPoolAndRegister, deployProxy, @@ -52,7 +55,7 @@ export async function deployment(chainConfig: ChainConfig, options: DeployOption // eslint-disable-next-line no-console const log = options.print ? console.log : () => {}; const logAddress = (name: string, contract: Contract) => - log(name.padStart(18) + ":", contract.address); + log(name.padStart(24) + ":", contract.address); log("executing deployment script for", chainConfig.name); const [deployer, governanceFeeRecipient] = await ethers.getSigners(); @@ -63,6 +66,8 @@ export async function deployment(chainConfig: ChainConfig, options: DeployOption await checkConstant("ROUTING_CONTRACT_SOLANA_ADDRESS", ROUTING_CONTRACT_SOLANA_ADDRESS); await checkConstant("WORMHOLE_SOLANA_CHAIN_ID", WORMHOLE_SOLANA_CHAIN_ID.toString()); await checkConstant("SWIM_USD_DECIMALS", SWIM_USD_DECIMALS.toString()); + await checkConstant("POOL_PRECISION", POOL_PRECISION.toString()); + await checkConstant("ROUTING_PRECISION", ROUTING_PRECISION.toString()); await checkConstant("SWIM_FACTORY", SWIM_FACTORY_ADDRESS); logAddress( @@ -78,13 +83,16 @@ export async function deployment(chainConfig: ChainConfig, options: DeployOption const routingConfig = chainConfig.routing; if (routingConfig === "MOCK") { - const swimUsd = await deployToken(DEFAULTS.swimUsd); + const swimUsd = await deployToken({ id: TokenProjectId.SwimUsd, decimals: SWIM_USD_DECIMALS }); logAddress("SwimUSD", swimUsd); logAddress("RoutingLogic", await deployLogic("MockRoutingForPoolTests")); logAddress("RoutingProxy", await deployProxy("MockRoutingForPoolTests", [swimUsd.address])); } else { const wormholeTokenBridgeAddress = await (async () => { - if (routingConfig.wormholeTokenBridge !== "MOCK") return routingConfig.wormholeTokenBridge; + if (routingConfig.wormholeTokenBridge !== "MOCK") { + await completeSwimUsdAttestation(routingConfig.wormholeTokenBridge); + return routingConfig.wormholeTokenBridge; + } const coreBridge = await deployRegular("MockWormhole", []); logAddress("CoreBridge", coreBridge); @@ -105,10 +113,10 @@ export async function deployment(chainConfig: ChainConfig, options: DeployOption "address" in token ? token : { - symbol: token.symbol, + id: token.id, address: await (async () => { const tokenContract = await deployToken(token); - logAddress(token.symbol, tokenContract); + logAddress(token.id, tokenContract); return tokenContract.address; })(), } @@ -119,7 +127,7 @@ export async function deployment(chainConfig: ChainConfig, options: DeployOption deployer, governanceFeeRecipient ); - logAddress("Pool" + JSON.stringify(poolTokens.map((t) => t.symbol)), pool); + logAddress("Pool" + JSON.stringify(poolTokens.map((t) => t.id)), pool); } if (routingConfig !== "MOCK") await setupPropellerFees(routingConfig); diff --git a/packages/evm-contracts/src/testUtils.ts b/packages/evm-contracts/src/testUtils.ts index 03f5694be..4347bdf8a 100644 --- a/packages/evm-contracts/src/testUtils.ts +++ b/packages/evm-contracts/src/testUtils.ts @@ -1,9 +1,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ -import { BigNumber, formatFixed, parseFixed } from "@ethersproject/bignumber"; +import { formatFixed, parseFixed } from "@ethersproject/bignumber"; import { hexlify } from "@ethersproject/bytes"; import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import type { Decimalish } from "@swim-io/pool-math"; +import { PoolMath, toDecimal } from "@swim-io/pool-math"; +import { TOKEN_PROJECTS_BY_ID, TokenProjectId } from "@swim-io/token-projects"; +import { BN } from "bn.js"; +import { expect, use } from "chai"; +import type Decimal from "decimal.js"; import type { BigNumberish, BytesLike, Contract } from "ethers"; import { ethers } from "hardhat"; @@ -11,48 +17,76 @@ import type { Pool } from "../typechain-types/contracts/Pool"; import type { Routing } from "../typechain-types/contracts/Routing"; import type { ERC20Token } from "../typechain-types/contracts/test/ERC20Token"; -import { TOKEN_NUMBERS } from "./config"; -import { confirm, getProxy } from "./deploy"; +import { GAS_TOKEN_DECIMALS, POOL_PRECISION, ROUTING_PRECISION } from "./config"; +import { confirm, getProxy, getRoutingProxy } from "./deploy"; +// eslint-disable-next-line import/no-commonjs, @typescript-eslint/no-var-requires +use(require("chai-bn")(BN)); + +export const tolerance = 2 * 10 ** -POOL_PRECISION; + +export type { Decimalish }; export type HasAddress = { readonly address: string }; +export { toDecimal }; + const call = (contract: Contract, from: SignerWithAddress, method: string, args: readonly any[]) => confirm(contract.connect(from)[method](...args)); export class TokenWrapper { - constructor(readonly contract: ERC20Token, readonly decimals: number, readonly symbol: string) {} + readonly tokenNumber: number | null; + + constructor(readonly contract: ERC20Token, readonly decimals: number, readonly symbol: string) { + this.tokenNumber = + symbol === "swimUSD" //TODO workaround for token-projects not knowing new swimUSD yet + ? 0 + : Object.values(TokenProjectId) + .map((id) => TOKEN_PROJECTS_BY_ID[id]) + //TODO we're using includes() and lenght checking here instead of === here because e.g. + // on Avalanche USDC is called aUSDC... ultimately a better solution than this is + // obviously required to dynamically look up tokennumbers + .filter( + (project) => + symbol.includes(project.symbol) && + Math.abs(symbol.length - project.symbol.length) < 2 + )[0]?.tokenNumber; + } get address() { return this.contract.address; } - get tokenNumber() { - return TOKEN_NUMBERS[this.symbol as keyof typeof TOKEN_NUMBERS]; - } - static create = async (contract: ERC20Token) => new TokenWrapper(contract, await contract.decimals(), await contract.symbol()); - toAtomic = (human: BigNumberish) => - parseFixed(typeof human === "string" ? human : human.toString(), this.decimals); + toAtomic = (human: Decimalish) => + parseFixed(toDecimal(human).toFixed(this.decimals), this.decimals); + + toHuman = (atomic: BigNumberish) => toDecimal(formatFixed(atomic, this.decimals)); - toHuman = (atomic: BigNumberish) => formatFixed(atomic, this.decimals); + balanceOf = async (account: HasAddress) => + this.toHuman(await this.contract.balanceOf(account.address)); - balanceOf = (account: HasAddress) => this.contract.balanceOf(account.address); + allowance = async (owner: HasAddress, spender: HasAddress) => + this.toHuman(await this.contract.allowance(owner.address, spender.address)); - totalSupply = () => this.contract.totalSupply(); + totalSupply = async () => this.toHuman(await this.contract.totalSupply()); - approve = (from: SignerWithAddress, to: HasAddress, amount: BigNumberish) => - confirm(this.contract.connect(from).approve(to.address, amount)); + transfer = (from: SignerWithAddress, to: HasAddress, amount: Decimalish) => + confirm(this.contract.connect(from).transfer(to.address, this.toAtomic(amount))); - mint = (to: HasAddress, amount: BigNumberish) => confirm(this.contract.mint(to.address, amount)); + approve = (from: SignerWithAddress, to: HasAddress, amount: Decimalish) => + confirm(this.contract.connect(from).approve(to.address, this.toAtomic(amount))); - burn = (from: SignerWithAddress, amount: BigNumberish) => - confirm(this.contract.connect(from).burn(amount)); + mint = (to: HasAddress, amount: Decimalish) => + confirm(this.contract.mint(to.address, this.toAtomic(amount))); + + burn = (from: SignerWithAddress, amount: Decimalish) => + confirm(this.contract.connect(from).burn(this.toAtomic(amount))); } export class PoolWrapper { - constructor( + private constructor( readonly contract: Pool, readonly tokens: readonly TokenWrapper[], readonly lpToken: TokenWrapper @@ -62,134 +96,163 @@ export class PoolWrapper { return this.contract.address; } - static create = async (salt: string, tokens: readonly TokenWrapper[]) => { + static create = async (salt: string) => { const pool = (await getProxy("Pool", salt)) as Pool; + const state = await pool.getState(); + return new PoolWrapper( pool, - tokens, + await Promise.all( + state.balances.map(async (balance) => + TokenWrapper.create( + (await ethers.getContractAt("ERC20Token", balance.tokenAddress)) as ERC20Token + ) + ) + ), await TokenWrapper.create( (await ethers.getContractAt("LpToken", await pool.getLpToken())) as ERC20Token ) ); }; - toAtomicAmounts = (human: BigNumberish | readonly BigNumberish[]) => - this.tokens.map((t, i) => t.toAtomic(Array.isArray(human) ? human[i] : human)); + async poolmath() { + const state = await this.contract.getState(); + return new PoolMath( + state.balances.map((b, i) => toDecimal(formatFixed(b.balance, this.tokens[i].decimals))), + toDecimal(formatFixed(state.ampFactor.value, state.ampFactor.decimals)), + toDecimal(formatFixed(state.lpFee.value, state.lpFee.decimals)), + toDecimal(formatFixed(state.governanceFee.value, state.governanceFee.decimals)), + toDecimal(formatFixed(state.totalLpSupply.balance, this.lpToken.decimals)) + ); + } async getMarginalPrices() { return (await this.contract.getMarginalPrices()).map((decimalStruct) => - ethers.utils.formatUnits(decimalStruct.value, decimalStruct.decimals) + toDecimal(ethers.utils.formatUnits(decimalStruct.value, decimalStruct.decimals)) ); } async add( from: SignerWithAddress, - inputAmounts: readonly BigNumberish[], - minimumMintAmount: BigNumberish + inputAmounts: readonly Decimalish[], + minimumMintAmount: Decimalish ) { await this.approveAll(from, inputAmounts); - return call(this.contract, from, "add(uint256[],uint256)", [inputAmounts, minimumMintAmount]); + return call(this.contract, from, "add(uint256[],uint256)", [ + this.toAtomicAmounts(inputAmounts), + this.lpToken.toAtomic(minimumMintAmount), + ]); } async removeUniform( from: SignerWithAddress, - burnAmount: BigNumberish, - minimumOutputAmounts: readonly BigNumberish[] + burnAmount: Decimalish, + minimumOutputAmounts: readonly Decimalish[] ) { //no lp approval required return call(this.contract, from, "removeUniform(uint256,uint256[])", [ - burnAmount, - minimumOutputAmounts, + this.lpToken.toAtomic(burnAmount), + this.toAtomicAmounts(minimumOutputAmounts), ]); } async removeExactBurn( from: SignerWithAddress, - burnAmount: BigNumberish, + burnAmount: Decimalish, outputTokenIndex: number, - minimumOutputAmount: BigNumberish + minimumOutputAmount: Decimalish ) { //no lp approval required return call(this.contract, from, "removeExactBurn(uint256,uint8,uint256)", [ - burnAmount, + this.lpToken.toAtomic(burnAmount), outputTokenIndex, - minimumOutputAmount, + this.tokens[outputTokenIndex].toAtomic(minimumOutputAmount), ]); } async removeExactOutput( from: SignerWithAddress, - outputAmounts: readonly BigNumberish[], - maximumBurnAmount: BigNumberish + outputAmounts: readonly Decimalish[], + maximumBurnAmount: Decimalish ) { //no lp approval required return call(this.contract, from, "removeExactOutput(uint256[],uint256)", [ - outputAmounts, - maximumBurnAmount, + this.toAtomicAmounts(outputAmounts), + this.lpToken.toAtomic(maximumBurnAmount), ]); } async swap( from: SignerWithAddress, - inputAmount: BigNumberish, + inputAmount: Decimalish, inputTokenIndex: number, outputTokenIndex: number, - minimumOutputAmount: BigNumberish + minimumOutputAmount: Decimalish ) { await this.tokens[inputTokenIndex].approve(from, this.contract, inputAmount); return call(this.contract, from, "swap", [ - inputAmount, + this.tokens[inputTokenIndex].toAtomic(inputAmount), inputTokenIndex, outputTokenIndex, - minimumOutputAmount, + this.tokens[outputTokenIndex].toAtomic(minimumOutputAmount), ]); } async swapExactInput( from: SignerWithAddress, - inputAmounts: readonly BigNumberish[], + inputAmounts: readonly Decimalish[], outputTokenIndex: number, - minimumOutputAmount: BigNumberish + minimumOutputAmount: Decimalish ) { await this.approveAll(from, inputAmounts); return call(this.contract, from, "swapExactInput", [ - inputAmounts, + this.toAtomicAmounts(inputAmounts), outputTokenIndex, - minimumOutputAmount, + this.tokens[outputTokenIndex].toAtomic(minimumOutputAmount), ]); } async swapExactOutput( from: SignerWithAddress, - maximumInputAmount: BigNumberish, + maximumInputAmount: Decimalish, inputTokenIndex: number, - outputAmounts: readonly BigNumberish[] + outputAmounts: readonly Decimalish[] ) { await this.tokens[inputTokenIndex].approve(from, this.contract, maximumInputAmount); return call(this.contract, from, "swapExactOutput", [ - maximumInputAmount, + this.tokens[inputTokenIndex].toAtomic(maximumInputAmount), inputTokenIndex, - outputAmounts, + this.toAtomicAmounts(outputAmounts), ]); } - private readonly approveAll = (from: SignerWithAddress, amounts: readonly BigNumberish[]) => - Promise.all( - this.tokens.map((token, i) => - BigNumber.from(amounts[i]).isZero() - ? Promise.resolve() - : token.approve(from, this.contract, amounts[i]) - ) - ); + private readonly toAtomicAmounts = (human: readonly Decimalish[]) => + this.tokens.map((t, i) => t.toAtomic(human[i])); + + private async approveAll(from: SignerWithAddress, amounts: readonly Decimalish[]) { + for (let i = 0; i < this.tokens.length; ++i) { + const token = this.tokens[i]; + if (!(await token.allowance(from, this)).eq(amounts[i])) + await token.approve(from, this.contract, amounts[i]); + } + } } export class RoutingWrapper { - constructor(readonly contract: Routing) {} + private constructor(readonly contract: Routing, readonly swimUsd: TokenWrapper) {} get address() { return this.contract.address; } + static create = async () => { + const contract = await getRoutingProxy(); + const swimUsd = await TokenWrapper.create( + await ethers.getContractAt("ERC20Token", await contract.swimUsdAddress()) + ); + return new RoutingWrapper(contract, swimUsd); + }; + async getMemoInteractionEvents(memo?: BytesLike) { //we have to manually right-pad with zeros because either hardhat or ethers // are screwing up the look-up otherwise.... solid software @@ -202,17 +265,23 @@ export class RoutingWrapper { async onChainSwap( from: SignerWithAddress, fromToken: TokenWrapper, - inputAmount: BigNumberish, + inputAmount: Decimalish, toOwner: HasAddress, toToken: TokenWrapper, - minimumOutputAmount: BigNumberish, + minimumOutputAmount: Decimalish, memo?: BytesLike ) { await fromToken.approve(from, this, inputAmount); return this.memoCall( from, "onChainSwap(address,uint256,address,address,uint256)", - [fromToken.address, inputAmount, toOwner.address, toToken.address, minimumOutputAmount], + [ + fromToken.address, + fromToken.toAtomic(inputAmount), + toOwner.address, + toToken.address, + toToken.toAtomic(minimumOutputAmount), + ], memo ); } @@ -220,8 +289,8 @@ export class RoutingWrapper { async crossChainInitiate( from: SignerWithAddress, fromToken: TokenWrapper, - inputAmount: BigNumberish, - firstMinimumOutputAmount: BigNumberish, + inputAmount: Decimalish, + firstMinimumOutputAmount: Decimalish, wormholeRecipientChain: number, toOwner: HasAddress | BytesLike, memo?: BytesLike @@ -232,8 +301,8 @@ export class RoutingWrapper { "crossChainInitiate(address,uint256,uint256,uint16,bytes32)", [ fromToken.address, - inputAmount, - firstMinimumOutputAmount, + fromToken.toAtomic(inputAmount), + this.swimUsd.toAtomic(firstMinimumOutputAmount), wormholeRecipientChain, this.asBytes32(toOwner), ], @@ -244,11 +313,11 @@ export class RoutingWrapper { async propellerInitiate( from: SignerWithAddress, fromToken: TokenWrapper, - inputAmount: BigNumberish, + inputAmount: Decimalish, wormholeRecipientChain: number, toOwner: HasAddress | BytesLike, gasKickstart: boolean, - maxPropellerFee: BigNumberish, + maxPropellerFee: Decimalish, toTokenNumber: number, memo?: BytesLike ) { @@ -258,11 +327,11 @@ export class RoutingWrapper { "propellerInitiate(address,uint256,uint16,bytes32,bool,uint64,uint16)", [ fromToken.address, - inputAmount, + fromToken.toAtomic(inputAmount), wormholeRecipientChain, this.asBytes32(toOwner), gasKickstart, - maxPropellerFee, + this.swimUsd.toAtomic(maxPropellerFee), toTokenNumber, ], memo @@ -273,13 +342,13 @@ export class RoutingWrapper { from: SignerWithAddress, encodedVm: BytesLike, toToken: TokenWrapper, - minimumOutputAmount: BigNumberish, + minimumOutputAmount: Decimalish, memo?: BytesLike ) { return this.memoCall( from, "crossChainComplete(bytes,address,uint256)", - [encodedVm, toToken.address, minimumOutputAmount], + [encodedVm, toToken.address, toToken.toAtomic(minimumOutputAmount)], memo ); } @@ -289,15 +358,41 @@ export class RoutingWrapper { } async propellerFeeConfig() { - return await this.contract.propellerFeeConfig(); + const feeConfig = await this.contract.propellerFeeConfig(); + if (feeConfig.method > 1) + throw Error(`unrecognized propeller fee config method: ${feeConfig.method}`); + const method = + feeConfig.method === 0 ? ("fixedSwimUsdPerGasToken" as const) : ("uniswapOracle" as const); + return { + method, + serviceFee: this.swimUsd.toHuman(feeConfig.serviceFee), + fixedSwimUsdPerGasToken: toDecimal(feeConfig.fixedSwimUsdPerGasToken.toString()).mul( + toDecimal(10).pow(-ROUTING_PRECISION - this.swimUsd.decimals + GAS_TOKEN_DECIMALS) + ), + uniswap: { + //TODO the information in here should be converted further into a more usable form + swimPool: feeConfig.uniswap.swimPool, + swimIntermediateIndex: feeConfig.uniswap.swimPool, + uniswapPool: feeConfig.uniswap.uniswapPool, + uniswapIntermediateIsFirst: feeConfig.uniswap.uniswapIntermediateIsFirst, + }, + }; } async usePropellerFixedGasTokenPrice( owner: SignerWithAddress, - fixedSwimUsdPerGasToken: BigNumberish + fixedSwimUsdPerGasToken: Decimalish //in human i.e. intuitive units ) { return confirm( - this.contract.connect(owner).usePropellerFixedGasTokenPrice(fixedSwimUsdPerGasToken) + this.contract.connect(owner).usePropellerFixedGasTokenPrice({ + value: parseFixed( + toDecimal(fixedSwimUsdPerGasToken) + .mul(toDecimal(10).pow(this.swimUsd.decimals - GAS_TOKEN_DECIMALS)) + .toFixed(), + ROUTING_PRECISION + ), + decimals: ROUTING_PRECISION, + }) ); } @@ -332,3 +427,25 @@ export class RoutingWrapper { ? Buffer.from("00".repeat(12) + val.address.slice(2), "hex") : val; } + +export async function expectEqual( + token: TokenWrapper, + actual: HasAddress | Decimal, + expected: Decimalish +) { + expect(token.toAtomic("address" in actual ? await token.balanceOf(actual) : actual)).to.equal( + token.toAtomic(expected) + ); +} + +export async function expectCloseTo( + token: TokenWrapper, + actual: HasAddress | Decimal, + expected: Decimalish, + toleranceMultiplier = 1 +) { + //TODO find a better way to make "close to" comparisons than by going through BigNumber + expect( + token.toAtomic("address" in actual ? await token.balanceOf(actual) : actual) + ).to.be.closeTo(token.toAtomic(expected), token.toAtomic(tolerance).mul(toleranceMultiplier)); +} diff --git a/packages/evm-contracts/test/pool.ts b/packages/evm-contracts/test/pool.ts index 09c18bbce..1c901c1ed 100644 --- a/packages/evm-contracts/test/pool.ts +++ b/packages/evm-contracts/test/pool.ts @@ -1,39 +1,18 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ - -import { BigNumber, parseFixed } from "@ethersproject/bignumber"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { BN } from "bn.js"; -import { expect, use } from "chai"; +import { expect } from "chai"; import { ethers, network } from "hardhat"; import { LOCAL } from "../src/config"; -import { getRoutingProxy, getToken } from "../src/deploy"; import { deployment } from "../src/deployment"; -import { PoolWrapper, TokenWrapper } from "../src/testUtils"; +import type { HasAddress } from "../src/testUtils"; +import { PoolWrapper, expectCloseTo, expectEqual, tolerance } from "../src/testUtils"; import type { LpToken } from "../typechain-types/contracts/LpToken"; -// eslint-disable-next-line import/no-commonjs, @typescript-eslint/no-var-requires -use(require("chai-bn")(BN)); - describe("Pool Defi Operations", function () { - const liquidityProviderFunds = BigNumber.from(1e5); - const baseAmount = BigNumber.from("10"); - const tolerance = (token: TokenWrapper) => token.toAtomic("0.000002"); - - const asAtomic = (token: TokenWrapper, val: string | BigNumber | number) => - typeof val === "string" ? token.toAtomic(val) : val; - - function expectCloseTo( - token: TokenWrapper, - actual: string | BigNumber | number, - expected: string | BigNumber | number, - toleranceMultiplier = 1 - ) { - expect(asAtomic(token, actual)).to.be.closeTo( - asAtomic(token, expected), - tolerance(token).mul(toleranceMultiplier) - ); - } + const liquidityProviderFunds = 1e5; + const baseAmount = 10; + const userTokenIndex = 1; + const userFunds = 1; async function testFixture() { await network.provider.send("hardhat_reset"); @@ -42,22 +21,19 @@ describe("Pool Defi Operations", function () { await deployment({ ...LOCAL, routing: "MOCK" }, { print: false }); - const swimUsd = await TokenWrapper.create( - await ethers.getContractAt("ERC20Token", await (await getRoutingProxy()).swimUsdAddress()) - ); - - const [usdc, usdt] = await Promise.all( - LOCAL.pools[0].tokens.map(async (token) => await TokenWrapper.create(await getToken(token))) - ); - - const pool = await PoolWrapper.create(LOCAL.pools[0].salt, [swimUsd, usdc, usdt]); + const pool = await PoolWrapper.create(LOCAL.pools[0].salt); const { lpToken } = pool; - for (const token of pool.tokens) - await token.mint(liquidityProvider, token.toAtomic(liquidityProviderFunds)); + for (const token of pool.tokens) await token.mint(liquidityProvider, liquidityProviderFunds); + await pool.add( + liquidityProvider, + pool.tokens.map(() => baseAmount), + 0 + ); + await pool.tokens[userTokenIndex].mint(user, userFunds); - await pool.add(liquidityProvider, pool.toAtomicAmounts(baseAmount), 0); - await usdc.mint(user, usdc.toAtomic(1)); + const balancesOf = async (who: HasAddress) => + Promise.all(pool.tokens.map((token) => token.balanceOf(who))); return { deployer, @@ -67,225 +43,233 @@ describe("Pool Defi Operations", function () { user, pool, lpToken, - swimUsd, - usdc, - usdt, + balancesOf, }; } it("Check basic pool deployment parameters", async function () { - const { pool, lpToken, governance, liquidityProvider } = await loadFixture(testFixture); + const { pool, lpToken, governance, govFeeRecip, liquidityProvider } = await loadFixture( + testFixture + ); expect(await pool.contract.governance()).to.equal(governance.address); + expect(await pool.contract.governanceFeeRecipient()).to.equal(govFeeRecip.address); expect(await (lpToken.contract as LpToken).owner()).to.equal(pool.address); - expect(await lpToken.balanceOf(liquidityProvider)).to.equal( - lpToken.toAtomic(baseAmount).mul(3) - ); + await expectEqual(lpToken, liquidityProvider, baseAmount * pool.tokens.length); }); it("removeUniform should empty the pool and allow refilling after", async function () { - const { pool, lpToken, govFeeRecip, liquidityProvider, swimUsd, usdc, usdt } = - await loadFixture(testFixture); + const { pool, lpToken, govFeeRecip, liquidityProvider } = await loadFixture(testFixture); - const tokens = [swimUsd, usdc, usdt]; - - const lpAmount = lpToken.toAtomic(baseAmount).mul(3); - await pool.removeUniform(liquidityProvider, lpAmount, pool.toAtomicAmounts(baseAmount)); + const lpAmount = baseAmount * pool.tokens.length; + await pool.removeUniform( + liquidityProvider, + lpAmount, + pool.tokens.map(() => baseAmount) + ); - expect(await lpToken.balanceOf(govFeeRecip)).to.equal(0); - expect(await lpToken.balanceOf(liquidityProvider)).to.equal(0); - for (const token of tokens) - expect(await token.balanceOf(liquidityProvider)).to.equal( - token.toAtomic(liquidityProviderFunds) - ); + await expectEqual(lpToken, govFeeRecip, 0); + await expectEqual(lpToken, liquidityProvider, 0); + for (const token of pool.tokens) + await expectEqual(token, liquidityProvider, liquidityProviderFunds); - await pool.add(liquidityProvider, pool.toAtomicAmounts(baseAmount), 0); - expect(await lpToken.balanceOf(govFeeRecip)).to.equal(0); - expect(await lpToken.balanceOf(liquidityProvider)).to.equal( - lpToken.toAtomic(baseAmount).mul(3) + await pool.add( + liquidityProvider, + pool.tokens.map(() => baseAmount), + 0 ); - for (const token of tokens) - expect(await token.balanceOf(liquidityProvider)).to.equal( - token.toAtomic(liquidityProviderFunds.sub(baseAmount)) - ); + await expectEqual(lpToken, govFeeRecip, 0); + await expectEqual(lpToken, liquidityProvider, baseAmount * pool.tokens.length); + for (const token of pool.tokens) + await expectEqual(token, liquidityProvider, liquidityProviderFunds - baseAmount); }); it("add should return correct outputs", async function () { - const { pool, lpToken, govFeeRecip, user } = await loadFixture(testFixture); + const { pool, lpToken, govFeeRecip, user, balancesOf } = await loadFixture(testFixture); - const expectedUserLp = "0.976045"; - const expectedGovFee = "0.000063"; + const inputAmounts = pool.tokens.map((_, i) => (i === userTokenIndex ? userFunds : 0)); + const expected = (await pool.poolmath()).add(inputAmounts); + const userBalancesBefore = await balancesOf(user); - await pool.add(user, pool.toAtomicAmounts([0, 1, 0]), 0); + await pool.add(user, inputAmounts, 0); - expectCloseTo(lpToken, await lpToken.balanceOf(user), expectedUserLp); - expectCloseTo(lpToken, await lpToken.balanceOf(govFeeRecip), expectedGovFee); + for (let i = 0; i < pool.tokens.length; ++i) + await expectEqual(pool.tokens[i], user, userBalancesBefore[i].sub(inputAmounts[i])); + + await expectCloseTo(lpToken, user, expected.lpOutputAmount); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); }); it("removeExactBurn and removeExactOutput should return correct and consistent outputs", async function () { + const outputIndex = userTokenIndex; + const inputAmount = userFunds; const setup = async () => { - const { pool, lpToken, govFeeRecip, usdc, user } = await loadFixture(testFixture); + const { pool, lpToken, govFeeRecip, user, balancesOf } = await loadFixture(testFixture); + + const inputAmounts = pool.tokens.map((_, i) => (i === outputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).add(inputAmounts); + const userBalancesBefore = await balancesOf(user); + + await pool.add(user, inputAmounts, 0); - const expectedLp = lpToken.toAtomic("0.976046"); - const expectedUsdc = usdc.toAtomic("0.999495"); - const expectedGovFee = lpToken.toAtomic("0.000063"); + for (let i = 0; i < pool.tokens.length; ++i) + await expectEqual(pool.tokens[i], user, userBalancesBefore[i].sub(inputAmounts[i])); - await pool.add(user, pool.toAtomicAmounts([0, 1, 0]), 0); + await expectCloseTo(lpToken, user, expected.lpOutputAmount); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); - expect(await lpToken.balanceOf(user)).to.equal(expectedLp); const govFee = await lpToken.balanceOf(govFeeRecip); - //"flush" governance fee from first add for easier checking after - const miscAddress = "0x" + "0".repeat(39) + "1"; - await lpToken.contract.connect(govFeeRecip).transfer(miscAddress, govFee); - return { - expectedLp, - expectedUsdc, - expectedGovFee, - pool, - lpToken, - govFeeRecip, - usdc, - user, - }; + //"flush" governance fee from initial add for easier checking after + const flush = { address: "0x" + "0".repeat(39) + "1" }; + await lpToken.transfer(govFeeRecip, flush, govFee); + const userLp = await lpToken.balanceOf(user); + return { pool, lpToken, govFeeRecip, user, userLp }; }; + let expected; { - const { expectedLp, expectedUsdc, expectedGovFee, pool, lpToken, govFeeRecip, usdc, user } = - await setup(); + const { pool, lpToken, govFeeRecip, user, userLp } = await setup(); + + expected = (await pool.poolmath()).removeExactBurn(userLp, outputIndex); - await pool.removeExactBurn(user, expectedLp, 1, 0); + await pool.removeExactBurn(user, userLp, outputIndex, 0); - expectCloseTo(usdc, await usdc.balanceOf(user), expectedUsdc); - expectCloseTo(lpToken, await lpToken.balanceOf(govFeeRecip), expectedGovFee); + await expectCloseTo(pool.tokens[outputIndex], user, expected.stableOutputAmount); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); } { - const { expectedLp, expectedUsdc, expectedGovFee, pool, lpToken, govFeeRecip, usdc, user } = - await setup(); + const { pool, lpToken, govFeeRecip, user, userLp } = await setup(); - const outputAmount = expectedUsdc.sub(tolerance(usdc)); - await pool.removeExactOutput(user, [0, outputAmount, 0], expectedLp); + const outputAmount = expected.stableOutputAmount.sub(tolerance); + const outputAmounts = pool.tokens.map((_, i) => (i === outputIndex ? outputAmount : 0)); - expect(await usdc.balanceOf(user)).to.equal(outputAmount); - expectCloseTo(lpToken, await lpToken.balanceOf(user), 0, 2); - expectCloseTo(lpToken, await lpToken.balanceOf(govFeeRecip), expectedGovFee); + await pool.removeExactOutput(user, outputAmounts, userLp); + + await expectEqual(pool.tokens[outputIndex], user, outputAmount); + await expectCloseTo(lpToken, user, 0, 2); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); } }); it("swap equals swapExactInput exactly", async function () { - const swap = async () => { - const { pool, lpToken, govFeeRecip, usdc, usdt, user } = await loadFixture(testFixture); + const inputIndex = userTokenIndex; + const inputAmount = userFunds; + const outputIndex = 2; - await pool.swap(user, usdc.toAtomic(1), 1, 2, 0); - return { - swapUserUsdc: await usdt.balanceOf(user), - swapGovFee: await lpToken.balanceOf(govFeeRecip), - }; - }; + const swapExactInput = await (async () => { + const { pool, lpToken, govFeeRecip, user } = await loadFixture(testFixture); - const { swapUserUsdc, swapGovFee } = await swap(); + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, outputIndex); - const swapExactInput = async () => { - const { pool, lpToken, govFeeRecip, usdt, user } = await loadFixture(testFixture); + await pool.swapExactInput(user, inputAmounts, outputIndex, 0); + + await expectCloseTo(pool.tokens[outputIndex], user, expected.stableOutputAmount); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); - await pool.swapExactInput(user, pool.toAtomicAmounts([0, 1, 0]), 2, 0); return { - swapExactInputUserUsdc: await usdt.balanceOf(user), - swapExactInputGovFee: await lpToken.balanceOf(govFeeRecip), + stableAmount: await pool.tokens[outputIndex].balanceOf(user), + govFeeAmount: await lpToken.balanceOf(govFeeRecip), }; - }; + })(); - const { swapExactInputUserUsdc, swapExactInputGovFee } = await swapExactInput(); + const { pool, lpToken, govFeeRecip, user } = await loadFixture(testFixture); + + await pool.swap(user, inputAmount, inputIndex, outputIndex, 0); - expect(swapUserUsdc).to.equal(swapExactInputUserUsdc); - expect(swapGovFee).to.equal(swapExactInputGovFee); + await expectEqual(pool.tokens[outputIndex], user, swapExactInput.stableAmount); + await expectEqual(lpToken, govFeeRecip, swapExactInput.govFeeAmount); }); it("swapExactInput and swapExactOutput should return correct and consistent outputs", async function () { - const expectedUsdt = "0.929849"; + //const expectedUsdt = "0.929849"; - const swapExactInputGovFee = await (async () => { - const { pool, lpToken, govFeeRecip, usdc, usdt, user } = await loadFixture(testFixture); + const inputIndex = userTokenIndex; + const inputAmount = userFunds; + const outputIndex = 2; - await pool.swapExactInput(user, pool.toAtomicAmounts([0, 1, 0]), 2, 0); + const swapExactInput = await (async () => { + const { pool, lpToken, govFeeRecip, user } = await loadFixture(testFixture); - expect(await usdc.balanceOf(user)).to.equal(0); - expectCloseTo(usdt, await usdt.balanceOf(user), expectedUsdt); + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, outputIndex); - return lpToken.balanceOf(govFeeRecip); - })(); + await pool.swapExactInput(user, inputAmounts, outputIndex, 0); - const { pool, lpToken, govFeeRecip, usdc, usdt, user } = await loadFixture(testFixture); + await expectEqual(pool.tokens[inputIndex], user, 0); + await expectCloseTo(pool.tokens[outputIndex], user, expected.stableOutputAmount); + await expectCloseTo(lpToken, govFeeRecip, expected.governanceMintAmount); - const outputAmount = usdt.toAtomic(expectedUsdt).sub(tolerance(usdt)); - await pool.swapExactOutput(user, usdc.toAtomic(1), 1, [0, 0, outputAmount]); + return { + stableAmount: await pool.tokens[outputIndex].balanceOf(user), + govFeeAmount: await lpToken.balanceOf(govFeeRecip), + }; + })(); - const remainingUsdc = await usdc.balanceOf(user); - const actualUsdt = await usdt.balanceOf(user); + const { pool, lpToken, govFeeRecip, user } = await loadFixture(testFixture); - expectCloseTo(usdc, remainingUsdc, 0, 2); - expect(actualUsdt).to.equal(outputAmount); + const outputAmount = swapExactInput.stableAmount.sub(tolerance); + const outputAmounts = pool.tokens.map((_, i) => (i === outputIndex ? outputAmount : 0)); - expectCloseTo(lpToken, await lpToken.balanceOf(govFeeRecip), swapExactInputGovFee); + await pool.swapExactOutput(user, inputAmount, inputIndex, outputAmounts); + + await expectEqual(pool.tokens[outputIndex], user, outputAmount); + await expectCloseTo(pool.tokens[inputIndex], user, 0, 2); + await expectCloseTo(lpToken, govFeeRecip, swapExactInput.govFeeAmount, 2); }); it("Check marginal prices are correct for stable swap", async function () { - const { pool, lpToken, liquidityProvider, swimUsd, usdc, usdt } = await loadFixture( - testFixture - ); + const { pool, lpToken, liquidityProvider } = await loadFixture(testFixture); - const expectedLpSupply = lpToken.toAtomic("16.749421"); - const marginalPriceDecimals = 18; - const expectedPrices = ["0.698014", "0.930686", "1.628701"].map((p) => - parseFixed(p, marginalPriceDecimals) - ); - const priceTolerance = parseFixed("0.000001", marginalPriceDecimals); - const decimals = [swimUsd, usdc, usdt].map((t) => t.decimals); + // const expectedLpSupply = lpToken.toAtomic("16.749421"); + // const marginalPriceDecimals = 18; + // const expectedPrices = ["0.698014", "0.930686", "1.628701"].map((p) => + // parseFixed(p, marginalPriceDecimals) + // ); + // const priceTolerance = parseFixed("0.000001", marginalPriceDecimals); + // const decimals = [swimUsd, usdc, usdt].map((t) => t.decimals); + const removeAmounts = [1, 4, 7]; - await pool.removeExactOutput( - liquidityProvider, - pool.toAtomicAmounts([1, 4, 7]), - lpToken.toAtomic(baseAmount).mul(3) - ); + await pool.removeExactOutput(liquidityProvider, removeAmounts, baseAmount * 3); - const actualLpSupply = await lpToken.totalSupply(); - //test against the internal function, rather than the convenience function - const actualPrices = await pool.contract.getMarginalPrices(); - expectCloseTo(lpToken, actualLpSupply, expectedLpSupply); - for (let i = 0; i < expectedPrices.length; ++i) { - expect(actualPrices[i].value).to.be.closeTo(expectedPrices[i], priceTolerance); - expect(actualPrices[i].decimals).to.equal( - marginalPriceDecimals + decimals[i] - lpToken.decimals - ); - } + const expected = (await pool.poolmath()).marginalPrices(); - await lpToken.burn(liquidityProvider, actualLpSupply.div(2)); - const doubledPrices = await pool.contract.getMarginalPrices(); - for (let i = 0; i < doubledPrices.length; ++i) { - expect(doubledPrices[i].value).to.be.closeTo(expectedPrices[i].mul(2), priceTolerance.mul(2)); - expect(doubledPrices[i].decimals).to.equal( - marginalPriceDecimals + decimals[i] - lpToken.decimals - ); - } + const actualPrices = await pool.getMarginalPrices(); + for (let i = 0; i < pool.tokens.length; ++i) + expect(expected[i].sub(actualPrices[i]).abs().lessThanOrEqualTo(tolerance)); + + await lpToken.burn(liquidityProvider, (await lpToken.balanceOf(liquidityProvider)).div(2)); + const doubledPrices = await pool.getMarginalPrices(); + for (let i = 0; i < pool.tokens.length; ++i) + expect(expected[i].mul(2).sub(doubledPrices[i]).abs().lessThanOrEqualTo(tolerance)); }); it("Works for skewed swimUsd and LP", async function () { - const { pool, lpToken, liquidityProvider, swimUsd } = await loadFixture(testFixture); + const { pool, lpToken, liquidityProvider } = await loadFixture(testFixture); - const poolBalances = pool.toAtomicAmounts(["0.879412", "2052.006916", "2117.774978"]); - const lpSupply = lpToken.toAtomic("809.89675"); - const addswimUsd = swimUsd.toAtomic("20"); - const expectedLp = lpToken.toAtomic("979.838246"); + const poolBalances = ["0.879412", "2052.006916", "2117.774978"]; + const lpSupply = "809.89675"; + const inputIndex = 0; + const inputAmount = "20"; + //const expectedLp = "979.838246"; - const lpAmount = lpToken.toAtomic(baseAmount).mul(3); - await pool.removeUniform(liquidityProvider, lpAmount, pool.toAtomicAmounts(baseAmount)); + await pool.removeUniform( + liquidityProvider, + baseAmount * pool.tokens.length, + pool.tokens.map(() => baseAmount) + ); await pool.add(liquidityProvider, poolBalances, 0); const rectifySupply = await lpToken.balanceOf(liquidityProvider); await lpToken.burn(liquidityProvider, rectifySupply.sub(lpSupply)); - await pool.add(liquidityProvider, [addswimUsd, 0, 0], 0); - const receivedLp = (await lpToken.balanceOf(liquidityProvider)).sub(lpSupply); - expectCloseTo(lpToken, receivedLp, expectedLp); + + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).add(inputAmounts); + + await pool.add(liquidityProvider, inputAmounts, 0); + //TODO figure out why this is less accurate than everything else + await expectCloseTo(lpToken, liquidityProvider, expected.lpOutputAmount.add(lpSupply), 5); }); // it("Check marginal prices are correct for constant product", async function () { diff --git a/packages/evm-contracts/test/routing.ts b/packages/evm-contracts/test/routing.ts index de3739c61..ad4b1c7dc 100644 --- a/packages/evm-contracts/test/routing.ts +++ b/packages/evm-contracts/test/routing.ts @@ -1,27 +1,33 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -import { BigNumber, parseFixed } from "@ethersproject/bignumber"; +import { BigNumber } from "@ethersproject/bignumber"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; -import { BN } from "bn.js"; -import { expect, use } from "chai"; +import { expect } from "chai"; +import Decimal from "decimal.js"; import { ethers, network } from "hardhat"; import { + GAS_TOKEN_DECIMALS, LOCAL, ROUTING_CONTRACT_SOLANA_ADDRESS, SWIM_USD_SOLANA_ADDRESS, + SWIM_USD_TOKEN_INDEX, WORMHOLE_SOLANA_CHAIN_ID, } from "../src/config"; -import { getRegular, getRoutingProxy, getToken } from "../src/deploy"; +import { getRegular } from "../src/deploy"; import { deployment } from "../src/deployment"; import { CoreBridgeMessage, SwimPayload, TokenBridgePayload } from "../src/payloads"; -import { PoolWrapper, RoutingWrapper, TokenWrapper } from "../src/testUtils"; -import type { HasAddress } from "../src/testUtils"; +import { + PoolWrapper, + RoutingWrapper, + expectCloseTo, + expectEqual, + toDecimal, +} from "../src/testUtils"; +import type { Decimalish, HasAddress, TokenWrapper } from "../src/testUtils"; import type { IWormhole } from "../typechain-types/contracts/interfaces/IWormhole"; -// eslint-disable-next-line import/no-commonjs, @typescript-eslint/no-var-requires -use(require("chai-bn")(BN)); - const asBytes = (hexVal: string, size: number) => { const _hexVal = (hexVal.startsWith("0x") ? hexVal.slice(2) : hexVal).toLowerCase(); if (hexVal.length % 2) throw Error(`hex string ${hexVal} has odd number of characters`); @@ -42,37 +48,22 @@ const asEvmAddress = (buf: Buffer) => { return ethers.utils.getAddress("0x" + hexEnc.slice(-40)); }; -const asBigNumber = (fixedValue: string, decimals: number) => { - const index = fixedValue.indexOf("."); - const truncated = index === -1 ? fixedValue : fixedValue.slice(0, index + decimals + 1); - return parseFixed(truncated, decimals); -}; +// const asBigNumber = (fixedValue: string, decimals: number) => { +// const index = fixedValue.indexOf("."); +// const truncated = index === -1 ? fixedValue : fixedValue.slice(0, index + decimals + 1); +// return parseFixed(truncated, decimals); +// }; -const tenToThe = (exp: number) => BigNumber.from(10).pow(exp); +// const tenToThe = (exp: number) => BigNumber.from(10).pow(exp); const toSwimPayload = (toOwner: HasAddress, ...args: readonly any[]) => new SwimPayload(1, asBytes(toOwner.address, 32), ...args); describe("Routing CrossChain and Propeller Defi Operations", function () { - const liquidityProviderFunds = BigNumber.from(1e5); - const baseAmount = BigNumber.from("10"); - const tolerance = (token: TokenWrapper) => token.toAtomic("0.000001"); - - const asAtomic = (token: TokenWrapper, val: string | BigNumber | number) => - typeof val === "string" ? token.toAtomic(val) : val; - - function expectCloseTo( - token: TokenWrapper, - actual: string | BigNumber | number, - expected: string | BigNumber | number, - toleranceMultiplier = 1 - ) { - expect(asAtomic(token, actual)).to.be.closeTo( - asAtomic(token, expected), - tolerance(token).mul(toleranceMultiplier) - ); - } - + const liquidityProviderFunds = 1e5; + const baseAmount = 10; + const userTokenIndex = 1; + const userFunds = 1; const memo = Buffer.from("00".repeat(15) + "01", "hex"); const evmChainId = 37; //some random number that's not 1 (== WORMHOLE_SOLANA_CHAIN_ID) @@ -84,33 +75,30 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { await deployment(LOCAL, { print: false }); - const routingProxy = new RoutingWrapper(await getRoutingProxy()); + const routing = await RoutingWrapper.create(); - const swimUsd = await TokenWrapper.create( - await ethers.getContractAt("ERC20Token", await routingProxy.contract.swimUsdAddress()) - ); + const { swimUsd } = routing; - const [usdc, usdt] = await Promise.all( - LOCAL.pools[0].tokens.map(async (token) => await TokenWrapper.create(await getToken(token))) - ); - - const pool = await PoolWrapper.create(LOCAL.pools[0].salt, [swimUsd, usdc, usdt]); + const pool = await PoolWrapper.create(LOCAL.pools[0].salt); - for (const token of pool.tokens) - await token.mint(liquidityProvider, token.toAtomic(liquidityProviderFunds)); + for (const token of pool.tokens) await token.mint(liquidityProvider, liquidityProviderFunds); - await pool.add(liquidityProvider, pool.toAtomicAmounts(baseAmount), 0); - await usdc.mint(user, usdc.toAtomic(1)); + await pool.add( + liquidityProvider, + pool.tokens.map(() => baseAmount), + 0 + ); + await pool.tokens[userTokenIndex].mint(user, userFunds); const wormhole = (await getRegular("MockWormhole", [])) as IWormhole; const tokenBridge = await getRegular("MockTokenBridge", [wormhole.address]); - const createFakeVaa = (amount: BigNumber, targetChain: number, swimPayload: SwimPayload) => { + const createFakeVaa = (amount: Decimalish, targetChain: number, swimPayload: SwimPayload) => { const tokenBridgePayload = new TokenBridgePayload( 3, //payloadId 1 == transfer, 3 == transferWithPayload - amount, + swimUsd.toAtomic(amount), asBytes(SWIM_USD_SOLANA_ADDRESS, 32), WORMHOLE_SOLANA_CHAIN_ID, - asBytes(routingProxy.address, 32), + asBytes(routing.address, 32), targetChain, asBytes(tokenBridge.address, 32), swimPayload.encode() @@ -123,7 +111,7 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { 0, //timestamp 0, //nonce 0, //emitterChain - asBytes(routingProxy.address, 32), //emitterAddress + asBytes(routing.address, 32), //emitterAddress BigNumber.from(0), //sequence 15, //consistencyLevel tokenBridgePayload.encode() @@ -132,13 +120,13 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { }; const checkEmittedPayload = async ( - expectedAmount: BigNumber, + expectedAmount: Decimalish, targetChain: number, recipient: Buffer | HasAddress, propellerParams?: { readonly propellerEnabled: boolean; readonly gasKickstart: boolean; - readonly maxPropellerFee: BigNumber; + readonly maxPropellerFee: Decimalish; readonly toToken: TokenWrapper | number; readonly memo?: Buffer; } @@ -152,11 +140,11 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { const tokenBridgePayload = TokenBridgePayload.decode( Buffer.from(wormholeEvents[0].args[3].slice(2), "hex") ); - expect(tokenBridgePayload.amount).to.be.closeTo(expectedAmount, 2); + await expectCloseTo(swimUsd, swimUsd.toHuman(tokenBridgePayload.amount), expectedAmount); expect(asHex(tokenBridgePayload.originAddress)).to.equal(SWIM_USD_SOLANA_ADDRESS); expect(tokenBridgePayload.originChain).to.equal(WORMHOLE_SOLANA_CHAIN_ID); if (targetChain !== WORMHOLE_SOLANA_CHAIN_ID) - expect(asEvmAddress(tokenBridgePayload.targetAddress)).to.equal(routingProxy.address); + expect(asEvmAddress(tokenBridgePayload.targetAddress)).to.equal(routing.address); else expect(asHex(tokenBridgePayload.targetAddress)).to.equal(ROUTING_CONTRACT_SOLANA_ADDRESS); expect(tokenBridgePayload.targetChain).to.equal(targetChain); @@ -172,7 +160,11 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { if (propellerParams) { expect(swimPayload.propellerEnabled).to.equal(true); expect(swimPayload.gasKickstart).to.equal(propellerParams.gasKickstart); - expect(swimPayload.maxPropellerFee).to.equal(propellerParams.maxPropellerFee); + await expectEqual( + swimUsd, + swimUsd.toHuman(swimPayload.maxPropellerFee), + propellerParams.maxPropellerFee + ); expect(swimPayload.toTokenNumber).to.equal( typeof propellerParams.toToken === "number" ? propellerParams.toToken @@ -189,138 +181,190 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { liquidityProvider, user, pool, - routingProxy, + routing, + swimUsd, checkEmittedPayload, createFakeVaa, - swimUsd, - usdc, - usdt, }; } it("onChainSwap - correct defi outputs", async function () { - const { routingProxy, user, usdc, usdt } = await loadFixture(testFixture); - const expectedAmount = usdt.toAtomic("0.929849"); + const { routing, pool, govFeeRecip, user } = await loadFixture(testFixture); + + const inputIndex = userTokenIndex; + const inputAmount = userFunds; + const outputIndex = 2; + + const inputToken = pool.tokens[inputIndex]; + const outputToken = pool.tokens[outputIndex]; + + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, outputIndex); - await routingProxy.onChainSwap(user, usdc, usdc.toAtomic(1), user, usdt, 0); + await routing.onChainSwap( + user, + inputToken, + inputAmount, + user, + outputToken, + SWIM_USD_TOKEN_INDEX + ); - expect(await usdc.balanceOf(user)).to.equal(0); - expectCloseTo(usdt, await usdt.balanceOf(user), expectedAmount); + await expectEqual(pool.tokens[inputIndex], user, 0); + await expectCloseTo(pool.tokens[outputIndex], user, expected.stableOutputAmount); + await expectCloseTo(pool.lpToken, govFeeRecip, expected.governanceMintAmount); }); it("crossChainInitiate - correct defi outputs", async function () { - const { routingProxy, checkEmittedPayload, user, usdc, swimUsd } = await loadFixture( + const { routing, pool, govFeeRecip, user, checkEmittedPayload } = await loadFixture( testFixture ); - const expectedAmount = swimUsd.toAtomic("0.929849"); + const inputIndex = userTokenIndex; + const inputAmount = userFunds; const recipient = user; const targetChain = evmChainId; - await routingProxy.crossChainInitiate( + const inputToken = pool.tokens[inputIndex]; + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, SWIM_USD_TOKEN_INDEX); + + await routing.crossChainInitiate( user, - usdc, - usdc.toAtomic(1), + inputToken, + inputAmount, 0, targetChain, recipient, memo ); - expect((await routingProxy.getMemoInteractionEvents(memo)).length).to.equal(1); - await checkEmittedPayload(expectedAmount, targetChain, recipient); + await expectEqual(pool.tokens[inputIndex], user, 0); + await expectCloseTo(pool.lpToken, govFeeRecip, expected.governanceMintAmount); + expect((await routing.getMemoInteractionEvents(memo)).length).to.equal(1); + await checkEmittedPayload(expected.stableOutputAmount, targetChain, recipient); }); it("crossChainComplete - bridge only", async function () { - const { routingProxy, createFakeVaa, user, swimUsd } = await loadFixture(testFixture); + const { routing, user, swimUsd, createFakeVaa } = await loadFixture(testFixture); - const bridgedSwimUsd = swimUsd.toAtomic(1); + const bridgedSwimUsd = 1; const fakeVaa = createFakeVaa(bridgedSwimUsd, evmChainId, toSwimPayload(user)); - expect(await swimUsd.balanceOf(user)).to.equal(0); + await expectEqual(swimUsd, user, 0); - await routingProxy.crossChainComplete(user, fakeVaa, swimUsd, 0, memo); + await routing.crossChainComplete(user, fakeVaa, swimUsd, 0, memo); - expect(await swimUsd.balanceOf(user)).to.equal(bridgedSwimUsd); + await expectEqual(swimUsd, user, bridgedSwimUsd); }); it("crossChainComplete - bridge and swap", async function () { - const { routingProxy, createFakeVaa, user, swimUsd, usdt } = await loadFixture(testFixture); + const { routing, pool, user, govFeeRecip, swimUsd, createFakeVaa } = await loadFixture( + testFixture + ); - const bridgedSwimUsd = swimUsd.toAtomic(1); - const expectedAmount = usdt.toAtomic("0.929849"); - const fakeVaa = createFakeVaa(bridgedSwimUsd, evmChainId, toSwimPayload(user)); + const bridgedSwimUsd = 1; + const outputIndex = 2; + + const outputToken = pool.tokens[outputIndex]; + const inputAmounts = pool.tokens.map((_, i) => + i === SWIM_USD_TOKEN_INDEX ? bridgedSwimUsd : 0 + ); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, outputIndex); - expect(await swimUsd.balanceOf(user)).to.equal(0); + const fakeVaa = createFakeVaa(bridgedSwimUsd, evmChainId, toSwimPayload(user)); - await routingProxy.crossChainComplete(user, fakeVaa, usdt, 0, memo); + await routing.crossChainComplete(user, fakeVaa, outputToken, 0, memo); - expectCloseTo(usdt, await usdt.balanceOf(user), expectedAmount); + await expectEqual(swimUsd, user, 0); + await expectCloseTo(outputToken, user, expected.stableOutputAmount); + await expectCloseTo(pool.lpToken, govFeeRecip, expected.governanceMintAmount); }); it("propellerInitiate", async function () { - const { routingProxy, checkEmittedPayload, user, swimUsd, usdc } = await loadFixture( + const { routing, pool, govFeeRecip, user, swimUsd, checkEmittedPayload } = await loadFixture( testFixture ); - const expectedAmount = swimUsd.toAtomic("0.929849"); + const inputIndex = userTokenIndex; + const inputAmount = userFunds; const recipient = user; const targetChain = evmChainId; const propellerParams = { propellerEnabled: true, gasKickstart: false, - maxPropellerFee: swimUsd.toAtomic("0.2"), + maxPropellerFee: 0.2, toToken: swimUsd, memo: memo, }; + const inputToken = pool.tokens[inputIndex]; + const inputAmounts = pool.tokens.map((_, i) => (i === inputIndex ? inputAmount : 0)); + const expected = (await pool.poolmath()).swapExactInput(inputAmounts, SWIM_USD_TOKEN_INDEX); - await routingProxy.propellerInitiate( + await routing.propellerInitiate( user, - usdc, - usdc.toAtomic(1), + inputToken, + inputAmount, targetChain, recipient, propellerParams.gasKickstart, propellerParams.maxPropellerFee, - propellerParams.toToken.tokenNumber, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + propellerParams.toToken.tokenNumber!, propellerParams.memo ); - expect((await routingProxy.getMemoInteractionEvents(memo)).length).to.equal(1); - await checkEmittedPayload(expectedAmount, evmChainId, recipient, propellerParams); + await expectEqual(inputToken, user, 0); + await expectEqual(swimUsd, user, 0); + await expectCloseTo(pool.lpToken, govFeeRecip, expected.governanceMintAmount); + expect((await routing.getMemoInteractionEvents(memo)).length).to.equal(1); + await checkEmittedPayload(expected.stableOutputAmount, evmChainId, recipient, propellerParams); }); it("propellerComplete - fixedGasPrice", async function () { - const { routingProxy, createFakeVaa, liquidityProvider, user, swimUsd } = await loadFixture( - testFixture - ); + const { routing, liquidityProvider, governance, user, swimUsd, createFakeVaa } = + await loadFixture(testFixture); - const feeConfig = await routingProxy.propellerFeeConfig(); - expect(feeConfig.method).to.equal(0); + const fixedSwimUsdPerGasToken = 10; - const bridgedSwimUsd = swimUsd.toAtomic(1); - const maxPropellerFee = swimUsd.toAtomic("0.2"); + await routing.usePropellerFixedGasTokenPrice(governance, fixedSwimUsdPerGasToken); + + const feeConfig = await routing.propellerFeeConfig(); + expect(feeConfig.method).to.equal("fixedSwimUsdPerGasToken"); + expect(feeConfig.fixedSwimUsdPerGasToken.toNumber()).to.equal(fixedSwimUsdPerGasToken); + + const bridgedSwimUsd = 1; + const maxPropellerFee = 0.2; const fakeVaa = createFakeVaa( bridgedSwimUsd, evmChainId, - toSwimPayload(user, true, false, maxPropellerFee, swimUsd.tokenNumber, memo) + toSwimPayload(user, true, false, swimUsd.toAtomic(maxPropellerFee), swimUsd.tokenNumber, memo) ); - expect(await swimUsd.balanceOf(user)).to.equal(0); + await expectEqual(swimUsd, user, 0); - const { gasUsed, effectiveGasPrice } = await routingProxy.propellerComplete( + const { gasUsed, effectiveGasPrice } = await routing.propellerComplete( liquidityProvider, fakeVaa ); - const expectedAmount = bridgedSwimUsd.sub(feeConfig.serviceFee).sub( - gasUsed - .mul(effectiveGasPrice.add(10 ** 9)) + //already contains ~1 gwei tip apparently, so we don't add PROPELLER_GAS_TIP + const gasCostInGasTokens = gasUsed.mul(effectiveGasPrice); + + const expectedSwimUsdFee = feeConfig.serviceFee.add( + toDecimal(gasCostInGasTokens.toString()) .mul(feeConfig.fixedSwimUsdPerGasToken) - .div(BigNumber.from(10).pow(18)) + .div(toDecimal(10).pow(GAS_TOKEN_DECIMALS)) ); - expectCloseTo(swimUsd, await swimUsd.balanceOf(user), expectedAmount, 10 ** 4); + const expectedAmount = toDecimal(bridgedSwimUsd).sub(expectedSwimUsdFee); + + // console.log("actualGasUsed", gasUsed); + // console.log("expected:", expectedAmount); + // console.log(" actual:", await swimUsd.balanceOf(user)); + + //TODO messy check + await expectCloseTo(swimUsd, user, expectedAmount, 100); }); //A word on uniswap sqrt prices - we can get a realistic price from: @@ -338,103 +382,146 @@ describe("Routing CrossChain and Propeller Defi Operations", function () { // BigNumber.from(10).pow(12).div(sqrtPrice.pow(2).div(BigNumber.from(2).pow(2 * 96)) it("propellerComplete - uniswapOracle", async function () { - const { pool, routingProxy, createFakeVaa, deployer, liquidityProvider, user, swimUsd, usdc } = + const { routing, pool, deployer, liquidityProvider, user, swimUsd, createFakeVaa } = await loadFixture(testFixture); + const bridgedSwimUsd = 1; + const maxPropellerFee = 0.2; + const withSwap = false; + const usdcIsFirst = true; + const usdcPerEthHuman = 10; + const skewBalances = true; + const withDebugOutput = false; + + const usdc = pool.tokens[1]; + const outputIndex = withSwap ? 2 : 0; + const outputToken = pool.tokens[outputIndex]; //cheapen swimUSD as compared to the other tokens - await pool.removeExactOutput( - liquidityProvider, - pool.toAtomicAmounts([0, 9, 9]), - pool.lpToken.toAtomic(baseAmount).mul(3) + if (skewBalances) await pool.removeExactOutput(liquidityProvider, [0, 9, 9], baseAmount * 3); + + const tenToThe = (exp: number) => toDecimal(10).pow(exp); + + //usdcIsFirst -> WETH/USDC (unintuitive) rather than USDC/WETH (intuitive) + //realistically this would be WETH but for testing we don't care + const wethAddr = "0x" + "00".repeat(20); + const addrs = usdcIsFirst + ? ([usdc.address, wethAddr] as const) + : ([wethAddr, usdc.address] as const); + + const sqrtPrice = Math.pow(usdcPerEthHuman, 0.5 - (usdcIsFirst ? 1 : 0)); + const sqrtPriceX96 = toDecimal(sqrtPrice).mul(toDecimal(2).pow(96)); + // const uniswapPrice = usdcIsFirst + // ? sqrtPriceX96.mul(tenToThe(swimUsd.decimals)).div(tenToThe(GAS_TOKEN_DECIMALS)) + // : sqrtPriceX96.mul(tenToThe(GAS_TOKEN_DECIMALS)).div(tenToThe(swimUsd.decimals)); + const uniswapPrice = sqrtPriceX96.mul( + //the final div 2 is because we are taking the square root and hence the decimal + // difference is also halfed! + tenToThe(((GAS_TOKEN_DECIMALS - swimUsd.decimals) * (usdcIsFirst ? 1 : -1)) / 2) ); - const ethDecimals = 18; - - const deployMockUniswap = async (intuitiveUsdcPerEth: number, usdcIsFirst: boolean) => { - //usdcIsFirst -> WETH/USDC (unintuitive) rather than USDC/WETH (intuitive) - //realistically this would be WETH but for testing we don't care - const wethAddr = "0x" + "00".repeat(20); - const addrs = usdcIsFirst - ? ([usdc.address, wethAddr] as const) - : ([wethAddr, usdc.address] as const); - - const sqrtPrice = Math.pow(intuitiveUsdcPerEth, 0.5 - (usdcIsFirst ? 1 : 0)); - const sqrtPriceBN = asBigNumber(sqrtPrice.toString(), 18) - .mul(BigNumber.from(2).pow(96)) - .div(tenToThe(18)); - const factor = tenToThe(ethDecimals / 2 - Math.floor(usdc.decimals / 2)).mul( - usdc.decimals % 2 == 1 ? asBigNumber(Math.sqrt(10).toString(), 18) : tenToThe(18) - ); - - const uniswapPrice = usdcIsFirst - ? sqrtPriceBN.mul(factor).div(tenToThe(18)) - : sqrtPriceBN.mul(tenToThe(18)).div(factor); - - const mockUniswap = await ( - await ethers.getContractFactory("MockUniswapV3Pool") - ).deploy(...addrs, uniswapPrice); - await mockUniswap.deployed(); - return mockUniswap; - }; - - const intuitiveUsdcPerEth = 100; - const mockUniswap = await deployMockUniswap(intuitiveUsdcPerEth, false); - const usdcPerEth = ethers.utils.formatUnits(intuitiveUsdcPerEth, ethDecimals - usdc.decimals); + const mockUniswap = await ( + await ethers.getContractFactory("MockUniswapV3Pool") + ).deploy(...addrs, uniswapPrice.trunc().toFixed()); + await mockUniswap.deployed(); + const usdcPerEthAtomic = toDecimal(usdcPerEthHuman).mul( + tenToThe(usdc.decimals - GAS_TOKEN_DECIMALS) + ); - await routingProxy.usePropellerUniswapOracle(deployer, usdc, mockUniswap); + await routing.usePropellerUniswapOracle(deployer, usdc, mockUniswap); - const feeConfig = await routingProxy.propellerFeeConfig(); - expect(feeConfig.method).to.equal(1); + const feeConfig = await routing.propellerFeeConfig(); + expect(feeConfig.method).to.equal("uniswapOracle"); - const bridgedSwimUsd = swimUsd.toAtomic(1); - const maxPropellerFee = swimUsd.toAtomic("0.2"); const fakeVaa = createFakeVaa( bridgedSwimUsd, evmChainId, - toSwimPayload(user, true, false, maxPropellerFee, swimUsd.tokenNumber, memo) + toSwimPayload( + user, + true, + false, + swimUsd.toAtomic(maxPropellerFee), + outputToken.tokenNumber, + memo + ) ); - expect(await swimUsd.balanceOf(user)).to.equal(0); - - const poolMarginalPrices = await pool.getMarginalPrices(); - - const { gasUsed, effectiveGasPrice } = await routingProxy.propellerComplete( - liquidityProvider, - fakeVaa - ); + await expectEqual(swimUsd, user, 0); + + const poolmath = await pool.poolmath(); + const poolMarginalPricesAtomic = await pool.getMarginalPrices(); + + const receipt = await routing.propellerComplete(liquidityProvider, fakeVaa); + const { gasUsed, effectiveGasPrice } = receipt; + + const swimUsdPerUsdcAtomic = poolMarginalPricesAtomic[1].div(poolMarginalPricesAtomic[0]); + const swimUsdPerGasTokenAtomic = swimUsdPerUsdcAtomic.mul(usdcPerEthAtomic); + const gasTokenCostWei = toDecimal(gasUsed.mul(effectiveGasPrice).toString()); + const expectedSwimUsdGasFeeAtomic = gasTokenCostWei.mul(swimUsdPerGasTokenAtomic); + const expectedSwimUsdGasFee = swimUsd.toHuman(expectedSwimUsdGasFeeAtomic.trunc().toString()); + const uncheckedSwimUsdFee = feeConfig.serviceFee.add(expectedSwimUsdGasFee); + const checkedSwimUsdFee = Decimal.min(uncheckedSwimUsdFee, maxPropellerFee, bridgedSwimUsd); + + const expectedSwimUsd = toDecimal(bridgedSwimUsd).sub(checkedSwimUsdFee); + + const expectedBalance = + outputToken.tokenNumber === swimUsd.tokenNumber + ? expectedSwimUsd + : poolmath + .swapExactInput( + pool.tokens.map((_, i) => (i === 0 ? expectedSwimUsd : 0)), + outputIndex + ) + .stableOutputAmount.toFixed(outputToken.decimals); + + if (withDebugOutput) { + const actualSwimUsd = swimUsd.toHuman( + receipt.logs.find( + (log) => + log.topics[0] === + ethers.utils.keccak256( + ethers.utils.toUtf8Bytes("Transfer(address,address,uint256)") + ) && + log.topics[1] === asHex(asBytes(routing.address, 32)) && + log.topics[2] === asHex(asBytes(pool.address, 32)) + )?.data ?? "0" + ); + const swimUsdPerGasTokenHuman = swimUsdPerGasTokenAtomic.mul( + tenToThe(GAS_TOKEN_DECIMALS - swimUsd.decimals) + ); + console.table( + [ + ["actual gas used", "gas", "receipt", gasUsed.toString()], + ["actual gas price", "wei", "receipt", effectiveGasPrice.toString()], + ["actual gas cost", "wei", "receipt*", gasTokenCostWei], + ...poolMarginalPricesAtomic.map((mp, i) => [ + `marginal ${pool.tokens[i].symbol}/LP`, + "atomic", + "pool on-chain", + mp, + ]), + ["swimUSD/USDC", "atomic", "pool on-chain*", swimUsdPerUsdcAtomic], + ["USDC/ETH", "human", "test", usdcPerEthHuman], + ["USDC/ETH", "atomic", "test", usdcPerEthAtomic], + ["SwimUSD/ETH", "human", "pool on-chain*", swimUsdPerGasTokenHuman], + ["SwimUSD/ETH", "atomic", "pool on-chain*", swimUsdPerGasTokenAtomic], + ["swimUSD serive fee", "human", "on-chain", feeConfig.serviceFee], + ["expected swimUSD gas fee", "human", "pool on-chain*", expectedSwimUsdGasFee], + ["expected unchecked SwimUSD fee", "human", "pool on-chain*", uncheckedSwimUsdFee], + ["max Propeller fee", "human", "test", maxPropellerFee], + ["expected checked SwimUSD fee", "human", "pool on-chain*", checkedSwimUsdFee], + ["bridged SwimUSD", "human", "test", bridgedSwimUsd], + ["expected SwimUSD", "human", "pool on-chain*", expectedSwimUsd], + ["actual SwimUSD", "human", "receipt", actualSwimUsd], + ["expected balance", "human", "on-chain", expectedBalance], + ["actual balance", "human", "on-chain", await outputToken.balanceOf(user)], + ].map((e) => { + const asDec = toDecimal(e[3]); + const value = asDec.gt(1e-6) ? asDec.toFixed(6) : asDec.toExponential(2); + return { what: e[0], unit: e[1], source: e[2], value: value.padStart(25) }; + }) + ); + } - const swimUsdPerUsdc = ethers.utils.formatUnits( - asBigNumber(poolMarginalPrices[1], 36).div(asBigNumber(poolMarginalPrices[0], 18)), - 18 - ); - const swimUsdPerGasToken = ethers.utils.formatUnits( - asBigNumber(swimUsdPerUsdc, 18).mul(asBigNumber(usdcPerEth, 18)), - 36 - ); - const remuneratedGasPrice = effectiveGasPrice; //already contains ~1 gwei tip apparently - const gasTokenCost = gasUsed.mul(remuneratedGasPrice); - const expectedSwimUsdGasFee = gasTokenCost - .mul(asBigNumber(swimUsdPerGasToken, 18)) - .div(tenToThe(18)); - - const expectedBalance = bridgedSwimUsd.sub(feeConfig.serviceFee).sub(expectedSwimUsdGasFee); - // console.log("------------------------------------"); - // console.log(" actual gas used:", gasUsed); - // console.log("effectiveGasPrice:", effectiveGasPrice); - // console.log(" actual gas cost:", ethers.utils.formatEther(gasUsed.mul(effectiveGasPrice))); - // console.log("------------------------------------"); - // console.log("remuneratedGasP:", remuneratedGasPrice); - // console.log(" gasTokenCost:", gasTokenCost); - // console.log("marginal Prices:", poolMarginalPrices); - // console.log(" swimUSD/USDC:", swimUsdPerUsdc); - // console.log(" USDC/ETH:", usdcPerEth); - // console.log(" SwimUSD/ETH:", swimUsdPerGasToken); - // console.log("swimUSD gas fee:", expectedSwimUsdGasFee); - // console.log("------------------------------------"); - // console.log("balance SwimUSD:", await swimUsd.balanceOf(user)); - // console.log("balance usdc:", await usdc.balanceOf(user)); - // console.log("expected:", expectedBalance); - - expectCloseTo(swimUsd, await swimUsd.balanceOf(user), expectedBalance, 10 ** 4); + await expectCloseTo(outputToken, user, expectedBalance, 5000); }); }); From bd33c786b4906711860d0cf5cbba6884fd044990 Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 04:38:55 -0700 Subject: [PATCH 05/14] feat: add .env example --- packages/evm-contracts/.env.example | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/evm-contracts/.env.example diff --git a/packages/evm-contracts/.env.example b/packages/evm-contracts/.env.example new file mode 100644 index 000000000..e4c61d548 --- /dev/null +++ b/packages/evm-contracts/.env.example @@ -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 +ETHERSCAN_API_KEY= From b263a7f5ab2784f346a8e84a31cf22251f98a05a Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 04:39:40 -0700 Subject: [PATCH 06/14] test: implement, deploy, and fund faucet contract for test tokens on dev nets --- .../evm-contracts/contracts/test/Faucet.sol | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 packages/evm-contracts/contracts/test/Faucet.sol diff --git a/packages/evm-contracts/contracts/test/Faucet.sol b/packages/evm-contracts/contracts/test/Faucet.sol new file mode 100644 index 000000000..a64adbe37 --- /dev/null +++ b/packages/evm-contracts/contracts/test/Faucet.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.15; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +contract Faucet { + + struct Requester { + uint16 index; + uint8 count; + uint64 blocknumber; + } + + using SafeERC20 for IERC20; + + uint private constant MAX_TOKEN_COUNT = 6; + uint8 private constant MAX_REQUESTS = 10; + + address private _owner; + IERC20[MAX_TOKEN_COUNT] public tokens; + uint[MAX_TOKEN_COUNT] public amounts; + address[] public requesters; + + mapping(address => Requester) public requesterMapping; + + constructor(address owner_) { + _owner = owner_; + } + + function setup(address[] calldata tokens_, uint[] calldata amounts_) external { + require(msg.sender == _owner, "computer says no"); + require(tokens_.length <= MAX_TOKEN_COUNT, "what are you doing"); + require(tokens_.length == amounts_.length, "length mismatch"); + for (uint i = 0; i < tokens_.length; ++i) { + tokens[i] = IERC20(tokens_[i]); + amounts[i] = amounts_[i]; + } + } + + fallback() external { + Requester storage requester = requesterMapping[tx.origin]; + if (requester.count == 0) { + uint index = requesters.length; + requesters.push(tx.origin); + requester.index = uint16(index); + requester.count = 1; + requester.blocknumber = uint64(block.number); + } + else { + require(requester.blocknumber != block.number, "You Can't Get Ye Flask"); + require(requester.count < MAX_REQUESTS, "Greed is a Sin"); + requester.blocknumber = uint64(block.number); + ++requester.count; + } + + for (uint i = 0; i < MAX_TOKEN_COUNT; ++i) + if (address(tokens[i]) != address(0)) + tokens[i].safeTransfer(msg.sender, amounts[i]); + } +} From 66d6015382f853e13d2f91c246d8c593d2c783af Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 04:40:25 -0700 Subject: [PATCH 07/14] chore: revisit Propeller deterministic sandwich attack issue --- docs/poolmath/frontrunning.py | 75 +++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 docs/poolmath/frontrunning.py diff --git a/docs/poolmath/frontrunning.py b/docs/poolmath/frontrunning.py new file mode 100755 index 000000000..621120d35 --- /dev/null +++ b/docs/poolmath/frontrunning.py @@ -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): + 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") From 4157f6da32db2fd86bad466bff79e839e3e1fef2 Mon Sep 17 00:00:00 2001 From: swimivan Date: Tue, 25 Oct 2022 04:46:41 -0700 Subject: [PATCH 08/14] fix: add missing yarn.lock --- yarn.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yarn.lock b/yarn.lock index 9170f8b16..b8730a6ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7384,6 +7384,8 @@ __metadata: "@openzeppelin/contracts-upgradeable": ^4.7.3 "@openzeppelin/hardhat-upgrades": ^1.20.0 "@swim-io/eslint-config": "workspace:^" + "@swim-io/pool-math": "workspace:^" + "@swim-io/token-projects": "workspace:^" "@swim-io/tsconfig": "workspace:^" "@typechain/ethers-v5": ^10.1.0 "@typechain/hardhat": ^6.1.2 @@ -7394,6 +7396,7 @@ __metadata: "@typescript-eslint/parser": ^5.38.1 chai: ^4.3.6 chai-bn: ^0.3.1 + decimal.js: ^10.3.1 dotenv: ^16.0.1 eslint: ^8.22.0 eslint-config-prettier: ^8.5.0 From 9774e6ad68669e5487023f3d6df07da1f5e9cc81 Mon Sep 17 00:00:00 2001 From: swimivan Date: Wed, 26 Oct 2022 18:07:53 -0700 Subject: [PATCH 09/14] fix: Fix solidity lint issues --- packages/evm-contracts/contracts/Invariant.sol | 4 ++-- packages/evm-contracts/contracts/Pool.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/evm-contracts/contracts/Invariant.sol b/packages/evm-contracts/contracts/Invariant.sol index 9c731927c..3b8cb80b0 100644 --- a/packages/evm-contracts/contracts/Invariant.sol +++ b/packages/evm-contracts/contracts/Invariant.sol @@ -14,8 +14,8 @@ library Invariant { using CenterAlignment for uint; - uint constant MARGINAL_PRICE_MULTIPLIER = 10**MARGINAL_PRICE_DECIMALS; - uint constant ONE_AMP_SHIFTED = 1 << AMP_SHIFT; + 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). diff --git a/packages/evm-contracts/contracts/Pool.sol b/packages/evm-contracts/contracts/Pool.sol index 176bb4009..3ccf1873c 100644 --- a/packages/evm-contracts/contracts/Pool.sol +++ b/packages/evm-contracts/contracts/Pool.sol @@ -583,8 +583,8 @@ contract Pool is IPool, Initializable, UUPSUpgradeable { toExternalAmpValue(uint32(threshold)) ); } - // solhint-disable-next-line not-rely-on-time _ampInitialValue = uint32(currentAmpFactor); + // solhint-disable-next-line not-rely-on-time _ampInitialTimestamp = uint32(block.timestamp); _ampTargetValue = uint32(ampTargetValue); _ampTargetTimestamp = targetTimestamp; From ab786153339906dc226a5c2e0a5a3ecfd8953a73 Mon Sep 17 00:00:00 2001 From: swimivan Date: Thu, 27 Oct 2022 01:32:39 -0700 Subject: [PATCH 10/14] fix: Fix TypeScript lint issue --- packages/evm-contracts/src/testUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/evm-contracts/src/testUtils.ts b/packages/evm-contracts/src/testUtils.ts index 4347bdf8a..f5bb3dcf8 100644 --- a/packages/evm-contracts/src/testUtils.ts +++ b/packages/evm-contracts/src/testUtils.ts @@ -104,9 +104,7 @@ export class PoolWrapper { pool, await Promise.all( state.balances.map(async (balance) => - TokenWrapper.create( - (await ethers.getContractAt("ERC20Token", balance.tokenAddress)) as ERC20Token - ) + TokenWrapper.create(await ethers.getContractAt("ERC20Token", balance.tokenAddress)) ) ), await TokenWrapper.create( From f18e5d3b78f78800d1b8c4fd412586870f3883b9 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 27 Oct 2022 12:01:53 +0300 Subject: [PATCH 11/14] refactor(evm-contracts): build workspace deps --- packages/evm-contracts/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/evm-contracts/package.json b/packages/evm-contracts/package.json index 54fc6a774..c480ea9f0 100644 --- a/packages/evm-contracts/package.json +++ b/packages/evm-contracts/package.json @@ -33,7 +33,8 @@ "coverage": "hardhat coverage", "typecheck": "tsc", "verify": "yarn typecheck && yarn format:check && yarn lint && yarn test", - "build": "rm -rf ./build/ ./types/ ./tsconfig.tsbuildinfo && yarn clean && yarn compile && tsc", + "build": "rm -rf ./build/ ./types/ ./tsconfig.tsbuildinfo && yarn clean && yarn compile && yarn build:ts", + "build:ts": "yarn workspaces foreach --topological-dev --parallel --recursive --from @swim-io/evm-contracts run build", "prepare": "yarn verify && yarn build" }, "devDependencies": { From 96106475a00f859850dffab583f13dd79ca2c55d Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Thu, 27 Oct 2022 12:51:58 +0300 Subject: [PATCH 12/14] fix(evm-contracts): build command for CI --- .github/workflows/evm-contracts-verify.yaml | 2 +- packages/evm-contracts/package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/evm-contracts-verify.yaml b/.github/workflows/evm-contracts-verify.yaml index 151b79cce..d487ac23f 100644 --- a/.github/workflows/evm-contracts-verify.yaml +++ b/.github/workflows/evm-contracts-verify.yaml @@ -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" diff --git a/packages/evm-contracts/package.json b/packages/evm-contracts/package.json index c480ea9f0..54fc6a774 100644 --- a/packages/evm-contracts/package.json +++ b/packages/evm-contracts/package.json @@ -33,8 +33,7 @@ "coverage": "hardhat coverage", "typecheck": "tsc", "verify": "yarn typecheck && yarn format:check && yarn lint && yarn test", - "build": "rm -rf ./build/ ./types/ ./tsconfig.tsbuildinfo && yarn clean && yarn compile && yarn build:ts", - "build:ts": "yarn workspaces foreach --topological-dev --parallel --recursive --from @swim-io/evm-contracts run build", + "build": "rm -rf ./build/ ./types/ ./tsconfig.tsbuildinfo && yarn clean && yarn compile && tsc", "prepare": "yarn verify && yarn build" }, "devDependencies": { From 4c910bded9917326951213c6c74ccb348829ec41 Mon Sep 17 00:00:00 2001 From: swimivan Date: Thu, 27 Oct 2022 06:33:26 -0700 Subject: [PATCH 13/14] fix: Incorporating PR comments --- packages/evm-contracts/contracts/Routing.sol | 25 +------ packages/evm-contracts/hardhat.config.ts | 74 ++++++++++++-------- packages/evm-contracts/src/testUtils.ts | 2 +- 3 files changed, 49 insertions(+), 52 deletions(-) diff --git a/packages/evm-contracts/contracts/Routing.sol b/packages/evm-contracts/contracts/Routing.sol index 2394ac26a..11d87b2eb 100644 --- a/packages/evm-contracts/contracts/Routing.sol +++ b/packages/evm-contracts/contracts/Routing.sol @@ -36,13 +36,16 @@ contract Routing is struct PropellerFeeConfig { GasTokenPriceMethod method; + //service fee specified in swimUSD uint64 serviceFee; + //specified in atomic with 18 decimals, i.e. how many atomic swimUSD per 1 wei? // assuming a gas token price of 1 (human) swimUSD / 1 (human) gas token where swimUSD has // 6 decimals and gas token has 18 decimals means 10^-12 atomic swimUSD per 1 wei gas token // and thus taking 18 decimals into account fixedSwimUsdPerGasToken would equal 10^6 uint fixedSwimUsdPerGasToken; + UniswapOracleParams uniswap; } @@ -254,27 +257,6 @@ contract Routing is swimUsdAddress_ ); - //old way of not going through the Solana Routing contract but doing a direct transfer - // leaving in for testing purposes / as a workaround for now. - // if (wormholeRecipientChain == WORMHOLE_SOLANA_CHAIN_ID) { - // IERC20(swimUsdAddress_).safeApprove(address(tokenBridge), swimUsdAmount); - - // try - // tokenBridge.transferTokens{value: msg.value}( - // swimUsdAddress_, - // swimUsdAmount, - // WORMHOLE_SOLANA_CHAIN_ID, - // toOwner, - // 0, //arbiterFee - // wormholeNonce - // ) - // returns (uint64 _wormholeSequence) { - // wormholeSequence = _wormholeSequence; - // } catch (bytes memory lowLevelData) { - // revert WormholeInteractionFailed(lowLevelData); - // } - // ++wormholeNonce; - // } else { wormholeSequence = wormholeTransferWithPayload( swimUsdAmount, wormholeRecipientChain, @@ -282,7 +264,6 @@ contract Routing is wormholeNonce, swimUsdAddress_ ); - // } } function propellerInitiate( diff --git a/packages/evm-contracts/hardhat.config.ts b/packages/evm-contracts/hardhat.config.ts index f84adf439..8f755452c 100644 --- a/packages/evm-contracts/hardhat.config.ts +++ b/packages/evm-contracts/hardhat.config.ts @@ -36,12 +36,24 @@ task("deploy", "Run the deployment script", async (_, hre) => { task( "update", "Updates a given proxy contract to a new implementation via updateTo", - async ({ proxy, logic, owner }, hre) => { + async ( + { + proxy, + logic, + owner, + }: { readonly proxy: string; readonly logic: string; readonly owner?: string }, + hre + ) => { const { ethers } = hre; - const _owner = owner ? await ethers.getSigner(owner as string) : (await ethers.getSigners())[0]; - //TODO check that proxy and logic exist - const _proxy = (await ethers.getContractAt("BlankLogic", proxy as string)).connect(_owner); - await (await _proxy.upgradeTo(logic as string)).wait(); + const requireExists = async (address: string) => { + if ((await ethers.provider.getCode(address)).length <= 2) + throw Error(`No contract deployed at ${address}`); + }; + await Promise.all([proxy, logic].map(requireExists)); + + const _owner = owner ? await ethers.getSigner(owner) : (await ethers.getSigners())[0]; + const _proxy = (await ethers.getContractAt("BlankLogic", proxy)).connect(_owner); + await (await _proxy.upgradeTo(logic)).wait(); } ) .addPositionalParam("proxy", "address of the proxy that will be upgraded") @@ -51,34 +63,38 @@ task( ) .addOptionalPositionalParam("owner", "owner who's authorized to execute the upgrade", ""); -task("pool-state", "Print state of given pool", async ({ pool }, { ethers }) => { - const [isPaused, balances, lpSupply, ampFactorDec, lpFeeDec, govFeeDec] = await ( - await ethers.getContractAt("Pool", pool as string) - ).getState(); - const decimaltoFixed = (decimal: readonly [BigNumber, number]) => - formatFixed(decimal[0], decimal[1]); - const getDecimals = async (address: string) => - (await ethers.getContractAt("ERC20", address)).decimals(); - const toTokenInfo = async (token: readonly [string, BigNumber]) => ({ - address: token[0], - amount: decimaltoFixed([token[1], await getDecimals(token[0])]), - }); - const state = { - isPaused, - balances: await Promise.all(balances.map(toTokenInfo)), - lpSupply: await toTokenInfo(lpSupply), - ampFactor: decimaltoFixed(ampFactorDec), - lpFee: decimaltoFixed(lpFeeDec), - govFee: decimaltoFixed(govFeeDec), - }; - console.log(JSON.stringify(state, null, 2)); -}).addPositionalParam("pool", "Address of the pool"); +task( + "pool-state", + "Print state of given pool", + async ({ pool }: { readonly pool: string }, { ethers }) => { + const [isPaused, balances, lpSupply, ampFactorDec, lpFeeDec, govFeeDec] = await ( + await ethers.getContractAt("Pool", pool) + ).getState(); + const decimaltoFixed = (decimal: readonly [BigNumber, number]) => + formatFixed(decimal[0], decimal[1]); + const getDecimals = async (address: string) => + (await ethers.getContractAt("ERC20", address)).decimals(); + const toTokenInfo = async (token: readonly [string, BigNumber]) => ({ + address: token[0], + amount: decimaltoFixed([token[1], await getDecimals(token[0])]), + }); + const state = { + isPaused, + balances: await Promise.all(balances.map(toTokenInfo)), + lpSupply: await toTokenInfo(lpSupply), + ampFactor: decimaltoFixed(ampFactorDec), + lpFee: decimaltoFixed(lpFeeDec), + govFee: decimaltoFixed(govFeeDec), + }; + console.log(JSON.stringify(state, null, 2)); + } +).addPositionalParam("pool", "Address of the pool"); task( "selectors", "Print the selectors of all functions, events, and errors of a given contract", - async ({ name }, { ethers }) => { - const interfce = (await ethers.getContractFactory(name as string)).interface; + async ({ name }: { readonly name: string }, { ethers }) => { + const interfce = (await ethers.getContractFactory(name)).interface; const printType = (type: "functions" | "events" | "errors") => console.log( type + ":\n", diff --git a/packages/evm-contracts/src/testUtils.ts b/packages/evm-contracts/src/testUtils.ts index f5bb3dcf8..66b0e9d54 100644 --- a/packages/evm-contracts/src/testUtils.ts +++ b/packages/evm-contracts/src/testUtils.ts @@ -42,7 +42,7 @@ export class TokenWrapper { ? 0 : Object.values(TokenProjectId) .map((id) => TOKEN_PROJECTS_BY_ID[id]) - //TODO we're using includes() and lenght checking here instead of === here because e.g. + //TODO we're using includes() and length checking here instead of === here because e.g. // on Avalanche USDC is called aUSDC... ultimately a better solution than this is // obviously required to dynamically look up tokennumbers .filter( From 9843e551467b32931a421e960f2c0cb65068a010 Mon Sep 17 00:00:00 2001 From: swimivan Date: Fri, 28 Oct 2022 00:03:29 -0700 Subject: [PATCH 14/14] fix: fix typo in comment --- packages/evm-contracts/hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm-contracts/hardhat.config.ts b/packages/evm-contracts/hardhat.config.ts index 8f755452c..e15613445 100644 --- a/packages/evm-contracts/hardhat.config.ts +++ b/packages/evm-contracts/hardhat.config.ts @@ -12,7 +12,7 @@ import { task } from "hardhat/config"; import type { HardhatUserConfig, HttpNetworkUserConfig } from "hardhat/types"; dotenv.config(); -//update .env.examples if you add additional environment variables! +//update .env.example if you add additional environment variables! const { FACTORY_MNEMONIC, MNEMONIC, ETHERSCAN_API_KEY } = process.env; task(