Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fiery-frogs-exist.md
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
5 changes: 4 additions & 1 deletion packages/lib-ts/src/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ export const MAX_UINT256_HEX = '0xffffffffffffffffffffffffffffffffffffffffffffff

export const STANDARD_DECIMALS: u8 = 18

export const ONE_HUNDRED_PCT_BPS: u16 = 10_000

export enum ListType {
AllowList = 0,
DenyList = 1,
}

export const MIMIC_HELPER_ADDRESS = '0x5cf82cbed1110fc2f75b3413d53abac492931804'
export const MIMIC_HELPER_ADDRESS = '0x2ef71e27560874b932ef1cf9e95d340595a92f44'
export const MIMIC_PUBLIC_SMART_ACCOUNT_ADDRESS = '0x2518928fdf52319cc5a9a797354cf159cd2221bb'
1 change: 1 addition & 0 deletions packages/lib-ts/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './BorshDeserializer'
export * from './constants'
export * from './serialize'
export * from './strings'
export * from './swapAndSplit'
export { Consensus, Math }
121 changes: 121 additions & 0 deletions packages/lib-ts/src/helpers/swapAndSplit.ts
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 {
Comment thread
alavarello marked this conversation as resolved.
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
}
3 changes: 2 additions & 1 deletion packages/lib-ts/src/tokens/TokenAmount.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { environment } from '../environment'
import { ONE_HUNDRED_PCT_BPS } from '../helpers/constants'
import { BigInt, JSON, Result } from '../types'

import { BlockchainToken } from './BlockchainToken'
import { SerializableToken, Token } from './Token'
import { USD } from './USD'

const BPS_SCALE = BigInt.fromI32(10_000)
const BPS_SCALE = BigInt.fromI32(ONE_HUNDRED_PCT_BPS)

/**
* Represents an amount of a specific token, combining the token metadata with a quantity.
Expand Down
170 changes: 170 additions & 0 deletions packages/lib-ts/tests/helpers/swapAndSplit.spec.ts
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', () => {
Comment thread
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()
})
})
Loading