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

feat: add support for creating allocations via EVM contract #1970

Merged
merged 2 commits into from
Oct 3, 2024
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
37 changes: 30 additions & 7 deletions cmd/boost/direct_deal.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ var directDealAllocate = &cli.Command{
Usage: "automatic yes to prompts; assume 'yes' as answer to all prompts and run non-interactively",
Aliases: []string{"y", "yes"},
},
&cli.StringFlag{
Name: "evm-client-contract",
Usage: "f4 address of EVM contract to spend DataCap from",
},
},
Before: before,
Action: func(cctx *cli.Context) error {
Expand All @@ -101,6 +105,7 @@ var directDealAllocate = &cli.Command{
pieceFile := cctx.String("piece-file")
miners := cctx.StringSlice("miner")
pinfos := cctx.StringSlice("piece-info")

if pieceFile == "" && len(pinfos) < 1 {
return fmt.Errorf("must provide at least one --piece-info or use --piece-file")
}
Expand Down Expand Up @@ -256,13 +261,31 @@ var directDealAllocate = &cli.Command{

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

msgs, err := util.CreateAllocationMsg(ctx, gapi, pieceInfos, walletAddr, cctx.Int("batch-size"))

if err != nil {
return err
var msgs []*types.Message
var allocationsAddr address.Address
if cctx.IsSet("evm-client-contract") {
evmContract := cctx.String("evm-client-contract")
if evmContract == "" {
return fmt.Errorf("evm-client-contract can't be empty")
}
evmContractAddr, err := address.NewFromString(evmContract)
if err != nil {
return err
}
allocationsAddr = evmContractAddr
msgs, err = util.CreateAllocationViaEVMMsg(ctx, gapi, pieceInfos, walletAddr, evmContractAddr, cctx.Int("batch-size"))
if err != nil {
return err
}
} else {
allocationsAddr = walletAddr
msgs, err = util.CreateAllocationMsg(ctx, gapi, pieceInfos, walletAddr, cctx.Int("batch-size"))
if err != nil {
return err
}
}

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

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

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

newallocations, err := gapi.StateGetAllocations(ctx, walletAddr, types.EmptyTSK)
newallocations, err := gapi.StateGetAllocations(ctx, allocationsAddr, types.EmptyTSK)
if err != nil {
return fmt.Errorf("failed to get allocations: %w", err)
}
Expand Down
282 changes: 282 additions & 0 deletions cmd/boost/util/evm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package util

import (
"bytes"
"context"
"fmt"
"strings"

mbig "math/big"

eabi "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
"github.com/filecoin-project/go-state-types/big"
"github.com/filecoin-project/go-state-types/builtin"
verifreg9 "github.com/filecoin-project/go-state-types/builtin/v9/verifreg"
"github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/actors"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/lotus/chain/types/ethtypes"
)

// https://github.com/fidlabs/contract-metaallocator/blob/main/src/Client.sol
const contractABI = `[
{
"type": "function",
"name": "allowances",
"inputs": [
{
"name": "client",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "allowance",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transfer",
"inputs": [
{
"name": "params",
"type": "tuple",
"internalType": "struct DataCapTypes.TransferParams",
"components": [
{
"name": "to",
"type": "tuple",
"internalType": "struct CommonTypes.FilAddress",
"components": [
{
"name": "data",
"type": "bytes",
"internalType": "bytes"
}
]
},
{
"name": "amount",
"type": "tuple",
"internalType": "struct CommonTypes.BigInt",
"components": [
{
"name": "val",
"type": "bytes",
"internalType": "bytes"
},
{
"name": "neg",
"type": "bool",
"internalType": "bool"
}
]
},
{
"name": "operator_data",
"type": "bytes",
"internalType": "bytes"
}
]
}
],
"outputs": [],
"stateMutability": "nonpayable"
}
]`

func getAddressAllowanceOnContract(ctx context.Context, api api.Gateway, wallet address.Address, contract address.Address) (*big.Int, error) {
// Parse the contract ABI
parsedABI, err := eabi.JSON(strings.NewReader(contractABI))
if err != nil {
return nil, fmt.Errorf("Failed to parse contract ABI: %w", err)
}

// Convert from Filecoin to Eth Address
walletId, err := api.StateLookupID(ctx, wallet, types.EmptyTSK)
if err != nil {
return nil, err
}
walletEvm, err := ethtypes.EthAddressFromFilecoinAddress(walletId)
if err != nil {
return nil, err
}

// Prepare EVM calldata
calldata, err := parsedABI.Pack("allowances", walletEvm)
if err != nil {
return nil, fmt.Errorf("failed to serialize parameters to check allowances: %w", err)
}

// Encode EVM calldata as Message parameters
allowanceParam := abi.CborBytes(calldata)
allowanceParams, err := actors.SerializeParams(&allowanceParam)
if err != nil {
return nil, fmt.Errorf("failed to serialize params: %w", err)
}

// Read allowance from the contract
allowanceMsg := &types.Message{
To: contract,
From: wallet,
Method: builtin.MethodsEVM.InvokeContract,
Params: allowanceParams,
Value: big.Zero(),
}
result, err := api.StateCall(ctx, allowanceMsg, types.EmptyTSK)
if err != nil {
return nil, err
}

if result.MsgRct.ExitCode.IsError() {
return nil, fmt.Errorf("Checking allowance failed with ExitCode %d", result.MsgRct.ExitCode)
}

// Decode return value (cbor -> evm ABI -> math/big Int -> filecoin big Int)
var decodedReturn abi.CborBytes
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check exit code success here before unmarshaling the result

err = decodedReturn.UnmarshalCBOR(bytes.NewReader(result.MsgRct.Return))
if err != nil {
return nil, err
}
retValue, err := parsedABI.Unpack("allowances", decodedReturn)
if err != nil {
return nil, err
}
allowance, err := big.FromString(retValue[0].(*mbig.Int).String())
return &allowance, err
}

func buildTransferViaEVMParams(amount *big.Int, receiverParams []byte) ([]byte, error) {
// Parse the contract's ABI
parsedABI, err := eabi.JSON(strings.NewReader(contractABI))
if err != nil {
return nil, fmt.Errorf("Failed to parse contract ABI: %w", err)
}

// convert amount from Filecoin big.Int to math/big Int
var amountMbig mbig.Int
_, success := amountMbig.SetString(amount.String(), 10)
if !success {
return nil, fmt.Errorf("failed to serialize the amount")
}

// build calldata
calldata, err := parsedABI.Pack(
"transfer",
TransferParams{
To: FilAddress{Data: builtin.VerifiedRegistryActorAddr.Bytes()},
Amount: BigInt{
Neg: amount.LessThan(big.Zero()),
Val: amountMbig.Bytes(),
},
OperatorData: receiverParams,
})
if err != nil {
return nil, fmt.Errorf("failed to serialize params: %w", err)
}

transferParam := abi.CborBytes(calldata)
transferParams, err := actors.SerializeParams(&transferParam)
if err != nil {
return nil, fmt.Errorf("failed to serialize params: %w", err)
}
return transferParams, nil
}

func CreateAllocationViaEVMMsg(ctx context.Context, api api.Gateway, infos []PieceInfos, wallet address.Address, contract address.Address, batchSize int) ([]*types.Message, error) {
// Create allocation requests
rDataCap, allocationRequests, err := CreateAllocationRequests(ctx, api, infos)
if err != nil {
return nil, err
}

// Get datacap balance of the contract
aDataCap, err := api.StateVerifiedClientStatus(ctx, contract, types.EmptyTSK)
if err != nil {
return nil, err
}

if aDataCap == nil {
return nil, fmt.Errorf("contract %s does not have any datacap", wallet)
}

// Check that we have enough data cap to make the allocation
if rDataCap.GreaterThan(big.NewInt(aDataCap.Int64())) {
return nil, fmt.Errorf("requested datacap %s is greater then the available contract datacap %s", rDataCap, aDataCap)
}

// Get datacap allowance of the wallet
allowance, err := getAddressAllowanceOnContract(ctx, api, wallet, contract)
if err != nil {
return nil, err
}

// Check that we have enough data cap to make the allocation
if rDataCap.GreaterThan(*allowance) {
return nil, fmt.Errorf("requested datacap %s is greater then the available datacap allowance %s", rDataCap, allowance)
}

// Batch allocationRequests to create message
var messages []*types.Message
for i := 0; i < len(allocationRequests); i += batchSize {
end := i + batchSize
if end > len(allocationRequests) {
end = len(allocationRequests)
}
batch := allocationRequests[i:end]
arequest := &verifreg9.AllocationRequests{
Allocations: batch,
}
bDataCap := big.NewInt(0)
for _, bd := range batch {
bDataCap.Add(big.NewInt(int64(bd.Size)).Int, bDataCap.Int)
}

receiverParams, err := actors.SerializeParams(arequest)
if err != nil {
return nil, fmt.Errorf("failed to serialize the parameters: %w", err)
}

amount := big.Mul(bDataCap, builtin.TokenPrecision)

transferParams, error := buildTransferViaEVMParams(&amount, receiverParams)
if error != nil {
return nil, error
}

msg := &types.Message{
To: contract,
From: wallet,
Method: builtin.MethodsEVM.InvokeContract,
Params: transferParams,
Value: big.Zero(),
}
messages = append(messages, msg)
}
return messages, nil
}

// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/CommonTypes.sol#L86
type FilAddress struct {
Data []byte
}

// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/CommonTypes.sol#L80
type BigInt struct {
Val []byte
Neg bool
}

// https://github.com/filecoin-project/filecoin-solidity/blob/f655709ab02fcf4b7859f47149f1e0cbfa975041/contracts/v0.8/types/DataCapTypes.sol#L52
type TransferParams struct {
To FilAddress
Amount BigInt
OperatorData []byte
}
Loading