-
Notifications
You must be signed in to change notification settings - Fork 53
/
Copy pathGovernance.sol
245 lines (209 loc) · 9.54 KB
/
Governance.sol
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;
import "wormhole-solidity-sdk/interfaces/IWormhole.sol";
import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
contract Governance {
using BytesParsing for bytes;
// Only 2 Guardian signatures are required for quorum to call the pause function on governed contracts.
uint public constant PAUSER_QUORUM = 2;
// "GeneralPurposeGovernance" (left padded)
bytes32 public constant MODULE =
0x000000000000000047656E6572616C507572706F7365476F7665726E616E6365;
/// @notice The known set of governance actions.
/// @dev As the governance logic is expanded to more runtimes, it's
/// important to keep them in sync, at least the newer ones should ensure
/// they don't overlap with the existing ones.
///
/// Existing implementations are not strongly required to be updated
/// to be aware of new actions (as they will never need to know the
/// action indices higher than the one corresponding to the current
/// runtime), but it's good practice.
///
/// When adding a new runtime, make sure to at least update in the README.md
enum GovernanceAction {
UNDEFINED,
EVM_CALL,
SOLANA_CALL
}
IWormhole immutable wormhole;
error PayloadTooLong(uint256 size);
error InvalidModule(bytes32 module);
error InvalidAction(uint8 action);
error InvalidGovernanceChainId(uint16 chainId);
error InvalidGovernanceContract(bytes32 contractAddress);
error GovernanceActionAlreadyConsumed(bytes32 digest);
error NotRecipientChain(uint16 chainId);
error NotRecipientContract(address contractAddress);
bytes32 constant CONSUMED_GOVERNANCE_ACTIONS_SLOT =
bytes32(uint256(keccak256("governance.consumedGovernanceActions")) - 1);
function _getConsumedGovernanceActionsStorage()
private
pure
returns (mapping(bytes32 => bool) storage $)
{
uint256 slot = uint256(CONSUMED_GOVERNANCE_ACTIONS_SLOT);
assembly ("memory-safe") {
$.slot := slot
}
}
/*
* @dev General purpose governance message to call arbitrary methods on a governed smart contract.
* This message adheres to the Wormhole governance packet standard: https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0002_governance_messaging.md
* The wire format for this message is:
* - MODULE - 32 bytes
* - action - 1 byte
* - chain - 2 bytes
* - governanceContract - 20 bytes
* - governedContract - 20 bytes
* - callDataLength - 2 bytes
* - callData - `callDataLength` bytes
*/
struct GeneralPurposeGovernanceMessage {
uint8 action;
uint16 chain;
address governanceContract;
address governedContract;
bytes callData;
}
constructor(address _wormhole) {
wormhole = IWormhole(_wormhole);
}
function performGovernance(bytes calldata vaa) external {
IWormhole.VM memory verified = _verifyGovernanceVAA(vaa);
GeneralPurposeGovernanceMessage memory message =
parseGeneralPurposeGovernanceMessage(verified.payload);
if (message.action != uint8(GovernanceAction.EVM_CALL)) {
revert InvalidAction(message.action);
}
if (message.chain != wormhole.chainId()) {
revert NotRecipientChain(message.chain);
}
if (message.governanceContract != address(this)) {
revert NotRecipientContract(message.governanceContract);
}
// TODO: any other checks? the call is trusted (signed by guardians),
// but what's the worst that could happen to this contract?
(bool success, bytes memory returnData) = message.governedContract.call(message.callData);
if (!success) {
revert(string(returnData));
}
}
function _replayProtect(bytes32 digest) internal {
mapping(bytes32 => bool) storage $ = _getConsumedGovernanceActionsStorage();
if ($[digest]) {
revert GovernanceActionAlreadyConsumed(digest);
}
$[digest] = true;
}
function _verifyGovernanceVAA(bytes memory encodedVM)
internal
returns (IWormhole.VM memory parsedVM)
{
IWormhole.VM memory vm = wormhole.parseVM(encodedVM);
GeneralPurposeGovernanceMessage memory message =
parseGeneralPurposeGovernanceMessage(vm.payload);
bytes memory pauseSig = abi.encodeWithSignature("pause()");
if (keccak256(message.callData) == keccak256(pauseSig)) {
// If we're calling the pause() function, only require 2 Guardian signatures
(bool valid, string memory reason) = _verifyVMForPause(vm);
if (!valid) {
revert(reason);
}
} else {
// If we're calling any other function signature, require the full 13 Guardian signatures
(bool valid, string memory reason) = wormhole.verifyVM(vm);
if (!valid) {
revert(reason);
}
}
if (vm.emitterChainId != wormhole.governanceChainId()) {
revert InvalidGovernanceChainId(vm.emitterChainId);
}
if (vm.emitterAddress != wormhole.governanceContract()) {
revert InvalidGovernanceContract(vm.emitterAddress);
}
_replayProtect(vm.hash);
return vm;
}
/**
* @dev LOGIC COPIED FROM WORMHOLE CORE CONTRACT AND UPDATED TO HANDLE THE PAUSING CASE.
* `verifyVMInternal` serves to validate an arbitrary vm against a valid Guardian set.
* This function is only meant to be called after `parseVM`, so the hash field can be trusted.
* It will return true if the number of signatures on the VAA is at least the number required for either full quorum or pausing quorum.
*/
function _verifyVMForPause(IWormhole.VM memory vm) internal view returns (bool valid, string memory reason) {
/// @dev Obtain the current guardianSet for the guardianSetIndex provided
IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(vm.guardianSetIndex);
/**
* @dev Checks whether the guardianSet has zero keys
* WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure
* that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet
* key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and
* signature verification.
*/
if(guardianSet.keys.length == 0){
return (false, "invalid guardian set");
}
/// @dev Checks if VM guardian set index matches the current index (unless the current set is expired).
if(vm.guardianSetIndex != wormhole.getCurrentGuardianSetIndex() && guardianSet.expirationTime < block.timestamp){
return (false, "guardian set has expired");
}
/**
* @dev We're using a fixed point number transformation with 1 decimal to deal with rounding.
* WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM
* if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and
* vm.signatures length is 0, this could compromise the integrity of both vm and signature verification.
*/
uint fullQuorum = wormhole.quorum(guardianSet.keys.length);
uint requiredPauseQuorum = PAUSER_QUORUM < fullQuorum ? PAUSER_QUORUM : fullQuorum;
if (vm.signatures.length < requiredPauseQuorum){
return (false, "no quorum");
}
/// @dev Verify the proposed vm.signatures against the guardianSet
(bool signaturesValid, string memory invalidReason) = wormhole.verifySignatures(vm.hash, vm.signatures, guardianSet);
if(!signaturesValid){
return (false, invalidReason);
}
/// If we are here, we've validated the VM is a valid multi-sig that matches the guardianSet.
return (true, "");
}
function encodeGeneralPurposeGovernanceMessage(GeneralPurposeGovernanceMessage memory m)
public
pure
returns (bytes memory encoded)
{
if (m.callData.length > type(uint16).max) {
revert PayloadTooLong(m.callData.length);
}
uint16 callDataLength = uint16(m.callData.length);
return abi.encodePacked(
MODULE,
m.action,
m.chain,
m.governanceContract,
m.governedContract,
callDataLength,
m.callData
);
}
function parseGeneralPurposeGovernanceMessage(bytes memory encoded)
public
pure
returns (GeneralPurposeGovernanceMessage memory message)
{
uint256 offset = 0;
bytes32 module;
(module, offset) = encoded.asBytes32Unchecked(offset);
if (module != MODULE) {
revert InvalidModule(module);
}
(message.action, offset) = encoded.asUint8Unchecked(offset);
(message.chain, offset) = encoded.asUint16Unchecked(offset);
(message.governanceContract, offset) = encoded.asAddressUnchecked(offset);
(message.governedContract, offset) = encoded.asAddressUnchecked(offset);
uint256 callDataLength;
(callDataLength, offset) = encoded.asUint16Unchecked(offset);
(message.callData, offset) = encoded.sliceUnchecked(offset, callDataLength);
encoded.checkLength(offset);
}
}