Skip to content

Commit 58c4da1

Browse files
authored
feat: add utils to generate cancel / bypass proposals from timelock schedule proposal (#342)
add utils to generate cancel / bypass proposals from timelock schedule proposal ## AI Summary This pull request introduces new methods to the `TimelockProposal` struct in `timelock_proposal.go` to enhance its functionality. The most significant changes include the addition of methods to derive cancellation and bypass proposals from an existing proposal. Enhancements to `TimelockProposal`: * Added `DeriveCancellationProposal` method to derive a new proposal that cancels the current proposal. This method ensures the original proposal remains unaffected and returns an error if the action is not of type 'schedule'. * Added `DeriveBypassProposal` method to derive a new proposal that bypasses the current proposal. Similar to the cancellation method, it ensures the original proposal remains unaffected and returns an error if the action is not of type 'schedule'.
1 parent 2ef2b56 commit 58c4da1

File tree

3 files changed

+188
-0
lines changed

3 files changed

+188
-0
lines changed

.changeset/shy-suits-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smartcontractkit/mcms": minor
3+
---
4+
5+
Add proposal cancel and bypass helpers

timelock_proposal.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
)
1818

1919
var ZERO_HASH = common.Hash{}
20+
var DefaultValidUntil = 72 * time.Hour
2021

2122
type TimelockProposal struct {
2223
BaseProposal
@@ -105,6 +106,59 @@ func (m *TimelockProposal) Validate() error {
105106

106107
return nil
107108
}
109+
func replaceChainMetadataWithAddresses(p *TimelockProposal, addresses map[types.ChainSelector]types.ChainMetadata) error {
110+
for chain := range p.ChainMetadata {
111+
newMeta, ok := addresses[chain]
112+
if !ok {
113+
return fmt.Errorf("cannot replace addresses in chain metadata, missing address for chain %d", chain)
114+
}
115+
p.ChainMetadata[chain] = newMeta
116+
}
117+
118+
return nil
119+
}
120+
121+
// deriveNewProposal creates a copy of the current proposal with overridden action, signatures, salt, and metadata.
122+
func (m *TimelockProposal) deriveNewProposal(action types.TimelockAction, metadata map[types.ChainSelector]types.ChainMetadata) (TimelockProposal, error) {
123+
// Create a copy of the current proposal, we don't want to affect the original proposal
124+
newProposal := *m
125+
newProposal.Signatures = []types.Signature{}
126+
ts := time.Now().Add(DefaultValidUntil).Unix()
127+
ts32, err := safecast.Int64ToUint32(ts)
128+
if err != nil {
129+
return TimelockProposal{}, err
130+
}
131+
// #nosec G115
132+
newProposal.ValidUntil = ts32
133+
bytesSalt := m.Salt()
134+
salt := common.BytesToHash(bytesSalt[:])
135+
newProposal.SaltOverride = &salt
136+
newProposal.Action = action
137+
err = replaceChainMetadataWithAddresses(&newProposal, metadata)
138+
if err != nil {
139+
return TimelockProposal{}, err
140+
}
141+
142+
return newProposal, nil
143+
}
144+
145+
// DeriveCancellationProposal derives a new proposal that cancels the current proposal.
146+
func (m *TimelockProposal) DeriveCancellationProposal(cancellerMetadata map[types.ChainSelector]types.ChainMetadata) (TimelockProposal, error) {
147+
if m.Action != types.TimelockActionSchedule {
148+
return TimelockProposal{}, fmt.Errorf("cannot derive a cancellation proposal from a non-schedule proposal. Action needs to be of type 'schedule'")
149+
}
150+
151+
return m.deriveNewProposal(types.TimelockActionCancel, cancellerMetadata)
152+
}
153+
154+
// DeriveBypassProposal derives a new proposal that bypasses the current proposal.
155+
func (m *TimelockProposal) DeriveBypassProposal(bypasserAddresses map[types.ChainSelector]types.ChainMetadata) (TimelockProposal, error) {
156+
if m.Action != types.TimelockActionSchedule {
157+
return TimelockProposal{}, fmt.Errorf("cannot derive a bypass proposal from a non-schedule proposal. Action needs to be of type 'schedule'")
158+
}
159+
160+
return m.deriveNewProposal(types.TimelockActionBypass, bypasserAddresses)
161+
}
108162

109163
// Convert the proposal to an MCMS only proposal and also return all predecessors for easy access later.
110164
// Every transaction to be sent from the Timelock is encoded with the corresponding timelock method.

timelock_proposal_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,3 +930,132 @@ func TestProposal_WithoutSaltOverride(t *testing.T) {
930930
assert.NotNil(t, saltBytes)
931931
assert.NotEqual(t, common.Hash{}, saltBytes)
932932
}
933+
934+
func buildDummyProposal(action types.TimelockAction) TimelockProposal {
935+
builder := NewTimelockProposalBuilder()
936+
builder.SetVersion("v1").
937+
SetValidUntil(2004259681).
938+
SetDescription("Test proposal").
939+
SetChainMetadata(map[types.ChainSelector]types.ChainMetadata{
940+
chaintest.Chain2Selector: {
941+
StartingOpCount: 0,
942+
MCMAddress: "0x0000000000000000000000000000000000000000",
943+
},
944+
}).
945+
SetOverridePreviousRoot(false).
946+
SetAction(action).
947+
SetDelay(types.MustParseDuration("1h")).
948+
AddTimelockAddress(chaintest.Chain2Selector, "0x01").
949+
SetOperations([]types.BatchOperation{{
950+
ChainSelector: chaintest.Chain2Selector,
951+
Transactions: []types.Transaction{
952+
{
953+
To: "0x0000000000000000000000000000000000000000",
954+
AdditionalFields: []byte(`{"value": 0}`),
955+
Data: []byte("data"),
956+
},
957+
},
958+
}})
959+
proposal, err := builder.Build()
960+
if err != nil {
961+
panic(err)
962+
}
963+
964+
return *proposal
965+
}
966+
967+
func TestDeriveBypassProposal(t *testing.T) {
968+
t.Parallel()
969+
970+
tests := []struct {
971+
name string
972+
proposal TimelockProposal
973+
wantErr bool
974+
wantAction types.TimelockAction
975+
}{
976+
{
977+
name: "valid schedule action",
978+
proposal: buildDummyProposal(types.TimelockActionSchedule),
979+
wantErr: false,
980+
wantAction: types.TimelockActionBypass,
981+
},
982+
{
983+
name: "invalid non-schedule action",
984+
proposal: buildDummyProposal(types.TimelockActionCancel),
985+
wantErr: true,
986+
wantAction: types.TimelockActionBypass,
987+
},
988+
}
989+
990+
for _, tt := range tests {
991+
t.Run(tt.name, func(t *testing.T) {
992+
t.Parallel()
993+
dummyAddressess := map[types.ChainSelector]types.ChainMetadata{
994+
chaintest.Chain2Selector: {
995+
StartingOpCount: 1,
996+
MCMAddress: "0x0000000000000000000000000000000000000001",
997+
},
998+
}
999+
newProposal, err := tt.proposal.DeriveBypassProposal(dummyAddressess)
1000+
if tt.wantErr {
1001+
require.Error(t, err)
1002+
assert.EqualError(t, err, "cannot derive a bypass proposal from a non-schedule proposal. Action needs to be of type 'schedule'")
1003+
} else {
1004+
require.NoError(t, err)
1005+
assert.Equal(t, tt.wantAction, newProposal.Action)
1006+
assert.Empty(t, newProposal.Signatures)
1007+
assert.NotEqual(t, tt.proposal, newProposal)
1008+
assert.Equal(t, newProposal.Salt(), tt.proposal.Salt())
1009+
assert.Equal(t, dummyAddressess, newProposal.ChainMetadata)
1010+
}
1011+
})
1012+
}
1013+
}
1014+
1015+
func TestDeriveCancellationProposal(t *testing.T) {
1016+
t.Parallel()
1017+
1018+
tests := []struct {
1019+
name string
1020+
proposal TimelockProposal
1021+
wantErr bool
1022+
wantAction types.TimelockAction
1023+
}{
1024+
{
1025+
name: "valid schedule action",
1026+
proposal: buildDummyProposal(types.TimelockActionSchedule),
1027+
wantErr: false,
1028+
wantAction: types.TimelockActionCancel,
1029+
},
1030+
{
1031+
name: "invalid non-schedule action",
1032+
proposal: buildDummyProposal(types.TimelockActionCancel),
1033+
wantErr: true,
1034+
wantAction: types.TimelockActionCancel,
1035+
},
1036+
}
1037+
1038+
for _, tt := range tests {
1039+
t.Run(tt.name, func(t *testing.T) {
1040+
t.Parallel()
1041+
dummyAddressess := map[types.ChainSelector]types.ChainMetadata{
1042+
chaintest.Chain2Selector: {
1043+
StartingOpCount: 1,
1044+
MCMAddress: "0x0000000000000000000000000000000000000001",
1045+
},
1046+
}
1047+
newProposal, err := tt.proposal.DeriveCancellationProposal(dummyAddressess)
1048+
if tt.wantErr {
1049+
require.Error(t, err)
1050+
assert.EqualError(t, err, "cannot derive a cancellation proposal from a non-schedule proposal. Action needs to be of type 'schedule'")
1051+
} else {
1052+
require.NoError(t, err)
1053+
assert.Equal(t, tt.wantAction, newProposal.Action)
1054+
assert.Empty(t, newProposal.Signatures)
1055+
assert.NotEqual(t, tt.proposal, newProposal)
1056+
assert.Equal(t, newProposal.Salt(), tt.proposal.Salt())
1057+
assert.Equal(t, dummyAddressess, newProposal.ChainMetadata)
1058+
}
1059+
})
1060+
}
1061+
}

0 commit comments

Comments
 (0)