diff --git a/.changeset/young-geese-joke.md b/.changeset/young-geese-joke.md new file mode 100644 index 00000000..fff20b6f --- /dev/null +++ b/.changeset/young-geese-joke.md @@ -0,0 +1,5 @@ +--- +'@protocolink/logics': patch +--- + +add Permit2 pull token logics diff --git a/package.json b/package.json index 8c0c7fc9..7a512085 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@paraswap/sdk": "^6.2.2", "@protocolink/common": "^0.3.4", - "@protocolink/core": "^0.4.3", + "@protocolink/core": "^0.4.4", "@types/lodash": "^4.14.195", "@uniswap/sdk-core": "^3.2.6", "@uniswap/token-lists": "^1.0.0-beta.31", diff --git a/src/logics/index.ts b/src/logics/index.ts index 13b3c3de..0b4b2bac 100644 --- a/src/logics/index.ts +++ b/src/logics/index.ts @@ -3,9 +3,10 @@ export * as aavev3 from './aave-v3'; export * as balancerv2 from './balancer-v2'; export * as compoundv2 from './compound-v2'; export * as compoundv3 from './compound-v3'; +export * as openoceanv2 from './openocean-v2'; export * as paraswapv5 from './paraswap-v5'; +export * as permit2 from './permit2'; +export * as radiantv2 from './radiant-v2'; export * as syncswap from './syncswap'; export * as uniswapv3 from './uniswap-v3'; export * as utility from './utility'; -export * as radiantv2 from './radiant-v2'; -export * as openoceanv2 from './openocean-v2'; diff --git a/src/logics/permit2/configs.ts b/src/logics/permit2/configs.ts new file mode 100644 index 00000000..e5fda47c --- /dev/null +++ b/src/logics/permit2/configs.ts @@ -0,0 +1,3 @@ +import * as core from '@protocolink/core'; + +export const supportedChainIds = Object.keys(core.contractAddressMap).map((chainId) => Number(chainId)); diff --git a/src/logics/permit2/index.ts b/src/logics/permit2/index.ts new file mode 100644 index 00000000..4cf9de04 --- /dev/null +++ b/src/logics/permit2/index.ts @@ -0,0 +1,2 @@ +export * from './configs'; +export * from './logic.pull-token'; diff --git a/src/logics/permit2/logic.pull-token.test.ts b/src/logics/permit2/logic.pull-token.test.ts new file mode 100644 index 00000000..7c8907b0 --- /dev/null +++ b/src/logics/permit2/logic.pull-token.test.ts @@ -0,0 +1,37 @@ +import { LogicTestCase } from 'test/types'; +import { PullTokenLogic, PullTokenLogicFields } from './logic.pull-token'; +import * as common from '@protocolink/common'; +import { constants, utils } from 'ethers'; +import * as core from '@protocolink/core'; +import { expect } from 'chai'; +import { mainnetTokens } from '@protocolink/test-helpers'; + +describe('Permit2 PullTokenLogic', function () { + const chainId = common.ChainId.mainnet; + const logic = new PullTokenLogic(chainId); + + context('Test build', function () { + const routerKit = new core.RouterKit(chainId); + const account = '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa'; + const iface = routerKit.permit2Iface; + + const testCases: LogicTestCase[] = [ + { fields: { input: new common.TokenAmount(mainnetTokens.WETH, '1') } }, + ]; + + testCases.forEach(({ fields }, i) => { + it(`case ${i + 1}`, async function () { + const routerLogic = await logic.build(fields, { account }); + const sig = routerLogic.data.substring(0, 10); + const permit2Address = await routerKit.getPermit2Address(); + + expect(routerLogic.to).to.eq(permit2Address); + expect(utils.isBytesLike(routerLogic.data)).to.be.true; + expect(sig).to.eq(iface.getSighash('transferFrom(address,address,uint160,address)')); + expect(routerLogic.inputs).to.deep.eq([]); + expect(routerLogic.approveTo).to.eq(constants.AddressZero); + expect(routerLogic.callback).to.eq(constants.AddressZero); + }); + }); + }); +}); diff --git a/src/logics/permit2/logic.pull-token.ts b/src/logics/permit2/logic.pull-token.ts new file mode 100644 index 00000000..353da600 --- /dev/null +++ b/src/logics/permit2/logic.pull-token.ts @@ -0,0 +1,26 @@ +import * as core from '@protocolink/core'; +import { supportedChainIds } from './configs'; + +export type PullTokenLogicFields = core.TokenInFields; + +export type PullTokenLogicOptions = Pick; + +@core.LogicDefinitionDecorator() +export class PullTokenLogic extends core.Logic implements core.LogicBuilderInterface { + static readonly supportedChainIds = supportedChainIds; + + async build(fields: PullTokenLogicFields, options: PullTokenLogicOptions) { + const { input } = fields; + const { account } = options; + const userAgent = await this.calcAgent(account); + const to = await this.getPermit2Address(); + const data = this.permit2Iface.encodeFunctionData('transferFrom(address,address,uint160,address)', [ + account, + userAgent, + input.amountWei, + input.token.address, + ]); + + return core.newLogic({ to, data }); + } +} diff --git a/test/logics/permit2/pull-token.test.ts b/test/logics/permit2/pull-token.test.ts new file mode 100644 index 00000000..fa1fffa2 --- /dev/null +++ b/test/logics/permit2/pull-token.test.ts @@ -0,0 +1,65 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { claimToken, getChainId, mainnetTokens, snapshotAndRevertEach } from '@protocolink/test-helpers'; +import * as common from '@protocolink/common'; +import * as core from '@protocolink/core'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import * as permit2 from 'src/logics/permit2'; +import * as utils from 'test/utils'; + +describe('mainnet: Test Permit2 PullToken Logic', function () { + let chainId: number; + let user: SignerWithAddress; + let routerKit: core.RouterKit; + let agent: string; + + before(async function () { + chainId = await getChainId(); + [, user] = await hre.ethers.getSigners(); + routerKit = new core.RouterKit(chainId); + agent = await routerKit.calcAgent(user.address); + + await claimToken(chainId, user.address, mainnetTokens.USDC, '100'); + await claimToken(chainId, user.address, mainnetTokens.WETH, '100'); + }); + + snapshotAndRevertEach(); + + const testCases = [ + { + fields: { input: new common.TokenAmount(mainnetTokens.USDC, '1') }, + }, + { + fields: { input: new common.TokenAmount(mainnetTokens.WETH, '1') }, + }, + ]; + + testCases.forEach(({ fields }, i) => { + it(`case ${i + 1}`, async function () { + // 1. build tokensReturn + const input = fields.input; + const permit2PullTokenLogic = new permit2.PullTokenLogic(chainId); + const tokensReturn: string[] = []; + const funds = new common.TokenAmounts(fields.input); + + // 2. build router logics + const routerLogics: core.DataType.LogicStruct[] = []; + routerLogics.push(await permit2PullTokenLogic.build(fields, { account: user.address })); + + // 3. get router permit2 datas + const permit2Datas = await utils.approvePermit2AndGetPermit2Datas(chainId, user, funds.erc20); + + // 4. send router tx + const transactionRequest = routerKit.buildExecuteTransactionRequest({ + permit2Datas, + routerLogics, + tokensReturn, + value: funds.native?.amountWei ?? 0, + }); + + await expect(user.sendTransaction(transactionRequest)).to.not.be.reverted; + await expect(user.address).to.changeBalance(input.token, -input.amount); + await expect(agent).to.changeBalance(input.token, input.amount); + }); + }); +}); diff --git a/test/utils/router.ts b/test/utils/router.ts index 456e0489..81dd6f29 100644 --- a/test/utils/router.ts +++ b/test/utils/router.ts @@ -17,6 +17,26 @@ export function calcRequiredAmountByBalanceBps(input: common.TokenAmount, balanc } export async function getRouterPermit2Datas(chainId: number, user: SignerWithAddress, inputs: common.TokenAmounts) { + const permit2Datas: string[] = []; + if (!inputs.isEmpty) { + // 1. approve permit2 and get permit2 permit call data + const permit2DatasWithoutTransferFrom = await approvePermit2AndGetPermit2Datas(chainId, user, inputs); + permit2Datas.push(...permit2DatasWithoutTransferFrom); + + // 2. get permit2 transferFrom data + const router = new core.RouterKit(chainId, hre.ethers.provider); + const transferFromCallData = await router.encodePermit2TransferFrom(user.address, inputs); + permit2Datas.push(transferFromCallData); + } + + return permit2Datas; +} + +export async function approvePermit2AndGetPermit2Datas( + chainId: number, + user: SignerWithAddress, + inputs: common.TokenAmounts +) { const permit2Datas: string[] = []; if (!inputs.isEmpty) { const router = new core.RouterKit(chainId, hre.ethers.provider); @@ -32,10 +52,6 @@ export async function getRouterPermit2Datas(chainId: number, user: SignerWithAdd const permitCallData = router.encodePermit2Permit(user.address, permitData.values, permitSig); permit2Datas.push(permitCallData); } - - // 3. get permit2 transferFrom data - const transferFromCallData = await router.encodePermit2TransferFrom(user.address, inputs); - permit2Datas.push(transferFromCallData); } return permit2Datas; diff --git a/yarn.lock b/yarn.lock index bf7182d8..224987b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1033,10 +1033,10 @@ type-fest "^3.12.0" zksync-web3 "^0.14.3" -"@protocolink/core@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@protocolink/core/-/core-0.4.3.tgz#4c2533acf62092d1e76b3e7ac7c2f9c69ae3e355" - integrity sha512-7AyR5Mu/aAfP9rDXkpc70gK29JtMDXDdzRfTXWKf7OisXXqPUuOuC6D0s2zbZr5DCd/IzMURZOSQ4VjfGsspfQ== +"@protocolink/core@^0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@protocolink/core/-/core-0.4.4.tgz#f47e4e1897f9cd5dc82276762b5ccbb70398a8f7" + integrity sha512-NiSe3YFqqKrtyInyRjE2e3VBqB75SreMbHzh2HA381hVESClpP8lH7O/B+6hNcDDn8zDujMb1hE2TDPzGymZ9w== dependencies: "@protocolink/common" "^0.3.4" "@uniswap/permit2-sdk" "^1.2.0"