Skip to content

Commit 259bcbc

Browse files
committed
evm: allow calling pause() with two Guardian signatures
1 parent c8aab39 commit 259bcbc

File tree

2 files changed

+121
-5
lines changed

2 files changed

+121
-5
lines changed

evm/src/wormhole/Governance.sol

+94-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
77
contract Governance {
88
using BytesParsing for bytes;
99

10+
// Only 2 Guardian signatures are required for quorum to call the pause function on governed contracts.
11+
uint PAUSER_QUORUM = 2;
12+
1013
// "GeneralPurposeGovernance" (left padded)
1114
bytes32 public constant MODULE =
1215
0x000000000000000047656E6572616C507572706F7365476F7665726E616E6365;
@@ -79,6 +82,8 @@ contract Governance {
7982
}
8083

8184
function performGovernance(bytes calldata vaa) external {
85+
86+
8287
IWormhole.VM memory verified = _verifyGovernanceVAA(vaa);
8388
GeneralPurposeGovernanceMessage memory message =
8489
parseGeneralPurposeGovernanceMessage(verified.payload);
@@ -115,11 +120,23 @@ contract Governance {
115120
internal
116121
returns (IWormhole.VM memory parsedVM)
117122
{
118-
(IWormhole.VM memory vm, bool valid, string memory reason) =
119-
wormhole.parseAndVerifyVM(encodedVM);
123+
IWormhole.VM memory vm = wormhole.parseVM(encodedVM);
124+
GeneralPurposeGovernanceMessage memory message =
125+
parseGeneralPurposeGovernanceMessage(vm.payload);
120126

121-
if (!valid) {
122-
revert(reason);
127+
bytes memory pauseSig = abi.encodeWithSignature("pause()");
128+
if (keccak256(message.callData) == keccak256(pauseSig)) {
129+
// If we're calling the pause() function, only require 2 Guardian signatures
130+
(bool valid, string memory reason) = _verifyVMForPause(vm, true);
131+
if (!valid) {
132+
revert(reason);
133+
}
134+
} else {
135+
// If we're calling any other function signature, require the full 13 Guardian signatures
136+
(bool valid, string memory reason) = wormhole.verifyVM(vm);
137+
if (!valid) {
138+
revert(reason);
139+
}
123140
}
124141

125142
if (vm.emitterChainId != wormhole.governanceChainId()) {
@@ -135,6 +152,79 @@ contract Governance {
135152
return vm;
136153
}
137154

155+
/**
156+
* @dev COPIED FROM WORMHOLE CORE CONTRACT AND RENAMED TO `verifyVMForPause`
157+
* `verifyVMInternal` serves to validate an arbitrary vm against a valid Guardian set
158+
* if checkHash is set then the hash field of the vm is verified against the hash of its contents
159+
* in the case that the vm is securely parsed and the hash field can be trusted, checkHash can be set to false
160+
* as the check would be redundant
161+
*/
162+
function _verifyVMForPause(IWormhole.VM memory vm, bool checkHash) internal view returns (bool valid, string memory reason) {
163+
/// @dev Obtain the current guardianSet for the guardianSetIndex provided
164+
IWormhole.GuardianSet memory guardianSet = wormhole.getGuardianSet(vm.guardianSetIndex);
165+
166+
/**
167+
* Verify that the hash field in the vm matches with the hash of the contents of the vm if checkHash is set
168+
* WARNING: This hash check is critical to ensure that the vm.hash provided matches with the hash of the body.
169+
* Without this check, it would not be safe to call verifyVM on it's own as vm.hash can be a valid signed hash
170+
* but the body of the vm could be completely different from what was actually signed by the guardians
171+
*/
172+
if(checkHash){
173+
bytes memory body = abi.encodePacked(
174+
vm.timestamp,
175+
vm.nonce,
176+
vm.emitterChainId,
177+
vm.emitterAddress,
178+
vm.sequence,
179+
vm.consistencyLevel,
180+
vm.payload
181+
);
182+
183+
bytes32 vmHash = keccak256(abi.encodePacked(keccak256(body)));
184+
185+
if(vmHash != vm.hash){
186+
return (false, "vm.hash doesn't match body");
187+
}
188+
}
189+
190+
/**
191+
* @dev Checks whether the guardianSet has zero keys
192+
* WARNING: This keys check is critical to ensure the guardianSet has keys present AND to ensure
193+
* that guardianSet key size doesn't fall to zero and negatively impact quorum assessment. If guardianSet
194+
* key length is 0 and vm.signatures length is 0, this could compromise the integrity of both vm and
195+
* signature verification.
196+
*/
197+
if(guardianSet.keys.length == 0){
198+
return (false, "invalid guardian set");
199+
}
200+
201+
/// @dev Checks if VM guardian set index matches the current index (unless the current set is expired).
202+
if(vm.guardianSetIndex != wormhole.getCurrentGuardianSetIndex() && guardianSet.expirationTime < block.timestamp){
203+
return (false, "guardian set has expired");
204+
}
205+
206+
/**
207+
* @dev We're using a fixed point number transformation with 1 decimal to deal with rounding.
208+
* WARNING: This quorum check is critical to assessing whether we have enough Guardian signatures to validate a VM
209+
* if making any changes to this, obtain additional peer review. If guardianSet key length is 0 and
210+
* vm.signatures length is 0, this could compromise the integrity of both vm and signature verification.
211+
*/
212+
uint fullQuorum = wormhole.quorum(guardianSet.keys.length);
213+
uint requiredPauseQuorum = PAUSER_QUORUM < fullQuorum ? PAUSER_QUORUM : fullQuorum;
214+
if (vm.signatures.length < requiredPauseQuorum){
215+
return (false, "no quorum");
216+
}
217+
218+
/// @dev Verify the proposed vm.signatures against the guardianSet
219+
(bool signaturesValid, string memory invalidReason) = wormhole.verifySignatures(vm.hash, vm.signatures, guardianSet);
220+
if(!signaturesValid){
221+
return (false, invalidReason);
222+
}
223+
224+
/// If we are here, we've validated the VM is a valid multi-sig that matches the guardianSet.
225+
return (true, "");
226+
}
227+
138228
function encodeGeneralPurposeGovernanceMessage(GeneralPurposeGovernanceMessage memory m)
139229
public
140230
pure

evm/test/wormhole/Governance.t.sol

+27-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ contract GovernedContract is Ownable {
1111
error RandomError();
1212

1313
bool public governanceStuffCalled;
14+
bool public paused;
1415

1516
function governanceStuff() public onlyOwner {
1617
governanceStuffCalled = true;
@@ -19,6 +20,10 @@ contract GovernedContract is Ownable {
1920
function governanceRevert() public view onlyOwner {
2021
revert RandomError();
2122
}
23+
24+
function pause() public onlyOwner {
25+
paused = true;
26+
}
2227
}
2328

2429
contract GovernanceTest is Test {
@@ -215,4 +220,25 @@ contract GovernanceTest is Test {
215220

216221
assert(myContract.governanceStuffCalled());
217222
}
218-
}
223+
224+
function test_pause_single_guardian() public {
225+
// build VAA to call pause
226+
// should go through smoothly (requiredQuorum == 1)
227+
}
228+
229+
function test_pause_multi_guardian() public {
230+
// hijack vm to overwrite Wormhole Guardian set to be 5 random private keys?
231+
// requiredQuorum == 2
232+
// build VAA to call pause, sign with 2 Guardians
233+
// this should succeed
234+
}
235+
236+
function test_pause_multi_guardian_fail() public {
237+
// hijack vm to overwrite Wormhole Guardian set to be 5 random private keys?
238+
// requiredQuorum == 2
239+
// build VAA to call pause, sign with 1 Guardian
240+
// this should fail since it won't hit quorum
241+
}
242+
243+
// copy/paste any other relevant tests from the Wormhole Core Contract
244+
}

0 commit comments

Comments
 (0)