Skip to content

Commit

Permalink
Contracts package (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
0x19 authored Mar 29, 2024
1 parent 35c99cf commit 4ffb355
Show file tree
Hide file tree
Showing 33 changed files with 1,438 additions and 253 deletions.
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

0 comments on commit 4ffb355

Please sign in to comment.