Skip to content

Commit

Permalink
feat: calculate amountOutMin to minimize slippage
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Aug 25, 2021
1 parent b7c2b67 commit eec37d3
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 33 deletions.
77 changes: 47 additions & 30 deletions app/src/store/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
BigNumber,
BigNumberish,
BytesLike,
Contract,
ContractTransaction,
hexDataSlice,
Expand Down Expand Up @@ -193,7 +194,7 @@ export default function useCartStore() {
*/
async function checkout() {
const { signer, userAddress } = useWalletStore();
const { swaps, donations, deadline } = cartDonationInputs.value;
const { swaps, donations, deadline } = await getCartDonationInputs();
const manager = new Contract(GRANT_ROUND_MANAGER_ADDRESS, GRANT_ROUND_MANAGER_ABI, signer.value);
const getInputToken = (swap: SwapSummary) => getAddress(hexDataSlice(swap.path, 0, 20));

Expand Down Expand Up @@ -224,42 +225,20 @@ export default function useCartStore() {
clearCart();
}

// --- Getters ---
/**
* @notice Returns true if the provided grantId is in the cart, false otherwise
* @param grantId Grant ID to check
*/
function isInCart(grantId: BigNumberish): boolean {
const grantIds = lsCart.value.map((item) => item.grantId);
return grantIds.includes(toString(grantId));
}

/**
* @notice Convert a cart into an array of objects summarizing the cart info, with human-readable values
* @returns Object where keys are token addresses, values are total amount of that token in cart
*/
const cartSummary = computed((): Record<keyof typeof SUPPORTED_TOKENS_MAPPING, number> => {
const output: Record<keyof typeof SUPPORTED_TOKENS_MAPPING, number> = {};
for (const item of cart.value) {
const tokenAddress = item.contributionToken.address;
if (tokenAddress in output) output[tokenAddress] += item.contributionAmount;
else output[tokenAddress] = item.contributionAmount;
}
return output;
});

/**
* @notice Takes an array of cart items and returns inputs needed for the GrantRoundManager.donate() method
*/
const cartDonationInputs = computed((): { swaps: SwapSummary[]; donations: Donation[]; deadline: number } => {
async function getCartDonationInputs(): Promise<{ swaps: SwapSummary[]; donations: Donation[]; deadline: number }> {
// Get the swaps array
const swaps: SwapSummary[] = Object.keys(cartSummary.value).map((tokenAddress) => {
const swapPromises = Object.keys(cartSummary.value).map(async (tokenAddress) => {
const decimals = SUPPORTED_TOKENS_MAPPING[tokenAddress].decimals;
const amountIn = parseUnits(String(cartSummary.value[tokenAddress]), decimals);
const amountOutMin = '1'; // TODO improve this
const path = SWAP_PATHS[<keyof typeof SWAP_PATHS>tokenAddress];
// Use Uniswap's Quoter.sol to get amountOutMin, unless the path indicates so swap is required
const amountOutMin = path.length === 42 ? amountIn : await quoteExactInput(path, amountIn);
return { amountIn, amountOutMin, path };
});
const swaps = <SwapSummary[]>await Promise.all(swapPromises);

// Get the donations array
const donations: Donation[] = cart.value.map((item) => {
Expand All @@ -284,6 +263,30 @@ export default function useCartStore() {
const now = new Date().getTime();
const nowPlus20Minutes = new Date(now + 20 * 60 * 1000).getTime();
return { swaps, donations: fixDonationRoundingErrors(donations), deadline: Math.floor(nowPlus20Minutes / 1000) };
}

// --- Getters ---
/**
* @notice Returns true if the provided grantId is in the cart, false otherwise
* @param grantId Grant ID to check
*/
function isInCart(grantId: BigNumberish): boolean {
const grantIds = lsCart.value.map((item) => item.grantId);
return grantIds.includes(toString(grantId));
}

/**
* @notice Convert a cart into an array of objects summarizing the cart info, with human-readable values
* @returns Object where keys are token addresses, values are total amount of that token in cart
*/
const cartSummary = computed((): Record<keyof typeof SUPPORTED_TOKENS_MAPPING, number> => {
const output: Record<keyof typeof SUPPORTED_TOKENS_MAPPING, number> = {};
for (const item of cart.value) {
const tokenAddress = item.contributionToken.address;
if (tokenAddress in output) output[tokenAddress] += item.contributionAmount;
else output[tokenAddress] = item.contributionAmount;
}
return output;
});

/**
Expand All @@ -297,14 +300,13 @@ export default function useCartStore() {
return summary.slice(0, -3); // trim the trailing ` + ` from the string
});

// Only export additional items as they are needed outside the store
return {
// Store
// WARNING: Be careful -- the `cart` ref is directly exposed so it can be edited by v-model, so just make
// sure to call `updateCart()` with the appropriate inputs whenever the `cart` ref is modified
cart,
// Getters
cartDonationInputs,
cartSummary,
cartSummaryString,
// Mutations
addToCart,
Expand All @@ -317,6 +319,8 @@ export default function useCartStore() {
};
}

// --- Pure functions (not reliant on state) ---

/**
* @notice Takes an array of donation data, and adjusts the ratios so they sum to 1e18 for each set of tokens
* @dev For each token, we adjust the first item in the cart to force the sum to be 100%. The adjustments will only
Expand All @@ -343,3 +347,16 @@ function fixDonationRoundingErrors(donations: Donation[]) {

return donations;
}

/**
* @notice Returns the amountOutMin expected for a given trade assuming 0.5% slippage
* @param path Swap path
* @param amountIn Swap input amount, as a full integer
*/
async function quoteExactInput(path: BytesLike, amountIn: BigNumberish): Promise<BigNumber> {
const { provider } = useWalletStore();
const abi = ['function quoteExactInput(bytes memory path, uint256 amountIn) external view returns (uint256 amountOut)']; // prettier-ignore
const quoter = new Contract('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', abi, provider.value);
const amountOut = await quoter.quoteExactInput(path, amountIn);
return amountOut.mul(995).div(1000); // multiplying by 995/1000 is equivalent to 0.5% slippage
}
10 changes: 7 additions & 3 deletions app/src/utils/ethers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
* Read more at https://github.com/vitejs/vite/issues/731
*/

export type { BigNumberish } from 'ethers';
// --- Types ---
export type { BigNumberish } from '@ethersproject/bignumber';
export type { ContractTransaction } from '@ethersproject/contracts';
export type { BytesLike } from '@ethersproject/bytes';
export type { Network } from '@ethersproject/networks';

// --- Methods and classes ---
export { getAddress, isAddress } from '@ethersproject/address';
export { BigNumber } from '@ethersproject/bignumber';
export { hexDataSlice } from '@ethersproject/bytes';
export { Contract } from '@ethersproject/contracts';
export type { ContractTransaction } from '@ethersproject/contracts';
export type { Network } from '@ethersproject/networks';
export { JsonRpcProvider, JsonRpcSigner, Web3Provider } from '@ethersproject/providers';
export { commify, formatUnits, parseUnits } from '@ethersproject/units';
export { MaxUint256 } from '@ethersproject/constants';

0 comments on commit eec37d3

Please sign in to comment.