Skip to content
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

Implement IntentFactory (v1.0.0) #705

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0ffc2a0
implement intent factory
ezynda3 Jul 4, 2024
ff3000b
Deploy to staging on Arbitrum
ezynda3 Jul 4, 2024
27dd18a
Fix typo
ezynda3 Jul 8, 2024
a1023a1
Merge branch 'main' of github.com:lifinance/contracts into LF-6656
ezynda3 Jul 8, 2024
ac2cb2c
Redeploy to staging
ezynda3 Jul 8, 2024
fa392a0
Refactor
ezynda3 Jul 8, 2024
529e309
Get LIFI quote
ezynda3 Jul 8, 2024
ef20b75
Update deploy script and finish demo
ezynda3 Jul 9, 2024
85500f1
Update script amount
ezynda3 Jul 9, 2024
8d6ad62
refactor
ezynda3 Jul 9, 2024
424f0d6
add native test case
ezynda3 Jul 10, 2024
535838b
add native withdraw test
ezynda3 Jul 10, 2024
3677160
add docs
ezynda3 Jul 10, 2024
6de49de
remove dead link
ezynda3 Jul 10, 2024
eefbfe4
Merge branch 'main' of github.com:lifinance/contracts into LF-6656
ezynda3 Jul 10, 2024
2d90dbc
Add ownership
ezynda3 Jul 10, 2024
bf4cc5e
Merge branch 'main' of github.com:lifinance/contracts into LF-6656
ezynda3 Jul 17, 2024
eab925e
Fix typos
ezynda3 Jul 17, 2024
c1f2d9e
Fix typos
ezynda3 Jul 17, 2024
033559e
Update comment
ezynda3 Jul 17, 2024
dba945d
Make recommended changes
ezynda3 Jul 17, 2024
bdbc6e0
Fixes
ezynda3 Jul 17, 2024
383d86d
Handle native outputs
ezynda3 Jul 17, 2024
a35e87e
Use uint instead of bool
ezynda3 Jul 17, 2024
c8d1008
Merge branch 'main' of github.com:lifinance/contracts into LF-6656
ezynda3 Jul 18, 2024
08f1b08
emit events
ezynda3 Jul 18, 2024
13eafcb
Merge branch 'main' of github.com:lifinance/contracts into LF-6656
ezynda3 Jul 19, 2024
306be6b
test revert when less than minAmount
ezynda3 Jul 19, 2024
5134f82
catch events
ezynda3 Jul 19, 2024
ef606a8
test reverts when trying to reexecute intent
ezynda3 Jul 21, 2024
e508837
test can always withdraw
ezynda3 Jul 21, 2024
78c1611
Allow withdraw to any address
ezynda3 Jul 22, 2024
cf812ab
Test that we can withdraw even after intent is executed
ezynda3 Jul 22, 2024
81870fb
Update naming
ezynda3 Jul 22, 2024
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
1 change: 1 addition & 0 deletions config/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"withdrawWallet": "0x08647cc950813966142A416D40C382e2c5DB73bB",
"lifuelRebalanceWallet": "0xC71284231A726A18ac85c94D75f9fe17A185BeAF",
"deployerWallet": "0x11F1022cA6AdEF6400e5677528a80d49a069C00c",
"intentExecutorWallet": "0x11F1022cA6AdEF6400e5677528a80d49a069C00c",
"approvedSigsForRefundWallet": [
{
"sig": "0x0d19e519",
Expand Down
16 changes: 16 additions & 0 deletions deployments/_deployments_log_file.json
Original file line number Diff line number Diff line change
Expand Up @@ -20552,5 +20552,21 @@
]
}
}
},
"IntentFactory": {
"arbitrum": {
"staging": {
"1.0.0": [
{
"ADDRESS": "0x6e9B8579097F45E843584595541fF805101a18aA",
"OPTIMIZER_RUNS": "1000000",
"TIMESTAMP": "2024-07-09 16:26:46",
"CONSTRUCTOR_ARGS": "0x",
"SALT": "09072024",
"VERIFIED": "true"
}
]
}
}
}
}
4 changes: 3 additions & 1 deletion deployments/arbitrum.diamond.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@
"Receiver": "0x59B341fF54543D66C7393FfD2A050E256c97669E",
"RelayerCelerIM": "0x9d3573b1d85112446593f617f1f3eb5ec1778D27",
"ServiceFeeCollector": "0x9cc3164f01ED3796Fdf7Da538484D634608D2203",
"TokenWrapper": ""
"TokenWrapper": "",
"IntentFactory": "",
"ReceiverStargateV2": ""
}
}
}
3 changes: 2 additions & 1 deletion deployments/arbitrum.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@
"SymbiosisFacet": "0xb590b3B312f3C73621aa1E363841c8baecc2E712",
"DeBridgeDlnFacet": "0xE500dED7b9C9f1020870B7a6Db076Dbd892C0fea",
"MayanFacet": "0xd596C903d78870786c5DB0E448ce7F87A65A0daD",
"StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B"
"StandardizedCallFacet": "0x637Ac9AddC9C38b3F52878E11620a9060DC71d8B",
"IntentFactory": "0x6e9B8579097F45E843584595541fF805101a18aA"
}
85 changes: 85 additions & 0 deletions docs/IntentFactory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Intent Factory

## Description

The intent factory allows for triggering an action for a user
in a non-custodial way. The user/on-ramp/protocol/bridge simply sends funds to
an address we compute. It can then deploy a contract to that address and
0xDEnYO marked this conversation as resolved.
Show resolved Hide resolved
trigger the action the user intended. This is done by encoding the user's
intent into that address/contract. Fallback handling allows the user to
withdraw their funds at any time.

## How To Use

1. The first step to generating an intent is to calculate the intent contract's
deterministic address. You do this by providing the following:
- A random `bytes32` id
- The receiver address
- The address of the output token
- Minimum amount of token to receive

```solidity
// Compute the address of the intent
address intentClone = factory.getIntentAddress(
IIntent.InitData({
intentId: RANDOM_BYTES32_ID,
receiver: RECEIVER_ADDRESS,
tokenOut: TOKEN_OUT_ADDRESS,
amountOutMin: 100 * 10**TOKEN_OUT_DECIMALS
})
);
```

2. Next the tokens needed to fulfill the intent need to be sent to the
pre-calculated address. (NOTE: you can send multiple tokens if you wish).
A normal use-case would be bridging tokens from one chain to the pre-calculated
address on another chain and waiting for the bridge to complete to execute
the intent.

3. Execute the intent by passing an array of sequential calldata that will
yield the intended output amount for the receiver. For example, the first call
would approve the deposited token to an AMM. The next call would perform the
swap. Finally transfer any positive slippage or a pre-determined fee. As long
as the minimum output amount is left, the call will succeed and the remaining
output tokens will be transferred to the receiver.

```solidity
IIntent.Call[] memory calls = new IIntent.Call[](2);

// get approve calldata
bytes memory approveCalldata = abi.encodeWithSignature(
"approve(address,uint256)",
AMM_ADDRESS,
1000
);
calls[0] = IIntent.Call({
to: TOKEN_OUT_ADDRESS,
value: 0,
data: approveCalldata
});

// get swap calldata
bytes memory swapCalldata = abi.encodeWithSignature(
"swap(address,uint256,address,uint256)",
TOKEN_IN_ADDRESS,
1000 * 10**TOKEN_IN_DECIMALS,
TOKEN_OUT_ADDRESS,
100 * 10**TOKEN_OUT_DECIMALS
);
calls[1] = IIntent.Call({
to: AMM_ADDRESS,
value: 0,
data: swapCalldata
});

// execute the intent
factory.deployAndExecuteIntent(
IIntent.InitData({
intentId: intentId,
receiver: RECEIVER_ADDRESS,
tokenOut: TOKEN_OUT_ADDRESS,
amountOutMin: 100 * 10**TOKEN_OUT_DECIMALS
}),
calls
);
```
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
- [Optimism Bridge Facet](./OptimismBridgeFacet.md)
- [Periphery Registry Facet](./PeripheryRegistryFacet.md)
- [Polygon Bridge Facet](./PolygonBridgeFacet.md)
- [Ronin Bridge Facet](./RoninBridgeFacet.md)
- [Squid Facet](./SquidFacet.md)
- [Standardized Call Facet](./StandardizedCallFacet.md)
- [Stargate Facet](./StargateFacet.md)
Expand Down Expand Up @@ -56,3 +55,4 @@
- [FeeCollector](./FeeCollector.md)
- [Receiver](./Receiver.md)
- [RelayerCelerIM](./RelayerCelerIM.md)
- [IntentFactory](./IntentFactory.md)
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"@arbitrum/sdk": "^3.0.0",
"@hop-protocol/sdk": "0.0.1-beta.310",
"@layerzerolabs/lz-v2-utilities": "^2.3.21",
"@lifi/sdk": "^3.0.0",
"@safe-global/api-kit": "^2.3.1",
"@safe-global/protocol-kit": "^3.1.0",
"@safe-global/safe-apps-sdk": "^9.0.0",
Expand Down
135 changes: 135 additions & 0 deletions script/demoScripts/demoIntentFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { arbitrum } from 'viem/chains'
import { defineCommand, runMain } from 'citty'
import * as Deployments from '../../deployments/arbitrum.staging.json'
import * as IntentFactory from '../../out/IntentFactory.sol/IntentFactory.json'
import {
Address,
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
keccak256,
parseAbi,
toHex,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { ChainId, getQuote } from '@lifi/sdk'

const INTENT_FACTORY_ADDRESS = Deployments.IntentFactory as Address
const ABI = IntentFactory.abi
const ERC20_ABI = parseAbi([
'function transfer(address,uint256) external',
'function approve(address,uint256) external',
])
const DAI = '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'
const USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
const AMOUNT_TO_SWAP = '1000000000000000000'

const main = defineCommand({
meta: {
name: 'propose-to-safe',
description: 'Propose a transaction to a Gnosis Safe',
},
args: {
privateKey: {
type: 'string',
description: 'Private key of the signer',
required: true,
},
},
async run({ args }) {
const { privateKey } = args
const account = privateKeyToAccount(`0x${privateKey}`)
// Read client
const publicClient = createPublicClient({
chain: arbitrum,
transport: http(),
})

// Write client
const walletClient = createWalletClient({
account,
chain: arbitrum,
transport: http(),
})

// Setup the intentfactory ABI
const intentFactory = {
address: INTENT_FACTORY_ADDRESS,
abi: ABI,
}

// Get an initial quote from LIFI
let quote = await getQuote({
fromAddress: account.address,
toAddress: account.address,
fromChain: ChainId.ARB,
toChain: ChainId.ARB,
fromToken: DAI,
toToken: USDC,
fromAmount: AMOUNT_TO_SWAP,
})
console.log(quote)

// Calculate the intent address
const intentData = {
0xDEnYO marked this conversation as resolved.
Show resolved Hide resolved
intentId: keccak256(toHex(parseInt(Math.random().toString()))),
receiver: account.address,
tokenOut: USDC,
amountOutMin: quote.estimate.toAmountMin,
}
const predictedIntentAddress: Address = (await publicClient.readContract({
...intentFactory,
functionName: 'getIntentAddress',
args: [intentData],
})) as Address
console.log(predictedIntentAddress)

// Send DAI to predictedIntentAddress
let tx = await walletClient.writeContract({
address: DAI,
abi: ERC20_ABI,
functionName: 'transfer',
args: [predictedIntentAddress, BigInt(AMOUNT_TO_SWAP)],
})
console.log(tx)

// Get updated quote and use intent address
quote = await getQuote({
fromAddress: predictedIntentAddress,
toAddress: predictedIntentAddress,
fromChain: ChainId.ARB,
toChain: ChainId.ARB,
fromToken: DAI,
toToken: USDC,
fromAmount: AMOUNT_TO_SWAP,
})

// Deploy intent and execute the swap
const calls = []
const approveCallData = encodeFunctionData({
abi: ERC20_ABI,
functionName: 'approve',
args: [quote.estimate.approvalAddress as Address, BigInt(AMOUNT_TO_SWAP)],
})
calls.push({
to: DAI,
data: approveCallData,
value: BigInt(0),
})
calls.push({
to: quote.transactionRequest?.to,
data: quote.transactionRequest?.data,
value: BigInt(0),
})
tx = await walletClient.writeContract({
address: intentFactory.address,
abi: ABI,
functionName: 'deployAndExecuteIntent',
args: [intentData, calls],
})
console.log(tx)
},
})

runMain(main)
38 changes: 38 additions & 0 deletions script/deploy/facets/DeployIntentFactory.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { DeployScriptBase } from "./utils/DeployScriptBase.sol";
import { IntentFactory } from "lifi/Periphery/IntentFactory.sol";
import { stdJson } from "forge-std/Script.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;

constructor() DeployScriptBase("IntentFactory") {}

function run()
public
returns (IntentFactory deployed, bytes memory constructorArgs)
{
constructorArgs = getConstructorArgs();
deployed = IntentFactory(deploy(type(IntentFactory).creationCode));
}

function getConstructorArgs() internal override returns (bytes memory) {
// get path of global config file
string memory globalConfigPath = string.concat(
root,
"/config/global.json"
);

// read file into json variable
string memory globalConfigJson = vm.readFile(globalConfigPath);

// extract intentExecutorWallet address
address intentExecutorWalletAddress = globalConfigJson.readAddress(
".intentExecutorWallet"
);

return abi.encode(intentExecutorWalletAddress);
}
}
Loading
Loading