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

DPA-1413: fix(solana): IsOperation in timelock inspector #231

Merged
merged 1 commit into from
Jan 14, 2025
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
5 changes: 5 additions & 0 deletions .changeset/cool-queens-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

feat(solana): timelock inspection - operation statuses check
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52
github.com/smartcontractkit/chain-selectors v1.0.35
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1
github.com/spf13/cast v1.7.1
github.com/stretchr/testify v1.10.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ github.com/smartcontractkit/chain-selectors v1.0.35 h1:k7iJqChFbH10WFpahjDtDJoYy
github.com/smartcontractkit/chain-selectors v1.0.35/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b h1:UBXi9Yj8YSMHDDaxQLu273x1fWjyEL9xP58nuJsqZfg=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250103152858-8973fd0c912b/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d h1:kdn+WtHqE3j+KApNIgiRAxzWmdFRPZJ9CeRjP2QU0vk=
github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250110192108-679d6f555e8d/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60=
github.com/smartcontractkit/chainlink-common v0.4.0 h1:GZ9MhHt5QHXSaK/sAZvKDxkEqF4fPiFHWHEPqs/2C2o=
github.com/smartcontractkit/chainlink-common v0.4.0/go.mod h1:yti7e1+G9hhkYhj+L5sVUULn9Bn3bBL5/AxaNqdJ5YQ=
github.com/smartcontractkit/chainlink-testing-framework/framework v0.4.1 h1:573e5JlpGOjY/RDJziG62Cw5D66xghWHJcmfWvv6yc4=
Expand Down
12 changes: 6 additions & 6 deletions sdk/solana/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,20 @@ func FindSeenSignedHashesPDA(
}

func FindTimelockConfigPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_config"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_config"), timelockID[:]}
return findPDA(programID, seeds)
}

func FindTimelockOperationPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_operation"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed, opID [32]byte) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_operation"), timelockID[:], opID[:]}
return findPDA(programID, seeds)
}

func FindTimelockSignerPDA(
programID solana.PublicKey, pdaSeed PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_signer"), pdaSeed[:]}
programID solana.PublicKey, timelockID PDASeed) (solana.PublicKey, error) {
seeds := [][]byte{[]byte("timelock_signer"), timelockID[:]}
return findPDA(programID, seeds)
}

Expand Down
5 changes: 3 additions & 2 deletions sdk/solana/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
var (
testChainSelector = types.ChainSelector(cselectors.SOLANA_DEVNET.Selector)
testProgramID = solana.MustPublicKeyFromBase58("6UmMZr5MEqiKWD5jqTJd1WCR5kT8oZuFYBLJFi1o6GQX")
testOpID = [32]byte{1, 2, 3, 4}
testPDASeed = PDASeed{'t', 'e', 's', 't', '-', 'm', 'c', 'm'}
testRoot = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")
)
Expand Down Expand Up @@ -82,9 +83,9 @@ func Test_FindTimelockConfigPDA(t *testing.T) {

func Test_FindTimelockOperationPDA(t *testing.T) {
t.Parallel()
pda, err := FindTimelockOperationPDA(testProgramID, testPDASeed)
pda, err := FindTimelockOperationPDA(testProgramID, testPDASeed, testOpID)
require.NoError(t, err)
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("8TL4xwjpntLQXeFbADMPnooDofGUwocc4ikHAJb41Fcm")))
require.Empty(t, cmp.Diff(pda, solana.MustPublicKeyFromBase58("8iy9TQJB1jBBMBmwmiguZDRZpbDQNuvRCQu4cfdpgbKy")))
}

func Test_FindTimelockSignerPDA(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions sdk/solana/inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ func TestInspector_GetConfig(t *testing.T) {
name: "success",
setup: func(mockJSONRPCClient *mocks.JSONRPCClient) {
mcmConfig := &bindings.MultisigConfig{
ChainId: chainSelector,
MultisigName: testPDASeed,
Owner: solana.SystemProgramID,
ChainId: chainSelector,
MultisigId: testPDASeed,
Owner: solana.SystemProgramID,
Signers: []bindings.McmSigner{
{EvmAddress: common.HexToAddress("0xabcdefabcdefabcdefabcdefabcdefabcdef"), Index: 0, Group: 0},
{EvmAddress: common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678"), Index: 1, Group: 0},
Expand Down
131 changes: 131 additions & 0 deletions sdk/solana/timelock_inspector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package solana

import (
"context"
"errors"

"github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go/rpc"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/mcm"
"github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/timelock"
solanaCommon "github.com/smartcontractkit/chainlink-ccip/chains/solana/utils/common"

"github.com/smartcontractkit/mcms/sdk"
)

var _ sdk.TimelockInspector = (*TimelockInspector)(nil)

var TimelockOpDoneTimestamp = uint64(1)

// TimelockInspector is an Inspector implementation for Solana chains for accessing the RBACTimelock contract
type TimelockInspector struct {
client *rpc.Client
}

// NewTimelockInspector creates a new TimelockInspector
func NewTimelockInspector(client *rpc.Client) *TimelockInspector {
return &TimelockInspector{client: client}
}

func (t TimelockInspector) GetProposers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetExecutors(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetBypassers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) GetCancellers(ctx context.Context, address string) ([]common.Address, error) {
panic("implement me")
}

func (t TimelockInspector) IsOperation(ctx context.Context, address string, opID [32]byte) (bool, error) {
_, err := t.getOpData(ctx, address, opID)
if err != nil {
if errors.Is(err, rpc.ErrNotFound) {
return false, nil
}

return false, err
}

return true, nil
}

func (t TimelockInspector) IsOperationPending(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

return op.Timestamp > TimelockOpDoneTimestamp, nil
}

func (t TimelockInspector) IsOperationReady(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

blockTime, err := solanaCommon.GetBlockTime(ctx, t.client, rpc.CommitmentConfirmed)
if err != nil {
return false, err
}

ts, err := safeConvertUint64ToInt64(op.Timestamp)
Copy link
Collaborator

Choose a reason for hiding this comment

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

rather than converting, could you make TimelockOpDoneTimestamp into a uint64?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if you mean something like uint64(op.Timestamp) , then lint will complain

sdk/solana/timelock_inspector.go:79:12: G115: integer overflow conversion uint64 -> int64 (gosec)
        i := int64(op.Timestamp)

if err != nil {
return false, err
}

// a safety catch to prevent nil blocktime - ideally should not happen
if blockTime == nil {
return false, errors.New("failed to get block time: nil value")
}

return op.Timestamp > TimelockOpDoneTimestamp && ts <= int64(*blockTime), nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we have to perform these check manually because anchor-go does not generate bindings for these.

Reason

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we be 100% sure that blockTime != nil when err == nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i can add a guard to be safe.

}

func (t TimelockInspector) IsOperationDone(ctx context.Context, address string, opID [32]byte) (bool, error) {
op, err := t.getOpData(ctx, address, opID)
if err != nil {
return false, err
}

return op.Timestamp == TimelockOpDoneTimestamp, nil
}

func (t TimelockInspector) getOpData(ctx context.Context, address string, opID [32]byte) (timelock.Operation, error) {
programID, seed, err := ParseContractAddress(address)
if err != nil {
return timelock.Operation{}, err
}

mcm.SetProgramID(programID)

pda, err := FindTimelockOperationPDA(programID, seed, opID)
if err != nil {
return timelock.Operation{}, err
}

var opAccount timelock.Operation
err = solanaCommon.GetAccountDataBorshInto(ctx, t.client, pda, rpc.CommitmentConfirmed, &opAccount)
if err != nil {
return timelock.Operation{}, err
}

return opAccount, nil
}

func safeConvertUint64ToInt64(value uint64) (int64, error) {
const maxInt64 = int64(^uint64(0) >> 1)

if value > uint64(maxInt64) {
return 0, errors.New("cannot convert uint64 to int64: value out of int64 range")
}

return int64(value), nil
}
Loading
Loading