-
Notifications
You must be signed in to change notification settings - Fork 1
Lib: Implement swapAndSplit function #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6c8637f
5c9732d
0084244
4594e71
2bda7c8
6aabb4a
1d37232
107f977
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@mimicprotocol/lib-ts": patch | ||
| "@mimicprotocol/cli": patch | ||
| "@mimicprotocol/test-ts": patch | ||
| --- | ||
|
|
||
| Add swapAndSplit function |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { environment } from '../environment' | ||
| import { EvmDynamicArg, EvmDynamicCallBuilder, IntentBuilder, SwapBuilder } from '../intents' | ||
| import { TokenAmount } from '../tokens' | ||
| import { Address, Bytes, ChainId, EvmEncodeParam } from '../types' | ||
|
|
||
| import { MIMIC_HELPER_ADDRESS, MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from './constants' | ||
|
|
||
| const MIMIC_HELPER = Address.fromHexString(MIMIC_HELPER_ADDRESS) | ||
| const MIMIC_PUBLIC_SMART_ACCOUNT = Address.fromHexString(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) | ||
|
|
||
| const PCT_SELECTOR = Bytes.fromHexString('0xe7032021') | ||
| const TRANSFER_SELECTOR = Bytes.fromHexString('0xa9059cbb') | ||
| const BALANCE_OF_SELECTOR = Bytes.fromHexString('0x70a08231') | ||
|
|
||
| const MAX_PCT_BPS: u16 = 10_000 | ||
|
|
||
| export class Allocation { | ||
| constructor( | ||
| public recipient: Address, | ||
| public pctBps: u16 | ||
| ) {} | ||
| } | ||
|
|
||
| /** | ||
| * @dev Creates an IntentBuilder containing operations to swap tokens and transfer the output to multiple recipients. | ||
| * The last recipient percentage is ignored, and will receive the remaining balance after the other allocations. | ||
| * @param chainId The chain ID of the swap and the transfers. | ||
| * @param amountIn The amount of tokens to swap. If the token is native, the `user` must be a smart account. | ||
| * @param minAmountOut The minimum amount of tokens to receive from the swap. ERC20 tokens only. | ||
| * @param allocations An array containing a recipient address and a percentage in basis points (e.g., 50 = 0.5%, 10_000 = 100%). | ||
| * It represents how the output of the swap will be split among the recipients. The total allocation must add up to 10_000. | ||
| * @param user The user address for the swap (optional). If not provided, the context user will be used. | ||
| * @returns An IntentBuilder object that can be used to build and send the intent. | ||
| */ | ||
| export function buildSwapAndSplit( | ||
| chainId: ChainId, | ||
| amountIn: TokenAmount, | ||
| minAmountOut: TokenAmount, | ||
| allocations: Allocation[], | ||
| user: Address | null = null | ||
| ): IntentBuilder { | ||
| if (allocations.length <= 1) throw new Error('More than 1 allocation is needed') | ||
|
|
||
| let totalPctBps: u32 = 0 | ||
| for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps | ||
| if (totalPctBps !== MAX_PCT_BPS) throw new Error('Total allocation percentage must be 10_000 bps') | ||
|
|
||
| const tokenOut = minAmountOut.token.address | ||
| if (tokenOut.isNative()) throw new Error('Output token cannot be native') | ||
|
|
||
| const builder = new IntentBuilder() | ||
|
|
||
| const swap = SwapBuilder.forChain(chainId) | ||
| .addUser(user || environment.getContext().user) | ||
| .addTokenInFromTokenAmount(amountIn) | ||
| .addTokenOutFromTokenAmount(minAmountOut, MIMIC_PUBLIC_SMART_ACCOUNT) | ||
|
|
||
| builder.addOperationBuilder(swap) | ||
|
|
||
| // Calculate the corresponding amount for each allocation, except the last one | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't fully get why "except the last one" |
||
| const dynamicCall1 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) | ||
|
|
||
| for (let i = 0; i < allocations.length - 1; i++) { | ||
| const pctBps = allocations[i].pctBps | ||
| dynamicCall1.addCall(MIMIC_HELPER, PCT_SELECTOR, [ | ||
| EvmDynamicArg.variable(0, 0, false), // amount (swap output) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems to be constant, maybe it can be abstracted into a variable to improve its readability |
||
| EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps | ||
| ]) | ||
| } | ||
|
|
||
| builder.addOperationBuilder(dynamicCall1) | ||
|
|
||
| // Transfer the corresponding amounts to each recipient, except the last one | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, why "except the last one" |
||
| const dynamicCall2 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) | ||
|
|
||
| for (let i = 0; i < allocations.length - 1; i++) { | ||
| const recipient = allocations[i].recipient | ||
| dynamicCall2.addCall(tokenOut, TRANSFER_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to | ||
| EvmDynamicArg.variable(1, i, false), // value (dynamicCall1 sub 'i' result) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, we can use a variable inside the loop to improve its readability, similar to |
||
| ]) | ||
| } | ||
|
|
||
| builder.addOperationBuilder(dynamicCall2) | ||
|
|
||
| // Get the remaining balance | ||
| const dynamicCall3 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) | ||
| dynamicCall3.addCall(tokenOut, BALANCE_OF_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', MIMIC_PUBLIC_SMART_ACCOUNT.toString())], false), // account | ||
| ]) | ||
|
|
||
| builder.addOperationBuilder(dynamicCall3) | ||
|
|
||
| // Transfer the remaining balance to the last recipient | ||
| const lastRecipient = allocations[allocations.length - 1].recipient | ||
| const dynamicCall4 = EvmDynamicCallBuilder.forChain(chainId).addUser(MIMIC_PUBLIC_SMART_ACCOUNT) | ||
| dynamicCall4.addCall(tokenOut, TRANSFER_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to | ||
| EvmDynamicArg.variable(3, 0, false), // value (dynamicCall3 result) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
| ]) | ||
|
|
||
| builder.addOperationBuilder(dynamicCall4) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would name dynamicCall1, 2, 3, and 4 properly |
||
|
|
||
| return builder | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import { buildSwapAndSplit } from '../../src/helpers' | ||
| import { MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS } from '../../src/helpers/constants' | ||
| import { Allocation } from '../../src/helpers/swapAndSplit' | ||
| import { EvmDynamicCall, OperationType, Swap } from '../../src/intents' | ||
| import { ERC20Token, TokenAmount } from '../../src/tokens' | ||
| import { Address } from '../../src/types' | ||
| import { randomSettler, setContext, setEvmEncode } from '../helpers' | ||
|
|
||
| const chainId = 1 | ||
| const user = Address.fromString('0x0000000000000000000000000000000000000001') | ||
| const recipient1 = Address.fromString('0x0000000000000000000000000000000000000002') | ||
| const recipient2 = Address.fromString('0x0000000000000000000000000000000000000003') | ||
| const recipient3 = Address.fromString('0x0000000000000000000000000000000000000004') | ||
| const tokenIn = ERC20Token.fromAddress( | ||
| Address.fromString('0x0000000000000000000000000000000000000010'), | ||
| chainId, | ||
| 6, | ||
| 'USDC' | ||
| ) | ||
| const tokenOut = ERC20Token.fromAddress( | ||
| Address.fromString('0x0000000000000000000000000000000000000020'), | ||
| chainId, | ||
| 18, | ||
| 'DAI' | ||
| ) | ||
| const amountIn = TokenAmount.fromStringDecimal(tokenIn, '10') | ||
| const minAmountOut = TokenAmount.fromStringDecimal(tokenOut, '100') | ||
|
|
||
| describe('buildSwapAndSplit', () => { | ||
| beforeEach(() => { | ||
| setContext(1, 1, user.toString(), [randomSettler(chainId)], 'trigger-123') | ||
|
|
||
| setEvmEncode('uint256', '0', '0x1000') // Dynamic variable 0 | ||
| setEvmEncode('uint256', '1', '0x1001') // Dynamic variable 1 | ||
| setEvmEncode('uint256', '3', '0x1003') // Dynamic variable 3 | ||
|
|
||
| setEvmEncode('uint16', '9050', '0x9050') | ||
| setEvmEncode('uint16', '125', '0x0125') | ||
|
|
||
| setEvmEncode('address', MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS, '0x2000') | ||
| setEvmEncode('address', recipient1.toString(), '0x2001') | ||
| setEvmEncode('address', recipient2.toString(), '0x2002') | ||
| setEvmEncode('address', recipient3.toString(), '0x2003') | ||
| }) | ||
|
|
||
| it('creates the intent properly', () => { | ||
| const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, [ | ||
| new Allocation(recipient1, 9050), | ||
| new Allocation(recipient2, 125), | ||
| new Allocation(recipient3, 825), | ||
| ]).build() | ||
|
|
||
| expect(intent.operations.length).toBe(5) | ||
| expect(intent.operations[0].opType).toBe(OperationType.Swap) | ||
| expect(intent.operations[1].opType).toBe(OperationType.EvmDynamicCall) | ||
| expect(intent.operations[2].opType).toBe(OperationType.EvmDynamicCall) | ||
| expect(intent.operations[3].opType).toBe(OperationType.EvmDynamicCall) | ||
| expect(intent.operations[4].opType).toBe(OperationType.EvmDynamicCall) | ||
|
|
||
| const swap = changetype<Swap>(intent.operations[0]) | ||
| expect(swap.sourceChain).toBe(chainId) | ||
| expect(swap.destinationChain).toBe(chainId) | ||
| expect(swap.tokensIn[0].token).toBe(tokenIn.address.toString()) | ||
| expect(swap.tokensIn[0].amount).toBe(amountIn.amount.toString()) | ||
| expect(swap.tokensOut[0].token).toBe(tokenOut.address.toString()) | ||
| expect(swap.tokensOut[0].minAmount).toBe(minAmountOut.amount.toString()) | ||
| expect(swap.user).toBe(user.toString()) | ||
|
|
||
| const pctCall = changetype<EvmDynamicCall>(intent.operations[1]) | ||
| expect(pctCall.calls.length).toBe(2) | ||
|
|
||
| expect(pctCall.calls[0].selector).toBe('0xe7032021') | ||
| expect(pctCall.calls[0].arguments[0].data).toBe('0x1000') | ||
| expect(pctCall.calls[0].arguments[1].data).toBe('0x9050') | ||
|
|
||
| expect(pctCall.calls[1].selector).toBe('0xe7032021') | ||
| expect(pctCall.calls[1].arguments[0].data).toBe('0x1000') | ||
| expect(pctCall.calls[1].arguments[1].data).toBe('0x0125') | ||
|
|
||
| const transferCall = changetype<EvmDynamicCall>(intent.operations[2]) | ||
| expect(transferCall.calls.length).toBe(2) | ||
|
|
||
| expect(transferCall.calls[0].selector).toBe('0xa9059cbb') | ||
| expect(transferCall.calls[0].arguments[0].data).toBe('0x2001') | ||
| expect(transferCall.calls[0].arguments[1].data).toBe('0x1001') | ||
|
|
||
| expect(transferCall.calls[1].selector).toBe('0xa9059cbb') | ||
| expect(transferCall.calls[1].arguments[0].data).toBe('0x2002') | ||
| expect(transferCall.calls[1].arguments[1].data).toBe('0x1001') | ||
|
|
||
| const balanceOfCall = changetype<EvmDynamicCall>(intent.operations[3]) | ||
| expect(balanceOfCall.calls.length).toBe(1) | ||
|
|
||
| expect(balanceOfCall.calls[0].selector).toBe('0x70a08231') | ||
| expect(balanceOfCall.calls[0].arguments[0].data).toBe('0x2000') | ||
|
|
||
| const finalTransferCall = changetype<EvmDynamicCall>(intent.operations[4]) | ||
| expect(finalTransferCall.calls.length).toBe(1) | ||
|
|
||
| expect(finalTransferCall.calls[0].selector).toBe('0xa9059cbb') | ||
| expect(finalTransferCall.calls[0].arguments[0].data).toBe('0x2003') | ||
| expect(finalTransferCall.calls[0].arguments[1].data).toBe('0x1003') | ||
| }) | ||
|
|
||
| it('throws when there is less than two allocations', () => { | ||
| expect(() => { | ||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, []) | ||
| }).toThrow('More than 1 allocation is needed') | ||
|
|
||
| expect(() => { | ||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, [new Allocation(recipient1, 10_000)]) | ||
| }).toThrow('More than 1 allocation is needed') | ||
| }) | ||
|
|
||
| it('throws when allocations do not add up to 100%', () => { | ||
| expect(() => { | ||
| const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 0)] | ||
|
|
||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) | ||
| }).toThrow('Total allocation percentage must be 10_000 bps') | ||
|
|
||
| expect(() => { | ||
| const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 2)] | ||
|
|
||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) | ||
| }).toThrow('Total allocation percentage must be 10_000 bps') | ||
| }) | ||
|
|
||
| it('throws when output token is native', () => { | ||
| expect(() => { | ||
| const nativeAmountOut = TokenAmount.fromStringDecimal(ERC20Token.native(chainId), '100') | ||
| const allocations = [new Allocation(recipient1, 50), new Allocation(recipient2, 9950)] | ||
|
|
||
| buildSwapAndSplit(chainId, amountIn, nativeAmountOut, allocations) | ||
| }).toThrow('Output token cannot be native') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.