Skip to content

Commit 9585ed2

Browse files
nik-surikcsongor
authored andcommitted
Conform to wormhole governance standard
1 parent 100949b commit 9585ed2

File tree

2 files changed

+211
-41
lines changed

2 files changed

+211
-41
lines changed

src/wormhole/Governance.sol

+82-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,25 @@
22
pragma solidity >=0.8.8 <0.9.0;
33

44
import "wormhole-solidity-sdk/interfaces/IWormhole.sol";
5+
import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
56

67
contract Governance {
8+
using BytesParsing for bytes;
9+
10+
// "GeneralPurposeGovernance" (left padded)
11+
bytes32 public constant MODULE =
12+
0x000000000000000047656E6572616C507572706F7365476F7665726E616E6365;
13+
14+
enum GovernanceAction {
15+
UNDEFINED,
16+
EVM_CALL
17+
}
18+
719
IWormhole immutable wormhole;
820

21+
error PayloadTooLong(uint256 size);
22+
error InvalidModule(bytes32 module);
23+
error InvalidAction(uint8 action);
924
error InvalidGovernanceChainId(uint16 chainId);
1025
error InvalidGovernanceContract(bytes32 contractAddress);
1126
error GovernanceActionAlreadyConsumed(bytes32 digest);
@@ -27,8 +42,22 @@ contract Governance {
2742
}
2843
}
2944

30-
struct GovernanceMessage {
31-
uint16 chainId;
45+
/*
46+
* @dev General purpose governance message to call arbitrary methods on a governed smart contract.
47+
* This message adheres to the Wormhole governance packet standard: https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0002_governance_messaging.md
48+
* The wire format for this message is:
49+
* - module - 32 bytes
50+
* - action - 1 byte
51+
* - chain - 2 bytes
52+
* - governanceContract - 20 bytes
53+
* - governedContract - 20 bytes
54+
* - callDataLength - 2 bytes
55+
* - callData - `callDataLength` bytes
56+
*/
57+
struct GeneralPurposeGovernanceMessage {
58+
bytes32 module;
59+
uint8 action;
60+
uint16 chain;
3261
address governanceContract;
3362
address governedContract;
3463
bytes callData;
@@ -40,19 +69,28 @@ contract Governance {
4069

4170
function performGovernance(bytes calldata vaa) external {
4271
IWormhole.VM memory verified = _verifyGovernanceVAA(vaa);
43-
GovernanceMessage memory message = abi.decode(verified.payload, (GovernanceMessage));
72+
GeneralPurposeGovernanceMessage memory message =
73+
parseGeneralPurposeGovernanceMessage(verified.payload);
4474

45-
if (message.chainId != wormhole.chainId()) {
46-
revert NotRecipientChain(message.chainId);
75+
if (message.module != MODULE) {
76+
revert InvalidModule(message.module);
4777
}
78+
79+
if (message.action != uint8(GovernanceAction.EVM_CALL)) {
80+
revert InvalidAction(message.action);
81+
}
82+
83+
if (message.chain != wormhole.chainId()) {
84+
revert NotRecipientChain(message.chain);
85+
}
86+
4887
if (message.governanceContract != address(this)) {
4988
revert NotRecipientContract(message.governanceContract);
5089
}
51-
bytes memory callData = message.callData;
5290

5391
// TODO: any other checks? the call is trusted (signed by guardians),
5492
// but what's the worst that could happen to this contract?
55-
(bool success, bytes memory returnData) = message.governedContract.call(callData);
93+
(bool success, bytes memory returnData) = message.governedContract.call(message.callData);
5694
if (!success) {
5795
revert(string(returnData));
5896
}
@@ -89,4 +127,41 @@ contract Governance {
89127

90128
return vm;
91129
}
130+
131+
function encodeGeneralPurposeGovernanceMessage(GeneralPurposeGovernanceMessage memory m)
132+
public
133+
pure
134+
returns (bytes memory encoded)
135+
{
136+
if (m.callData.length > type(uint16).max) {
137+
revert PayloadTooLong(m.callData.length);
138+
}
139+
uint16 callDataLength = uint16(m.callData.length);
140+
return abi.encodePacked(
141+
m.module,
142+
m.action,
143+
m.chain,
144+
m.governanceContract,
145+
m.governedContract,
146+
callDataLength,
147+
m.callData
148+
);
149+
}
150+
151+
function parseGeneralPurposeGovernanceMessage(bytes memory encoded)
152+
public
153+
pure
154+
returns (GeneralPurposeGovernanceMessage memory message)
155+
{
156+
uint256 offset = 0;
157+
(message.module, offset) = encoded.asBytes32Unchecked(offset);
158+
(message.action, offset) = encoded.asUint8Unchecked(offset);
159+
(message.chain, offset) = encoded.asUint16Unchecked(offset);
160+
(message.governanceContract, offset) = encoded.asAddressUnchecked(offset);
161+
(message.governedContract, offset) = encoded.asAddressUnchecked(offset);
162+
uint256 callDataLength;
163+
(callDataLength, offset) = encoded.asUint16Unchecked(offset);
164+
(message.callData, offset) = encoded.sliceUnchecked(offset, callDataLength);
165+
encoded.checkLength(offset);
166+
}
92167
}

test/wormhole/Governance.t.sol

+129-34
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import "openzeppelin-contracts/contracts/access/Ownable.sol";
77
import "wormhole-solidity-sdk/testing/helpers/WormholeSimulator.sol";
88

99
contract GovernedContract is Ownable {
10+
error RandomError();
11+
1012
bool public governanceStuffCalled;
1113

1214
function governanceStuff() public onlyOwner {
1315
governanceStuffCalled = true;
1416
}
17+
18+
function governanceRevert() public view onlyOwner {
19+
revert RandomError();
20+
}
1521
}
1622

1723
contract GovernanceTest is Test {
@@ -34,14 +40,22 @@ contract GovernanceTest is Test {
3440
myContract.transferOwnership(address(governance));
3541
}
3642

37-
function test_governance() public {
38-
uint16 thisChain = wormhole.chainId();
39-
40-
Governance.GovernanceMessage memory message = Governance.GovernanceMessage({
41-
chainId: thisChain,
42-
governanceContract: address(governance),
43-
governedContract: address(myContract),
44-
callData: abi.encodeWithSignature("governanceStuff()")
43+
function buildGovernanceVaa(
44+
bytes32 module,
45+
uint8 action,
46+
uint16 chainId,
47+
address governanceContract,
48+
address governedContract,
49+
bytes memory callData
50+
) public view returns (bytes memory) {
51+
Governance.GeneralPurposeGovernanceMessage memory message = Governance
52+
.GeneralPurposeGovernanceMessage({
53+
module: module,
54+
action: action,
55+
chain: chainId,
56+
governanceContract: governanceContract,
57+
governedContract: governedContract,
58+
callData: callData
4559
});
4660

4761
IWormhole.VM memory vaa = IWormhole.VM({
@@ -52,13 +66,110 @@ contract GovernanceTest is Test {
5266
emitterAddress: wormhole.governanceContract(),
5367
sequence: 0,
5468
consistencyLevel: 200,
55-
payload: abi.encode(message),
69+
payload: governance.encodeGeneralPurposeGovernanceMessage(message),
5670
guardianSetIndex: 0,
5771
signatures: new IWormhole.Signature[](0),
5872
hash: bytes32("")
5973
});
6074

61-
bytes memory signed = guardian.encodeAndSignMessage(vaa);
75+
return guardian.encodeAndSignMessage(vaa);
76+
}
77+
78+
function test_invalidModule() public {
79+
uint16 thisChain = wormhole.chainId();
80+
bytes32 coreBridgeModule =
81+
0x00000000000000000000000000000000000000000000000000000000436f7265;
82+
83+
bytes memory signed = buildGovernanceVaa(
84+
coreBridgeModule,
85+
uint8(Governance.GovernanceAction.EVM_CALL),
86+
thisChain,
87+
address(governance),
88+
address(myContract),
89+
abi.encodeWithSignature("governanceStuff()")
90+
);
91+
92+
vm.expectRevert(abi.encodeWithSignature("InvalidModule(bytes32)", coreBridgeModule));
93+
governance.performGovernance(signed);
94+
}
95+
96+
// TODO: this should ideally test all actions that != 1
97+
function test_invalidAction() public {
98+
uint16 thisChain = wormhole.chainId();
99+
100+
bytes memory signed = buildGovernanceVaa(
101+
governance.MODULE(),
102+
uint8(Governance.GovernanceAction.UNDEFINED),
103+
thisChain,
104+
address(governance),
105+
address(myContract),
106+
abi.encodeWithSignature("governanceStuff()")
107+
);
108+
109+
vm.expectRevert(abi.encodeWithSignature("InvalidAction(uint8)", 0));
110+
governance.performGovernance(signed);
111+
}
112+
113+
// TODO: this should ideally test all chainIds that != wormhole.chainId()
114+
function test_invalidChain() public {
115+
bytes memory signed = buildGovernanceVaa(
116+
governance.MODULE(),
117+
uint8(Governance.GovernanceAction.EVM_CALL),
118+
0,
119+
address(governance),
120+
address(myContract),
121+
abi.encodeWithSignature("governanceStuff()")
122+
);
123+
124+
vm.expectRevert(abi.encodeWithSignature("NotRecipientChain(uint16)", 0));
125+
governance.performGovernance(signed);
126+
}
127+
128+
// TODO: this should ideally test lots of address possibilities
129+
function test_invalidRecipientContract() public {
130+
uint16 thisChain = wormhole.chainId();
131+
address random = address(0x1234);
132+
133+
bytes memory signed = buildGovernanceVaa(
134+
governance.MODULE(),
135+
uint8(Governance.GovernanceAction.EVM_CALL),
136+
thisChain,
137+
random,
138+
address(myContract),
139+
abi.encodeWithSignature("governanceStuff()")
140+
);
141+
142+
vm.expectRevert(abi.encodeWithSignature("NotRecipientContract(address)", random));
143+
governance.performGovernance(signed);
144+
}
145+
146+
function test_governanceCallFailure() public {
147+
uint16 thisChain = wormhole.chainId();
148+
149+
bytes memory signed = buildGovernanceVaa(
150+
governance.MODULE(),
151+
uint8(Governance.GovernanceAction.EVM_CALL),
152+
thisChain,
153+
address(governance),
154+
address(myContract),
155+
abi.encodeWithSignature("governanceRevert()")
156+
);
157+
158+
vm.expectRevert(abi.encodeWithSignature("RandomError()"));
159+
governance.performGovernance(signed);
160+
}
161+
162+
function test_governance() public {
163+
uint16 thisChain = wormhole.chainId();
164+
165+
bytes memory signed = buildGovernanceVaa(
166+
governance.MODULE(),
167+
uint8(Governance.GovernanceAction.EVM_CALL),
168+
thisChain,
169+
address(governance),
170+
address(myContract),
171+
abi.encodeWithSignature("governanceStuff()")
172+
);
62173

63174
governance.performGovernance(signed);
64175

@@ -69,28 +180,14 @@ contract GovernanceTest is Test {
69180
address newOwner = address(0x1234);
70181
uint16 thisChain = wormhole.chainId();
71182

72-
Governance.GovernanceMessage memory message = Governance.GovernanceMessage({
73-
chainId: thisChain,
74-
governanceContract: address(governance),
75-
governedContract: address(myContract),
76-
callData: abi.encodeWithSignature("transferOwnership(address)", newOwner)
77-
});
78-
79-
IWormhole.VM memory vaa = IWormhole.VM({
80-
version: 1,
81-
timestamp: uint32(block.timestamp),
82-
nonce: 0,
83-
emitterChainId: wormhole.governanceChainId(),
84-
emitterAddress: wormhole.governanceContract(),
85-
sequence: 0,
86-
consistencyLevel: 200,
87-
payload: abi.encode(message),
88-
guardianSetIndex: 0,
89-
signatures: new IWormhole.Signature[](0),
90-
hash: bytes32("")
91-
});
92-
93-
bytes memory signed = guardian.encodeAndSignMessage(vaa);
183+
bytes memory signed = buildGovernanceVaa(
184+
governance.MODULE(),
185+
uint8(Governance.GovernanceAction.EVM_CALL),
186+
thisChain,
187+
address(governance),
188+
address(myContract),
189+
abi.encodeWithSignature("transferOwnership(address)", newOwner)
190+
);
94191

95192
governance.performGovernance(signed);
96193

@@ -99,6 +196,4 @@ contract GovernanceTest is Test {
99196

100197
assert(myContract.governanceStuffCalled());
101198
}
102-
103-
// TODO: tests triggering the error conditions
104199
}

0 commit comments

Comments
 (0)