Skip to content
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/giant-plums-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smartcontractkit/mcms": minor
---

Adds support for getMinDelay to timelock inspectors on all supported chain families
70 changes: 45 additions & 25 deletions e2e/tests/aptos/timelock_proposal.go
Copy link
Contributor

Choose a reason for hiding this comment

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

what's wrong with using := for the variable initializations? Is the linter complaining?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah the linter was complaining about it, not super fan of the rule though

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"slices"
"time"

"github.com/aptos-labs/aptos-go-sdk/api"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/smartcontractkit/chainlink-aptos/bindings/bind"
Expand Down Expand Up @@ -67,7 +68,11 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
a.Require().NoError(err)
a.Require().True(data.Success, data.VmStatus)
}

// Get Min Delay
timelockInspector := aptossdk.NewTimelockInspector(a.AptosRPCClient)
delay, err := timelockInspector.GetMinDelay(a.T().Context(), mcmsAddress.StringLong())
a.Require().NoError(err)
a.Require().Equal(uint64(0), delay)
// Configure Proposers
proposers := [3]common.Address{}
proposerKeys := [3]*ecdsa.PrivateKey{}
Expand All @@ -84,18 +89,22 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
Signers: proposers[:],
}
proposeConfigurer := aptossdk.NewConfigurer(a.AptosRPCClient, a.deployerAccount, aptossdk.TimelockRoleProposer)
result, err := proposeConfigurer.SetConfig(a.T().Context(), mcmsAddress.StringLong(), proposerConfig, false)
var result types.TransactionResult
result, err = proposeConfigurer.SetConfig(a.T().Context(), mcmsAddress.StringLong(), proposerConfig, false)
a.Require().NoError(err)
data, err := a.AptosRPCClient.WaitForTransaction(result.Hash)
var data *api.UserTransaction
data, err = a.AptosRPCClient.WaitForTransaction(result.Hash)
a.Require().NoError(err)
a.Require().True(data.Success, data.VmStatus)
}

// Initiate ownership transfer
{
tx, err := a.MCMSContract.MCMSAccount().TransferOwnershipToSelf(opts)
var tx *api.PendingTransaction
tx, err = a.MCMSContract.MCMSAccount().TransferOwnershipToSelf(opts)
a.Require().NoError(err)
data, err := a.AptosRPCClient.WaitForTransaction(tx.Hash)
var data *api.UserTransaction
data, err = a.AptosRPCClient.WaitForTransaction(tx.Hash)
a.Require().NoError(err)
a.Require().True(data.Success, data.VmStatus)
a.T().Logf("🚀 TransferOwnershipToSelf in tx: %s", tx.Hash)
Expand All @@ -120,9 +129,9 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
SetAction(types.TimelockActionSchedule).
SetDelay(types.NewDuration(time.Second * 2))

module, function, _, args, err := a.MCMSContract.MCMSAccount().Encoder().AcceptOwnership()
a.Require().NoError(err)
transaction, err := aptossdk.NewTransaction(
module, function, _, args, errAccept := a.MCMSContract.MCMSAccount().Encoder().AcceptOwnership()
a.Require().NoError(errAccept)
transaction, errTx := aptossdk.NewTransaction(
module.PackageName,
module.ModuleName,
function,
Expand All @@ -131,25 +140,28 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
"MCMS",
nil,
)
a.Require().NoError(err)
a.Require().NoError(errTx)
acceptOwnershipProposalBuilder.AddOperation(types.BatchOperation{
ChainSelector: a.ChainSelector,
Transactions: []types.Transaction{transaction},
})
acceptOwnershipTimelockProposal, err := acceptOwnershipProposalBuilder.Build()
var acceptOwnershipTimelockProposal *mcms.TimelockProposal
acceptOwnershipTimelockProposal, err = acceptOwnershipProposalBuilder.Build()
a.Require().NoError(err)

convertersMap := map[types.ChainSelector]sdk.TimelockConverter{
a.ChainSelector: aptossdk.NewTimelockConverter(),
}
acceptOwnershipProposal, _, err := acceptOwnershipTimelockProposal.Convert(a.T().Context(), convertersMap)
var acceptOwnershipProposal mcms.Proposal
acceptOwnershipProposal, _, err = acceptOwnershipTimelockProposal.Convert(a.T().Context(), convertersMap)
a.Require().NoError(err)

inspector := aptossdk.NewInspector(a.AptosRPCClient, aptossdk.TimelockRoleProposer)
inspectorsMap := map[types.ChainSelector]sdk.Inspector{
a.ChainSelector: inspector,
}
signable, err := mcms.NewSignable(&acceptOwnershipProposal, inspectorsMap)
var signable *mcms.Signable
signable, err = mcms.NewSignable(&acceptOwnershipProposal, inspectorsMap)
a.Require().NoError(err)

_, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(proposerKeys[0]))
Expand All @@ -159,32 +171,37 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
_, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(proposerKeys[2]))
a.Require().NoError(err)

quorumMet, err := signable.ValidateSignatures(a.T().Context())
var quorumMet bool
quorumMet, err = signable.ValidateSignatures(a.T().Context())
a.Require().NoError(err, "Error validating signatures")
a.Require().True(quorumMet, "Quorum not met")

// Set Root
encoders, err := acceptOwnershipProposal.GetEncoders()
var encoders map[types.ChainSelector]sdk.Encoder
encoders, err = acceptOwnershipProposal.GetEncoders()
a.Require().NoError(err)
aptosEncoder := encoders[a.ChainSelector].(*aptossdk.Encoder)
executors := map[types.ChainSelector]sdk.Executor{
a.ChainSelector: aptossdk.NewExecutor(a.AptosRPCClient, a.deployerAccount, aptosEncoder, aptossdk.TimelockRoleProposer),
}
executable, err := mcms.NewExecutable(&acceptOwnershipProposal, executors)
var executable *mcms.Executable
executable, err = mcms.NewExecutable(&acceptOwnershipProposal, executors)
a.Require().NoError(err, "Error creating executable")

result, err := executable.SetRoot(a.T().Context(), a.ChainSelector)
var result types.TransactionResult
result, err = executable.SetRoot(a.T().Context(), a.ChainSelector)
a.Require().NoError(err)

data, err := a.AptosRPCClient.WaitForTransaction(result.Hash)
var data *api.UserTransaction
data, err = a.AptosRPCClient.WaitForTransaction(result.Hash)
a.Require().NoError(err)
a.Require().True(data.Success, data.VmStatus)
a.T().Logf("✅ SetRoot in tx: %s", result.Hash)

// Assert
tree, _ := acceptOwnershipProposal.MerkleTree()
gotHash, gotValidUntil, err := inspector.GetRoot(a.T().Context(), mcmsAddress.StringLong())
a.Require().NoError(err)
gotHash, gotValidUntil, errSetRoot := inspector.GetRoot(a.T().Context(), mcmsAddress.StringLong())
a.Require().NoError(errSetRoot)
a.Require().Equal(validUntil, gotValidUntil)
a.Require().Equal(tree.Root, gotHash)

Expand Down Expand Up @@ -212,14 +229,17 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{
a.ChainSelector: timelockExecutor,
}
timelockExecutable, err := mcms.NewTimelockExecutable(a.T().Context(), acceptOwnershipTimelockProposal, timelockExecutors)
var timelockExecutable *mcms.TimelockExecutable
timelockExecutable, err = mcms.NewTimelockExecutable(a.T().Context(), acceptOwnershipTimelockProposal, timelockExecutors)
a.Require().NoError(err)

operationID, err := timelockExecutable.GetOpID(a.T().Context(), 0, acceptOwnershipTimelockProposal.Operations[0], a.ChainSelector)
var operationID common.Hash
operationID, err = timelockExecutable.GetOpID(a.T().Context(), 0, acceptOwnershipTimelockProposal.Operations[0], a.ChainSelector)
a.Require().NoError(err)
timelockInspector := aptossdk.NewTimelockInspector(a.AptosRPCClient)
ok, err := timelockInspector.IsOperation(a.T().Context(), mcmsAddress.StringLong(), operationID)
a.Require().NoError(err)

ok, errIsOp := timelockInspector.IsOperation(a.T().Context(), mcmsAddress.StringLong(), operationID)
a.Require().NoError(errIsOp)
a.Require().True(ok, "Operation not found in timelock")

a.Require().EventuallyWithT(
Expand All @@ -234,8 +254,8 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() {
a.T().Logf("🟢 Timelock operation ready, elapsed time: %v", elapsed.String())

for i := range acceptOwnershipTimelockProposal.Operations {
res, err := timelockExecutable.Execute(a.T().Context(), i)
a.Require().NoError(err)
res, errExec := timelockExecutable.Execute(a.T().Context(), i)
a.Require().NoError(errExec)
data, err = a.AptosRPCClient.WaitForTransaction(res.Hash)
a.Require().NoError(err)
a.Require().True(data.Success, data.VmStatus)
Expand Down
10 changes: 10 additions & 0 deletions e2e/tests/evm/timelock_inspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,13 @@ func (s *TimelockInspectionTestSuite) TestIsOperationDone() {
return isOpDone
}, 5*time.Second, 500*time.Millisecond, "Operation was not completed in time")
}

// TestGetMinDelay tests the GetMinDelay method
func (s *TimelockInspectionTestSuite) TestGetMinDelay() {
ctx := context.Background()
inspector := evm.NewTimelockInspector(s.ClientA)

delay, err := inspector.GetMinDelay(ctx, s.timelockContract.Address().Hex())
s.Require().NoError(err, "Failed to get min delay")
s.Require().EqualValues(0, delay)
}
13 changes: 13 additions & 0 deletions e2e/tests/solana/timelock_inspection.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var testPDASeedTimelockIsOperations = [32]byte{'t', 'e', 's', 't', '-', 't', 'i'
var testPDASeedTimelockIsOperationsPending = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'p', 'e', 'n', 'd'}
var testPDASeedTimelockIsOperationsReady = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'r', 'e', 'a', 'd', 'y'}
var testPDASeedTimelockIsOperationsDone = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'd', 'o', 'n', 'e'}
var testPDASeedTimelockMinDelay = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'm', 'i', 'n', 'd', 'e', 'l', 'a', 'y'}

func (s *SolanaTestSuite) TestGetProposers() {
s.SetupTimelock(testPDASeedTimelockGetProposers, 1*time.Second)
Expand Down Expand Up @@ -158,6 +159,18 @@ func (s *SolanaTestSuite) TestIsOperationDone() {
s.Require().True(operation, "Operation should be done")
}

func (s *SolanaTestSuite) TestGetMinDelay() {
ctx := context.Background()
minDelay := 1 * time.Second
s.SetupTimelock(testPDASeedTimelockMinDelay, minDelay)

inspector := solanasdk.NewTimelockInspector(s.SolanaClient)
delay, err := inspector.GetMinDelay(ctx, solanasdk.ContractAddress(s.TimelockProgramID, testPDASeedTimelockMinDelay))

s.Require().NoError(err, "Failed to query min delay")
s.Require().Equal(delay, uint64(minDelay.Seconds()), "Min delay should match the configured value")
}

func (s *SolanaTestSuite) createOperation(timelockID [32]byte) timelockutils.Operation {
salt, serr := timelockutils.SimpleSalt()
s.Require().NoError(serr)
Expand Down
10 changes: 10 additions & 0 deletions sdk/aptos/timelock_inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ type TimelockInspector struct {
bindingFn func(address aptos.AccountAddress, client aptos.AptosRpcClient) mcms.MCMS
}

func (tm TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) {
mcmsAddress, err := hexToAddress(address)
if err != nil {
return 0, fmt.Errorf("failed to parse MCMS address %q: %w", mcmsAddress, err)
}
mcmsBinding := tm.bindingFn(mcmsAddress, tm.client)

return mcmsBinding.MCMS().TimelockMinDelay(nil)
}

// NewTimelockInspector creates a new TimelockInspector
func NewTimelockInspector(client aptos.AptosRpcClient) *TimelockInspector {
return &TimelockInspector{
Expand Down
81 changes: 81 additions & 0 deletions sdk/aptos/timelock_inspector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,84 @@ func TestTimelockInspector_IsOperationDone(t *testing.T) {
})
}
}

func TestTimelockInspector_GetMinDelay(t *testing.T) {
t.Parallel()

type args struct {
mcmsAddr string
}
tests := []struct {
name string
args args
mockSetup func(m *mock_mcms.MCMS)
want uint64
wantErr assert.ErrorAssertionFunc
}{
{
name: "success",
args: args{
mcmsAddr: "0x123",
},
mockSetup: func(m *mock_mcms.MCMS) {
mockMCMSModule := mock_module_mcms.NewMCMSInterface(t)
m.EXPECT().MCMS().Return(mockMCMSModule)
mockMCMSModule.EXPECT().TimelockMinDelay(
mock.Anything,
).Return(uint64(321), nil)
},
want: 321,
wantErr: assert.NoError,
},
{
name: "failure - invalid MCMS address",
args: args{
mcmsAddr: "invalidaddress",
},
wantErr: AssertErrorContains("parse MCMS address"),
},
{
name: "failure - TimelockMinDelay failed",
args: args{
mcmsAddr: "0x123",
},
mockSetup: func(m *mock_mcms.MCMS) {
mockMCMSModule := mock_module_mcms.NewMCMSInterface(t)
m.EXPECT().MCMS().Return(mockMCMSModule)
mockMCMSModule.EXPECT().TimelockMinDelay(
mock.Anything,
).Return(uint64(0), errors.New("error during TimelockMinDelay"))
},
want: 0,
wantErr: AssertErrorContains("error during TimelockMinDelay"),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

mcmsBinding := mock_mcms.NewMCMS(t)
inspector := TimelockInspector{
bindingFn: func(mcmsAddress aptos.AccountAddress, _ aptos.AptosRpcClient) mcms.MCMS {
// only validate hex address if parsing was supposed to succeed
if tt.name != "failure - invalid MCMS address" {
require.Equal(t, Must(hexToAddress(tt.args.mcmsAddr)), mcmsAddress)
}

return mcmsBinding
},
}

if tt.mockSetup != nil {
tt.mockSetup(mcmsBinding)
}

got, err := inspector.GetMinDelay(t.Context(), tt.args.mcmsAddr)
if !tt.wantErr(t, err, fmt.Sprintf("GetMinDelay(%q)", tt.args.mcmsAddr)) {
return
}
assert.Equal(t, tt.want, got)
})
}
}
14 changes: 14 additions & 0 deletions sdk/evm/timelock_inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,17 @@ func (tm TimelockInspector) IsOperationDone(ctx context.Context, address string,

return timelock.IsOperationDone(&bind.CallOpts{Context: ctx}, opID)
}

// GetMinDelay returns the minimum delay for the timelock at the given address
func (tm TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) {
tl, err := bindings.NewRBACTimelock(common.HexToAddress(address), tm.client)
if err != nil {
return 0, err
}
d, err := tl.GetMinDelay(&bind.CallOpts{Context: ctx})
if err != nil {
return 0, err
}

return d.Uint64(), nil
}
Loading
Loading