Skip to content

Commit 620a97f

Browse files
feat: add support for creating allocations via EVM contract (#1970)
* Add support for creating allocations via Filplus EVM Client Contract * fixup! Add support for creating allocations via Filplus EVM Client Contract
1 parent eff5121 commit 620a97f

File tree

6 files changed

+497
-40
lines changed

6 files changed

+497
-40
lines changed

cmd/boost/direct_deal.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ var directDealAllocate = &cli.Command{
9393
Usage: "automatic yes to prompts; assume 'yes' as answer to all prompts and run non-interactively",
9494
Aliases: []string{"y", "yes"},
9595
},
96+
&cli.StringFlag{
97+
Name: "evm-client-contract",
98+
Usage: "f4 address of EVM contract to spend DataCap from",
99+
},
96100
},
97101
Before: before,
98102
Action: func(cctx *cli.Context) error {
@@ -101,6 +105,7 @@ var directDealAllocate = &cli.Command{
101105
pieceFile := cctx.String("piece-file")
102106
miners := cctx.StringSlice("miner")
103107
pinfos := cctx.StringSlice("piece-info")
108+
104109
if pieceFile == "" && len(pinfos) < 1 {
105110
return fmt.Errorf("must provide at least one --piece-info or use --piece-file")
106111
}
@@ -256,13 +261,31 @@ var directDealAllocate = &cli.Command{
256261

257262
log.Debugw("selected wallet", "wallet", walletAddr)
258263

259-
msgs, err := util.CreateAllocationMsg(ctx, gapi, pieceInfos, walletAddr, cctx.Int("batch-size"))
260-
261-
if err != nil {
262-
return err
264+
var msgs []*types.Message
265+
var allocationsAddr address.Address
266+
if cctx.IsSet("evm-client-contract") {
267+
evmContract := cctx.String("evm-client-contract")
268+
if evmContract == "" {
269+
return fmt.Errorf("evm-client-contract can't be empty")
270+
}
271+
evmContractAddr, err := address.NewFromString(evmContract)
272+
if err != nil {
273+
return err
274+
}
275+
allocationsAddr = evmContractAddr
276+
msgs, err = util.CreateAllocationViaEVMMsg(ctx, gapi, pieceInfos, walletAddr, evmContractAddr, cctx.Int("batch-size"))
277+
if err != nil {
278+
return err
279+
}
280+
} else {
281+
allocationsAddr = walletAddr
282+
msgs, err = util.CreateAllocationMsg(ctx, gapi, pieceInfos, walletAddr, cctx.Int("batch-size"))
283+
if err != nil {
284+
return err
285+
}
263286
}
264287

265-
oldallocations, err := gapi.StateGetAllocations(ctx, walletAddr, types.EmptyTSK)
288+
oldallocations, err := gapi.StateGetAllocations(ctx, allocationsAddr, types.EmptyTSK)
266289
if err != nil {
267290
return fmt.Errorf("failed to get allocations: %w", err)
268291
}
@@ -287,7 +310,7 @@ var directDealAllocate = &cli.Command{
287310
mcidStr = append(mcidStr, c.String())
288311
}
289312

290-
log.Infow("submitted data cap allocation message[s]", mcidStr)
313+
log.Infow("submitted data cap allocation message[s]", "CID", mcidStr)
291314
log.Info("waiting for message to be included in a block")
292315

293316
// wait for msgs to get mined into a block
@@ -318,7 +341,7 @@ var directDealAllocate = &cli.Command{
318341
return nil
319342
}
320343

321-
newallocations, err := gapi.StateGetAllocations(ctx, walletAddr, types.EmptyTSK)
344+
newallocations, err := gapi.StateGetAllocations(ctx, allocationsAddr, types.EmptyTSK)
322345
if err != nil {
323346
return fmt.Errorf("failed to get allocations: %w", err)
324347
}

cmd/boost/util/evm.go

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package util
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"strings"
8+
9+
mbig "math/big"
10+
11+
eabi "github.com/ethereum/go-ethereum/accounts/abi"
12+
"github.com/filecoin-project/go-address"
13+
"github.com/filecoin-project/go-state-types/abi"
14+
"github.com/filecoin-project/go-state-types/big"
15+
"github.com/filecoin-project/go-state-types/builtin"
16+
verifreg9 "github.com/filecoin-project/go-state-types/builtin/v9/verifreg"
17+
"github.com/filecoin-project/lotus/api"
18+
"github.com/filecoin-project/lotus/chain/actors"
19+
"github.com/filecoin-project/lotus/chain/types"
20+
"github.com/filecoin-project/lotus/chain/types/ethtypes"
21+
)
22+
23+
// https://github.com/fidlabs/contract-metaallocator/blob/main/src/Client.sol
24+
const contractABI = `[
25+
{
26+
"type": "function",
27+
"name": "allowances",
28+
"inputs": [
29+
{
30+
"name": "client",
31+
"type": "address",
32+
"internalType": "address"
33+
}
34+
],
35+
"outputs": [
36+
{
37+
"name": "allowance",
38+
"type": "uint256",
39+
"internalType": "uint256"
40+
}
41+
],
42+
"stateMutability": "view"
43+
},
44+
{
45+
"type": "function",
46+
"name": "transfer",
47+
"inputs": [
48+
{
49+
"name": "params",
50+
"type": "tuple",
51+
"internalType": "struct DataCapTypes.TransferParams",
52+
"components": [
53+
{
54+
"name": "to",
55+
"type": "tuple",
56+
"internalType": "struct CommonTypes.FilAddress",
57+
"components": [
58+
{
59+
"name": "data",
60+
"type": "bytes",
61+
"internalType": "bytes"
62+
}
63+
]
64+
},
65+
{
66+
"name": "amount",
67+
"type": "tuple",
68+
"internalType": "struct CommonTypes.BigInt",
69+
"components": [
70+
{
71+
"name": "val",
72+
"type": "bytes",
73+
"internalType": "bytes"
74+
},
75+
{
76+
"name": "neg",
77+
"type": "bool",
78+
"internalType": "bool"
79+
}
80+
]
81+
},
82+
{
83+
"name": "operator_data",
84+
"type": "bytes",
85+
"internalType": "bytes"
86+
}
87+
]
88+
}
89+
],
90+
"outputs": [],
91+
"stateMutability": "nonpayable"
92+
}
93+
]`
94+
95+
func getAddressAllowanceOnContract(ctx context.Context, api api.Gateway, wallet address.Address, contract address.Address) (*big.Int, error) {
96+
// Parse the contract ABI
97+
parsedABI, err := eabi.JSON(strings.NewReader(contractABI))
98+
if err != nil {
99+
return nil, fmt.Errorf("Failed to parse contract ABI: %w", err)
100+
}
101+
102+
// Convert from Filecoin to Eth Address
103+
walletId, err := api.StateLookupID(ctx, wallet, types.EmptyTSK)
104+
if err != nil {
105+
return nil, err
106+
}
107+
walletEvm, err := ethtypes.EthAddressFromFilecoinAddress(walletId)
108+
if err != nil {
109+
return nil, err
110+
}
111+
112+
// Prepare EVM calldata
113+
calldata, err := parsedABI.Pack("allowances", walletEvm)
114+
if err != nil {
115+
return nil, fmt.Errorf("failed to serialize parameters to check allowances: %w", err)
116+
}
117+
118+
// Encode EVM calldata as Message parameters
119+
allowanceParam := abi.CborBytes(calldata)
120+
allowanceParams, err := actors.SerializeParams(&allowanceParam)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed to serialize params: %w", err)
123+
}
124+
125+
// Read allowance from the contract
126+
allowanceMsg := &types.Message{
127+
To: contract,
128+
From: wallet,
129+
Method: builtin.MethodsEVM.InvokeContract,
130+
Params: allowanceParams,
131+
Value: big.Zero(),
132+
}
133+
result, err := api.StateCall(ctx, allowanceMsg, types.EmptyTSK)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
if result.MsgRct.ExitCode.IsError() {
139+
return nil, fmt.Errorf("Checking allowance failed with ExitCode %d", result.MsgRct.ExitCode)
140+
}
141+
142+
// Decode return value (cbor -> evm ABI -> math/big Int -> filecoin big Int)
143+
var decodedReturn abi.CborBytes
144+
err = decodedReturn.UnmarshalCBOR(bytes.NewReader(result.MsgRct.Return))
145+
if err != nil {
146+
return nil, err
147+
}
148+
retValue, err := parsedABI.Unpack("allowances", decodedReturn)
149+
if err != nil {
150+
return nil, err
151+
}
152+
allowance, err := big.FromString(retValue[0].(*mbig.Int).String())
153+
return &allowance, err
154+
}
155+
156+
func buildTransferViaEVMParams(amount *big.Int, receiverParams []byte) ([]byte, error) {
157+
// Parse the contract's ABI
158+
parsedABI, err := eabi.JSON(strings.NewReader(contractABI))
159+
if err != nil {
160+
return nil, fmt.Errorf("Failed to parse contract ABI: %w", err)
161+
}
162+
163+
// convert amount from Filecoin big.Int to math/big Int
164+
var amountMbig mbig.Int
165+
_, success := amountMbig.SetString(amount.String(), 10)
166+
if !success {
167+
return nil, fmt.Errorf("failed to serialize the amount")
168+
}
169+
170+
// build calldata
171+
calldata, err := parsedABI.Pack(
172+
"transfer",
173+
TransferParams{
174+
To: FilAddress{Data: builtin.VerifiedRegistryActorAddr.Bytes()},
175+
Amount: BigInt{
176+
Neg: amount.LessThan(big.Zero()),
177+
Val: amountMbig.Bytes(),
178+
},
179+
OperatorData: receiverParams,
180+
})
181+
if err != nil {
182+
return nil, fmt.Errorf("failed to serialize params: %w", err)
183+
}
184+
185+
transferParam := abi.CborBytes(calldata)
186+
transferParams, err := actors.SerializeParams(&transferParam)
187+
if err != nil {
188+
return nil, fmt.Errorf("failed to serialize params: %w", err)
189+
}
190+
return transferParams, nil
191+
}
192+
193+
func CreateAllocationViaEVMMsg(ctx context.Context, api api.Gateway, infos []PieceInfos, wallet address.Address, contract address.Address, batchSize int) ([]*types.Message, error) {
194+
// Create allocation requests
195+
rDataCap, allocationRequests, err := CreateAllocationRequests(ctx, api, infos)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
// Get datacap balance of the contract
201+
aDataCap, err := api.StateVerifiedClientStatus(ctx, contract, types.EmptyTSK)
202+
if err != nil {
203+
return nil, err
204+
}
205+
206+
if aDataCap == nil {
207+
return nil, fmt.Errorf("contract %s does not have any datacap", wallet)
208+
}
209+
210+
// Check that we have enough data cap to make the allocation
211+
if rDataCap.GreaterThan(big.NewInt(aDataCap.Int64())) {
212+
return nil, fmt.Errorf("requested datacap %s is greater then the available contract datacap %s", rDataCap, aDataCap)
213+
}
214+
215+
// Get datacap allowance of the wallet
216+
allowance, err := getAddressAllowanceOnContract(ctx, api, wallet, contract)
217+
if err != nil {
218+
return nil, err
219+
}
220+
221+
// Check that we have enough data cap to make the allocation
222+
if rDataCap.GreaterThan(*allowance) {
223+
return nil, fmt.Errorf("requested datacap %s is greater then the available datacap allowance %s", rDataCap, allowance)
224+
}
225+
226+
// Batch allocationRequests to create message
227+
var messages []*types.Message
228+
for i := 0; i < len(allocationRequests); i += batchSize {
229+
end := i + batchSize
230+
if end > len(allocationRequests) {
231+
end = len(allocationRequests)
232+
}
233+
batch := allocationRequests[i:end]
234+
arequest := &verifreg9.AllocationRequests{
235+
Allocations: batch,
236+
}
237+
bDataCap := big.NewInt(0)
238+
for _, bd := range batch {
239+
bDataCap.Add(big.NewInt(int64(bd.Size)).Int, bDataCap.Int)
240+
}
241+
242+
receiverParams, err := actors.SerializeParams(arequest)
243+
if err != nil {
244+
return nil, fmt.Errorf("failed to serialize the parameters: %w", err)
245+
}
246+
247+
amount := big.Mul(bDataCap, builtin.TokenPrecision)
248+
249+
transferParams, error := buildTransferViaEVMParams(&amount, receiverParams)
250+
if error != nil {
251+
return nil, error
252+
}
253+
254+
msg := &types.Message{
255+
To: contract,
256+
From: wallet,
257+
Method: builtin.MethodsEVM.InvokeContract,
258+
Params: transferParams,
259+
Value: big.Zero(),
260+
}
261+
messages = append(messages, msg)
262+
}
263+
return messages, nil
264+
}
265+
266+
// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/CommonTypes.sol#L86
267+
type FilAddress struct {
268+
Data []byte
269+
}
270+
271+
// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/CommonTypes.sol#L80
272+
type BigInt struct {
273+
Val []byte
274+
Neg bool
275+
}
276+
277+
// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/DataCapTypes.sol#L52
278+
type TransferParams struct {
279+
To FilAddress
280+
Amount BigInt
281+
OperatorData []byte
282+
}

0 commit comments

Comments
 (0)