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

Contracts package #178

Merged
merged 2 commits into from
Mar 29, 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: 37 additions & 0 deletions contracts/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package contracts

import (
"context"

"github.com/unpackdev/solgo/utils"
"go.uber.org/zap"
)

// Audit performs a security analysis of the contract using its associated detector,
// if available. It updates the contract descriptor with the audit results.
func (c *Contract) Audit(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
default:
if c.descriptor.HasDetector() && c.descriptor.HasContracts() {
detector := c.descriptor.Detector

semVer := utils.ParseSemanticVersion(c.descriptor.CompilerVersion)
detector.GetAuditor().GetConfig().SetCompilerVersion(semVer.String())

audit, err := c.descriptor.Detector.Analyze()
if err != nil {
zap.L().Debug(
"failed to analyze contract",
zap.Error(err),
zap.String("contract_address", c.descriptor.Address.Hex()),
)
return err
}
c.descriptor.Audit = audit
}

return nil
}
}
18 changes: 18 additions & 0 deletions contracts/bytecode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package contracts

import (
"fmt"
)

// DiscoverDeployedBytecode retrieves the deployed bytecode of the contract deployed at the specified address.
// It queries the blockchain using the provided client to fetch the bytecode associated with the contract address.
// The fetched bytecode is then stored in the contract descriptor for further processing.
func (c *Contract) DiscoverDeployedBytecode() error {
code, err := c.client.CodeAt(c.ctx, c.addr, nil)
if err != nil {
return fmt.Errorf("failed to get code at address %s: %s", c.addr.Hex(), err)
}
c.descriptor.DeployedBytecode = code

return nil
}
78 changes: 78 additions & 0 deletions contracts/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package contracts

import (
"context"
"fmt"
"github.com/unpackdev/solgo/bindings"

"github.com/ethereum/go-ethereum/common"
"github.com/unpackdev/solgo/utils"
)

// DiscoverChainInfo retrieves information about the contract's deployment chain, including transaction, receipt, and block details.
// If `otsLookup` is true, it queries the contract creator's information using the provided context. If `otsLookup` is false or
// if the creator's information is not available, it queries the contract creation transaction hash using etherscan.
// It then fetches the transaction, receipt, and block information associated with the contract deployment from the blockchain.
// This method populates the contract descriptor with the retrieved information.
func (c *Contract) DiscoverChainInfo(ctx context.Context, otsLookup bool) error {
var info *bindings.CreatorInformation

// What we are going to do, as erigon node is used in this particular case, is to query etherscan only if
// otterscan is not available.
if otsLookup {
var err error
info, err = c.bindings.GetContractCreator(ctx, c.network, c.addr)
if err != nil {
return fmt.Errorf("failed to get contract creator: %w", err)
}
}

var txHash common.Hash

if info == nil || info.CreationHash == utils.ZeroHash {
// Prior to continuing with the unpacking of the contract, we want to make sure that we can reach properly
// contract transaction and associated creation block. If we can't, we're not going to unpack it.
cInfo, err := c.etherscan.QueryContractCreationTx(ctx, c.addr)
if err != nil {
return fmt.Errorf("failed to query contract creation block and tx hash: %w", err)
}
txHash = cInfo.GetTransactionHash()
} else {
txHash = info.CreationHash
}

// Alright now lets extract block and transaction as well as receipt from the blockchain.
// We're going to use archive node for this, as we want to be sure that we can get all the data.

tx, _, err := c.client.TransactionByHash(ctx, txHash)
if err != nil {
return fmt.Errorf("failed to get transaction by hash: %s", err)
}
c.descriptor.Transaction = tx

receipt, err := c.client.TransactionReceipt(ctx, txHash)
if err != nil {
return fmt.Errorf("failed to get transaction receipt by hash: %s", err)
}
c.descriptor.Receipt = receipt

block, err := c.client.BlockByNumber(ctx, receipt.BlockNumber)
if err != nil {
return fmt.Errorf("failed to get block by number: %s", err)
}
c.descriptor.Block = block.Header()

if len(c.descriptor.ExecutionBytecode) < 1 {
c.descriptor.ExecutionBytecode = c.descriptor.Transaction.Data()
}

if len(c.descriptor.DeployedBytecode) < 1 {
code, err := c.client.CodeAt(ctx, receipt.ContractAddress, nil)
if err != nil {
return fmt.Errorf("failed to get contract code: %s", err)
}
c.descriptor.DeployedBytecode = code
}

return nil
}
69 changes: 69 additions & 0 deletions contracts/constructor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package contracts

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

"github.com/unpackdev/solgo/bytecode"
"github.com/unpackdev/solgo/utils"
"go.uber.org/zap"
)

// DiscoverConstructor discovers and decodes the constructor of the contract based on the provided context.
// It utilizes the contract's descriptor to gather information about the contract's bytecode, ABI, and transaction data.
// If a constructor is found in the bytecode, it decodes it using the provided ABI.
// The decoded constructor information is stored within the contract descriptor.
func (c *Contract) DiscoverConstructor(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
default:
if c.descriptor.Detector != nil && c.descriptor.Detector.GetIR() != nil && c.descriptor.Detector.GetIR().GetRoot() != nil {
detector := c.descriptor.Detector
irRoot := detector.GetIR().GetRoot()
abiRoot := detector.GetABI().GetRoot()

if irRoot.GetEntryContract() != nil && irRoot.GetEntryContract().GetConstructor() != nil &&
abiRoot != nil && abiRoot.GetEntryContract().GetMethodByType("constructor") != nil {
cAbi, _ := utils.ToJSON(abiRoot.GetEntryContract().GetMethodByType("constructor"))
constructorAbi := fmt.Sprintf("[%s]", string(cAbi))

tx := c.descriptor.Transaction
deployedBytecode := c.descriptor.DeployedBytecode

// Ensure that empty bytecode is not processed, otherwise:
// panic: runtime error: slice bounds out of range [:20] with capacity 0
if len(deployedBytecode) < 20 {
return nil
}

position := bytes.Index(tx.Data(), deployedBytecode[:20])
if position != -1 {
adjustedData := tx.Data()[position:]
constructorDataIndex := len(deployedBytecode)
if constructorDataIndex > len(adjustedData) {
return fmt.Errorf("constructor data index out of range")
}

constructor, err := bytecode.DecodeConstructorFromAbi(adjustedData[constructorDataIndex:], constructorAbi)
if err != nil {
if !strings.Contains(err.Error(), "would go over slice boundary") {
zap.L().Error(
"failed to decode constructor from bytecode",
zap.Error(err),
zap.Any("network", c.network),
zap.String("contract_address", c.addr.String()),
)
}
return fmt.Errorf("failed to decode constructor from bytecode: %s", err)
}
c.descriptor.Constructor = constructor
}
}
}

return nil
}
}
191 changes: 191 additions & 0 deletions contracts/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package contracts

import (
"context"
"fmt"

"github.com/0x19/solc-switch"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/unpackdev/solgo/bindings"
"github.com/unpackdev/solgo/clients"
"github.com/unpackdev/solgo/metadata"
"github.com/unpackdev/solgo/providers/bitquery"
"github.com/unpackdev/solgo/providers/etherscan"
"github.com/unpackdev/solgo/storage"
"github.com/unpackdev/solgo/tokens"
"github.com/unpackdev/solgo/utils"
)

// Metadata holds essential data related to an Ethereum smart contract.
// It includes information about the contract's bytecode, associated transactions, and blockchain context.
type Metadata struct {
RuntimeBytecode []byte
DeployedBytecode []byte
Block *types.Block
Transaction *types.Transaction
Receipt *types.Receipt
}

// Contract represents an Ethereum smart contract within the context of a specific network.
// It encapsulates the contract's address, network information, and associated metadata,
// and provides methods to interact with the contract on the blockchain.
type Contract struct {
ctx context.Context
clientPool *clients.ClientPool
client *clients.Client
addr common.Address
network utils.Network
descriptor *Descriptor
token *tokens.Token
bqp *bitquery.Provider
etherscan *etherscan.Provider
compiler *solc.Solc
bindings *bindings.Manager
tokenBind *bindings.Token
stor *storage.Storage
ipfsProvider metadata.Provider
}

// NewContract creates a new instance of Contract for a given Ethereum address and network.
// It initializes the contract's context, metadata, and associated blockchain clients.
// The function validates the contract's existence and its bytecode before creation.
func NewContract(ctx context.Context, network utils.Network, clientPool *clients.ClientPool, stor *storage.Storage, bqp *bitquery.Provider, etherscan *etherscan.Provider, compiler *solc.Solc, bindManager *bindings.Manager, ipfsProvider metadata.Provider, addr common.Address) (*Contract, error) {
if clientPool == nil {
return nil, fmt.Errorf("client pool is nil")
}

client := clientPool.GetClientByGroup(network.String())
if client == nil {
return nil, fmt.Errorf("client for network %s is nil", network.String())
}

if !common.IsHexAddress(addr.Hex()) {
return nil, fmt.Errorf("invalid address provided: %s", addr.Hex())
}

tokenBind, err := bindings.NewToken(ctx, network, bindManager, bindings.DefaultTokenBindOptions(addr))
if err != nil {
return nil, fmt.Errorf("failed to create new token %s bindings: %w", addr, err)
}

token, err := tokens.NewToken(
ctx,
network,
addr,
bindManager,
clientPool,
)
if err != nil {
return nil, fmt.Errorf("failed to create new token %s instance: %w", addr, err)
}

toReturn := &Contract{
ctx: ctx,
network: network,
clientPool: clientPool,
client: client,
addr: addr,
bqp: bqp,
etherscan: etherscan,
compiler: compiler,
descriptor: &Descriptor{
Network: network,
NetworkID: utils.GetNetworkID(network),
Address: addr,
Implementations: make([]common.Address, 0),
},
bindings: bindManager,
token: token,
tokenBind: tokenBind,
stor: stor,
ipfsProvider: ipfsProvider,
}

return toReturn, nil
}

// GetAddress returns the Ethereum address of the contract.
func (c *Contract) GetAddress() common.Address {
return c.addr
}

// GetNetwork returns the network (e.g., Mainnet, Ropsten) on which the contract is deployed.
func (c *Contract) GetNetwork() utils.Network {
return c.network
}

// GetDeployedBytecode returns the deployed bytecode of the contract.
// This bytecode is the compiled contract code that exists on the Ethereum blockchain.
func (c *Contract) GetDeployedBytecode() []byte {
return c.descriptor.DeployedBytecode
}

// GetExecutionBytecode returns the runtime bytecode of the contract.
// This bytecode is used during the execution of contract calls and transactions.
func (c *Contract) GetExecutionBytecode() []byte {
return c.descriptor.ExecutionBytecode
}

// GetBlock returns the blockchain block in which the contract was deployed or involved.
func (c *Contract) GetBlock() *types.Header {
return c.descriptor.Block
}

// SetBlock sets the blockchain block in which the contract was deployed or involved.
func (c *Contract) SetBlock(block *types.Header) {
c.descriptor.Block = block
}

// GetTransaction returns the Ethereum transaction associated with the contract's deployment or a specific operation.
func (c *Contract) GetTransaction() *types.Transaction {
return c.descriptor.Transaction
}

// SetTransaction sets the Ethereum transaction associated with the contract's deployment or a specific operation.
func (c *Contract) SetTransaction(tx *types.Transaction) {
c.descriptor.Transaction = tx
c.descriptor.ExecutionBytecode = tx.Data()
}

// GetReceipt returns the receipt of the transaction in which the contract was involved,
// providing details such as gas used and logs generated.
func (c *Contract) GetReceipt() *types.Receipt {
return c.descriptor.Receipt
}

// SetReceipt sets the receipt of the transaction in which the contract was involved,
// providing details such as gas used and logs generated.
func (c *Contract) SetReceipt(receipt *types.Receipt) {
c.descriptor.Receipt = receipt
}

// GetSender returns the Ethereum address of the sender of the contract's transaction.
// It extracts the sender's address using the transaction's signature.
func (c *Contract) GetSender() (common.Address, error) {
from, err := types.Sender(types.LatestSignerForChainID(c.descriptor.Transaction.ChainId()), c.descriptor.Transaction)
if err != nil {
return common.Address{}, fmt.Errorf("failed to get sender: %s", err)
}

return from, nil
}

// GetToken returns contract related discovered token (if found)
func (c *Contract) GetToken() *tokens.Token {
return c.token
}

// IsValid checks if the contract is valid by verifying its deployed bytecode.
// A contract is considered valid if it has non-empty deployed bytecode on the blockchain.
func (c *Contract) IsValid() (bool, error) {
if err := c.DiscoverDeployedBytecode(); err != nil {
return false, err
}
return len(c.descriptor.DeployedBytecode) > 2, nil
}

// GetDescriptor a public member to return back processed contract descriptor
func (c *Contract) GetDescriptor() *Descriptor {
return c.descriptor
}
Loading
Loading