-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtimelock_proposal.go
210 lines (172 loc) · 6.47 KB
/
timelock_proposal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package mcms
import (
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/go-playground/validator/v10"
"github.com/smartcontractkit/mcms/internal/utils/safecast"
"github.com/smartcontractkit/mcms/sdk"
"github.com/smartcontractkit/mcms/types"
)
var ZERO_HASH = common.Hash{}
type TimelockProposal struct {
BaseProposal
Action types.TimelockAction `json:"action" validate:"required,oneof=schedule cancel bypass"`
Delay types.Duration `json:"delay"`
TimelockAddresses map[types.ChainSelector]string `json:"timelockAddresses" validate:"required,min=1"`
Operations []types.BatchOperation `json:"operations" validate:"required,min=1,dive"`
SaltOverride *common.Hash `json:"salt,omitempty"`
}
var _ ProposalInterface = (*TimelockProposal)(nil)
// NewTimelockProposal unmarshal data from the reader to JSON and returns a new TimelockProposal.
// The predecessors parameter is a list of readers that contain the predecessors
// for the proposal for configuring operations counts, which makes the following
// assumptions:
// - The order of the predecessors array is the order in which the proposals are
// intended to be executed.
// - The op counts for the first proposal are meant to be the starting op for the
// full set of proposals.
// - The op counts for all other proposals except the first are ignored
// - all proposals are configured correctly and need no additional modifications
func NewTimelockProposal(r io.Reader, predecessors []io.Reader) (*TimelockProposal, error) {
return newProposal[*TimelockProposal](r, predecessors)
}
func WriteTimelockProposal(w io.Writer, p *TimelockProposal) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(p)
}
// TransactionCounts returns the number of transactions for each chain in the proposal
func (m *TimelockProposal) TransactionCounts() map[types.ChainSelector]uint64 {
counts := make(map[types.ChainSelector]uint64)
for _, op := range m.Operations {
counts[op.ChainSelector] += uint64(len(op.Transactions))
}
return counts
}
// Salt returns a unique salt for the proposal.
// We need the salt to be unique in case you use an identical operation again
// on the same chain across two different proposals. Predecessor protects against
// duplicates within the same proposal
func (m *TimelockProposal) Salt() [32]byte {
if m.SaltOverride != nil {
return *m.SaltOverride
}
// If the proposal doesn't have a salt, we create one from the
// valid until timestamp.
var salt [32]byte
binary.BigEndian.PutUint32(salt[:], m.ValidUntil)
return salt
}
func (m *TimelockProposal) Validate() error {
// Run tag-based validation
validate := validator.New()
if err := validate.Struct(m); err != nil {
return err
}
if m.Kind != types.KindTimelockProposal {
return NewInvalidProposalKindError(m.Kind, types.KindTimelockProposal)
}
// Validate all chains in transactions have an entry in chain metadata
for _, op := range m.Operations {
if _, ok := m.ChainMetadata[op.ChainSelector]; !ok {
return NewChainMetadataNotFoundError(op.ChainSelector)
}
for _, tx := range op.Transactions {
// Chain specific validations.
if err := ValidateAdditionalFields(tx.AdditionalFields, op.ChainSelector); err != nil {
return err
}
}
}
if err := timeLockProposalValidateBasic(*m); err != nil {
return err
}
return nil
}
// Convert the proposal to an MCMS only proposal and also return all predecessors for easy access later.
// Every transaction to be sent from the Timelock is encoded with the corresponding timelock method.
func (m *TimelockProposal) Convert(
ctx context.Context,
converters map[types.ChainSelector]sdk.TimelockConverter,
) (Proposal, []common.Hash, error) {
// 1) Clone the base proposal, update the kind, etc.
baseProposal := m.BaseProposal
baseProposal.Kind = types.KindProposal
// 2) Initialize the global predecessors slice
predecessors := make([]common.Hash, len(m.Operations))
// 3) Keep track of the last operation ID per chain
lastOpID := make(map[types.ChainSelector]common.Hash)
// Initialize them to ZERO_HASH
for sel := range m.ChainMetadata {
lastOpID[sel] = ZERO_HASH
}
// 4) Rebuild chainMetadata in baseProposal
chainMetadataMap := make(map[types.ChainSelector]types.ChainMetadata)
for chain, metadata := range m.ChainMetadata {
chainMetadataMap[chain] = types.ChainMetadata{
StartingOpCount: metadata.StartingOpCount,
MCMAddress: metadata.MCMAddress,
}
}
baseProposal.ChainMetadata = chainMetadataMap
// 5) We’ll build the final MCMS-only proposal
result := Proposal{
BaseProposal: baseProposal,
}
// 6) Loop through operations in *global* order
for i, bop := range m.Operations {
chainSelector := bop.ChainSelector
// If the chain isn't in converters, bail out
converter, ok := converters[chainSelector]
if !ok {
return Proposal{}, nil, fmt.Errorf("unable to find converter for chain selector %d", chainSelector)
}
chainMetadata, ok := m.ChainMetadata[chainSelector]
if !ok {
return Proposal{}, nil, fmt.Errorf("missing chain metadata for chainSelector %d", chainSelector)
}
// The predecessor for this op is the lastOpID for its chain
predecessor := lastOpID[chainSelector]
predecessors[i] = predecessor
timelockAddr := m.TimelockAddresses[chainSelector]
// Convert the batch operation
convertedOps, operationID, err := converter.ConvertBatchToChainOperations(
ctx,
bop,
timelockAddr,
chainMetadata.MCMAddress,
m.Delay,
m.Action,
predecessor,
m.Salt(),
)
if err != nil {
return Proposal{}, nil, err
}
// Append the converted operation to the MCMS only proposal
result.Operations = append(result.Operations, convertedOps...)
// Update lastOpID for that chain
lastOpID[chainSelector] = operationID
}
// 7) Return the MCMS-only proposal + the single slice of predecessors
return result, predecessors, nil
}
// timeLockProposalValidateBasic basic validation for an MCMS proposal
func timeLockProposalValidateBasic(timelockProposal TimelockProposal) error {
// Get the current Unix timestamp as an int64
currentTime := time.Now().Unix()
currentTimeCasted, err := safecast.Int64ToUint32(currentTime)
if err != nil {
return err
}
if timelockProposal.ValidUntil <= currentTimeCasted {
// ValidUntil is a Unix timestamp, so it should be greater than the current time
return NewInvalidValidUntilError(timelockProposal.ValidUntil)
}
return nil
}