-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
6c8637f
lib: partial add swap and split
PedroAraoz 5c9732d
lib: use public smart account
lgalende 0084244
lib: use balanceOf instead of pctRemainder
lgalende 4594e71
chore: add inline docs and extract constants
lgalende 2bda7c8
chore: add changeset
lgalende 6aabb4a
lib: use bps for allocation percentage
lgalende 1d37232
lib: add native token check
lgalende 107f977
chore: test swapAndSplit
lgalende 5927ff5
chore: improve error message and comments
lgalende 3705d71
chore: rename dynamic call builders
lgalende 219aca4
chore: improve dynamic args readability
lgalende 4df8cfd
lib: avoid hardcoding default user
lgalende 99b52ab
chore: move MAX_PCT_BPS to constants and rename
lgalende 1c8170e
lib: add smart account optional param
lgalende 4e70596
lib: update addresses
lgalende File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| 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, ONE_HUNDRED_PCT_BPS } 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 MIN_ALLOCATIONS: i32 = 2 | ||
|
|
||
| const SWAP_OP_INDEX: u32 = 0 | ||
| const SWAP_OP_SUB_INDEX: u32 = 0 | ||
| const PCT_OP_INDEX: u32 = 1 | ||
| const BALANCE_OF_OP_INDEX: u32 = 3 | ||
| const BALANCE_OF_OP_SUB_INDEX: u32 = 0 | ||
|
|
||
| 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. | ||
| * Each recipient receives the percentage of the output token specified in the allocations array. | ||
| * The last recipient receives its specified percentage plus any remaining balance caused by rounding. | ||
| * @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. | ||
| * @param smartAccount The smart account that receives the swap output and executes the transfers (optional). | ||
| * If not provided, the Mimic public smart account 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, | ||
| smartAccount: Address = MIMIC_PUBLIC_SMART_ACCOUNT | ||
| ): IntentBuilder { | ||
| if (allocations.length < MIN_ALLOCATIONS) throw new Error(`At least ${MIN_ALLOCATIONS} allocations are required`) | ||
|
|
||
| let totalPctBps: u32 = 0 | ||
| for (let i = 0; i < allocations.length; i++) totalPctBps += allocations[i].pctBps | ||
| if (totalPctBps !== ONE_HUNDRED_PCT_BPS) { | ||
| throw new Error(`Total allocation percentage must add up to ${ONE_HUNDRED_PCT_BPS} 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) | ||
| .addTokenInFromTokenAmount(amountIn) | ||
| .addTokenOutFromTokenAmount(minAmountOut, smartAccount) | ||
|
|
||
| if (user) swap.addUser(user) | ||
|
|
||
| builder.addOperationBuilder(swap) | ||
|
|
||
| // Calculate the corresponding amount for each allocation, except the last one, which will receive the remaining balance | ||
| const pctDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) | ||
| const swapOutput = EvmDynamicArg.variable(SWAP_OP_INDEX, SWAP_OP_SUB_INDEX, false) | ||
|
|
||
| for (let i = 0; i < allocations.length - 1; i++) { | ||
| const pctBps = allocations[i].pctBps | ||
| pctDynamicCall.addCall(MIMIC_HELPER, PCT_SELECTOR, [ | ||
| swapOutput, // amount | ||
| EvmDynamicArg.literal([new EvmEncodeParam('uint16', pctBps.toString())], false), // percent bps | ||
| ]) | ||
| } | ||
|
|
||
| builder.addOperationBuilder(pctDynamicCall) | ||
|
|
||
| // Transfer the corresponding amounts to each recipient, except the last one, which will receive the remaining balance | ||
| const transferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) | ||
|
|
||
| for (let i = 0; i < allocations.length - 1; i++) { | ||
| const recipient = allocations[i].recipient | ||
| const pctOutput = EvmDynamicArg.variable(PCT_OP_INDEX, i, false) | ||
| transferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', recipient.toString())], false), // to | ||
| pctOutput, // value | ||
| ]) | ||
| } | ||
|
|
||
| builder.addOperationBuilder(transferDynamicCall) | ||
|
|
||
| // Get the remaining balance | ||
| const balanceOfDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) | ||
| balanceOfDynamicCall.addCall(tokenOut, BALANCE_OF_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', smartAccount.toString())], false), // account | ||
| ]) | ||
|
|
||
| builder.addOperationBuilder(balanceOfDynamicCall) | ||
|
|
||
| // Transfer the remaining balance to the last recipient | ||
| const lastTransferDynamicCall = EvmDynamicCallBuilder.forChain(chainId).addUser(smartAccount) | ||
| const lastRecipient = allocations[allocations.length - 1].recipient | ||
| const balanceOfOutput = EvmDynamicArg.variable(BALANCE_OF_OP_INDEX, BALANCE_OF_OP_SUB_INDEX, false) | ||
|
|
||
| lastTransferDynamicCall.addCall(tokenOut, TRANSFER_SELECTOR, [ | ||
| EvmDynamicArg.literal([new EvmEncodeParam('address', lastRecipient.toString())], false), // to | ||
| balanceOfOutput, // value | ||
| ]) | ||
|
|
||
| builder.addOperationBuilder(lastTransferDynamicCall) | ||
|
|
||
| return builder | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,170 @@ | ||
| 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 anotherUser = Address.fromString('0x0000000000000000000000000000000000000005') | ||
| const anotherSmartAccount = Address.fromString('0x0000000000000000000000000000000000000006') | ||
| 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') | ||
| setEvmEncode('address', anotherSmartAccount.toString(), '0x2006') | ||
| }) | ||
|
|
||
| it('creates the intent properly', () => { | ||
|
alavarello marked this conversation as resolved.
|
||
| 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.user).toBe(user.toString()) | ||
| 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.tokensOut[0].recipient).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS.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('uses the given user when defined', () => { | ||
| const allocations = [new Allocation(recipient1, 9050), new Allocation(recipient2, 950)] | ||
| const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations, anotherUser).build() | ||
|
|
||
| const swap = changetype<Swap>(intent.operations[0]) | ||
| expect(swap.user).toBe(anotherUser.toString()) | ||
| expect(swap.tokensOut[0].recipient).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) | ||
|
|
||
| for (let i = 1; i < intent.operations.length; i++) { | ||
| expect(intent.operations[i].user).toBe(MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS) | ||
| } | ||
| }) | ||
|
|
||
| it('uses the given smart account when defined', () => { | ||
| const allocations = [new Allocation(recipient1, 9050), new Allocation(recipient2, 950)] | ||
| const intent = buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations, null, anotherSmartAccount).build() | ||
|
|
||
| const swap = changetype<Swap>(intent.operations[0]) | ||
| expect(swap.tokensOut[0].recipient).toBe(anotherSmartAccount.toString()) | ||
| expect(swap.user).toBe(user.toString()) | ||
|
|
||
| for (let i = 1; i < intent.operations.length; i++) { | ||
| expect(intent.operations[i].user).toBe(anotherSmartAccount.toString()) | ||
| } | ||
|
|
||
| const balanceOfCall = changetype<EvmDynamicCall>(intent.operations[3]) | ||
| expect(balanceOfCall.calls[0].arguments[0].data).toBe('0x2006') | ||
| }) | ||
|
|
||
| it('throws when there is less than two allocations', () => { | ||
| expect(() => { | ||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, []) | ||
| }).toThrow() | ||
|
|
||
| expect(() => { | ||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, [new Allocation(recipient1, 10_000)]) | ||
| }).toThrow() | ||
| }) | ||
|
|
||
| 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() | ||
|
|
||
| expect(() => { | ||
| const allocations = [new Allocation(recipient1, 9999), new Allocation(recipient2, 2)] | ||
|
|
||
| buildSwapAndSplit(chainId, amountIn, minAmountOut, allocations) | ||
| }).toThrow() | ||
| }) | ||
|
|
||
| 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() | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.