Skip to content

Commit da72b6e

Browse files
authored
[entropy] First cut at EVM executor (#1148)
* executor * executor tests * basic test works * cleanup * cleanup * fix ci
1 parent ff0d9fe commit da72b6e

File tree

6 files changed

+285
-3
lines changed

6 files changed

+285
-3
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.12;
3+
4+
import "../pyth/PythGovernanceInstructions.sol";
5+
import "../wormhole/interfaces/IWormhole.sol";
6+
import "./ExecutorErrors.sol";
7+
8+
contract Executor {
9+
using BytesLib for bytes;
10+
11+
// Magic is `PTGM` encoded as a 4 byte data: Pyth Governance Message
12+
// TODO: it's annoying that we can't import this from PythGovernanceInstructions
13+
uint32 constant MAGIC = 0x5054474d;
14+
15+
PythGovernanceInstructions.GovernanceModule constant MODULE =
16+
PythGovernanceInstructions.GovernanceModule.EvmExecutor;
17+
18+
// Instruction indicating that the executor contract on
19+
// targetChainId at executorAddress should call the contract at callAddress
20+
// with the provided callData
21+
struct GovernanceInstruction {
22+
PythGovernanceInstructions.GovernanceModule module;
23+
ExecutorAction action;
24+
uint16 targetChainId;
25+
// The address of the specific executor that should perform the call.
26+
// This argument is included to support multiple Executors on the same blockchain.
27+
address executorAddress;
28+
address callAddress;
29+
bytes callData;
30+
}
31+
32+
// We have different actions here for potential future extensibility
33+
enum ExecutorAction {
34+
// TODO: add an instruction to change the governance data source.
35+
Execute // 0
36+
}
37+
38+
IWormhole private wormhole;
39+
uint64 private lastExecutedSequence;
40+
uint16 private chainId;
41+
42+
uint16 private ownerEmitterChainId;
43+
bytes32 private ownerEmitterAddress;
44+
45+
constructor(
46+
address _wormhole,
47+
uint64 _lastExecutedSequence,
48+
uint16 _chainId,
49+
uint16 _ownerEmitterChainId,
50+
bytes32 _ownerEmitterAddress
51+
) {
52+
wormhole = IWormhole(_wormhole);
53+
lastExecutedSequence = _lastExecutedSequence;
54+
chainId = _chainId;
55+
ownerEmitterChainId = _ownerEmitterChainId;
56+
ownerEmitterAddress = _ownerEmitterAddress;
57+
}
58+
59+
// Execute the contract call in the provided wormhole message.
60+
// The argument should be the bytes of a valid wormhole message
61+
// whose payload is a serialized GovernanceInstruction.
62+
function execute(
63+
bytes memory encodedVm
64+
) public returns (bytes memory response) {
65+
IWormhole.VM memory vm = verifyGovernanceVM(encodedVm);
66+
67+
GovernanceInstruction memory gi = parseGovernanceInstruction(
68+
vm.payload
69+
);
70+
71+
if (gi.targetChainId != chainId && gi.targetChainId != 0)
72+
revert ExecutorErrors.InvalidGovernanceTarget();
73+
74+
if (
75+
gi.action != ExecutorAction.Execute ||
76+
gi.executorAddress != address(this)
77+
) revert ExecutorErrors.DeserializationError();
78+
79+
bool success;
80+
(success, response) = address(gi.callAddress).call(gi.callData);
81+
82+
// Check if the call was successful or not.
83+
if (!success) {
84+
// If there is return data, the delegate call reverted with a reason or a custom error, which we bubble up.
85+
if (response.length > 0) {
86+
// The first word of response is the length, so when we call revert we add 1 word (32 bytes)
87+
// to give the pointer to the beginning of the revert data and pass the size as the second argument.
88+
assembly {
89+
let returndata_size := mload(response)
90+
revert(add(32, response), returndata_size)
91+
}
92+
} else {
93+
revert ExecutorErrors.ExecutionReverted();
94+
}
95+
}
96+
}
97+
98+
/// @dev Called when `msg.value` is not zero and the call data is empty.
99+
receive() external payable {}
100+
101+
// Check that the encoded VM is a valid wormhole VAA from the correct emitter
102+
// and with a sufficiently recent sequence number.
103+
function verifyGovernanceVM(
104+
bytes memory encodedVM
105+
) internal returns (IWormhole.VM memory parsedVM) {
106+
(IWormhole.VM memory vm, bool valid, ) = wormhole.parseAndVerifyVM(
107+
encodedVM
108+
);
109+
110+
if (!valid) revert ExecutorErrors.InvalidWormholeVaa();
111+
112+
if (
113+
vm.emitterChainId != ownerEmitterChainId ||
114+
vm.emitterAddress != ownerEmitterAddress
115+
) revert ExecutorErrors.UnauthorizedEmitter();
116+
117+
if (vm.sequence <= lastExecutedSequence)
118+
revert ExecutorErrors.MessageOutOfOrder();
119+
120+
lastExecutedSequence = vm.sequence;
121+
122+
return vm;
123+
}
124+
125+
/// @dev Parse a GovernanceInstruction
126+
function parseGovernanceInstruction(
127+
bytes memory encodedInstruction
128+
) public pure returns (GovernanceInstruction memory gi) {
129+
uint index = 0;
130+
131+
uint32 magic = encodedInstruction.toUint32(index);
132+
133+
if (magic != MAGIC) revert ExecutorErrors.DeserializationError();
134+
135+
index += 4;
136+
137+
uint8 modNumber = encodedInstruction.toUint8(index);
138+
gi.module = PythGovernanceInstructions.GovernanceModule(modNumber);
139+
index += 1;
140+
141+
if (gi.module != MODULE) revert PythErrors.InvalidGovernanceTarget();
142+
143+
uint8 actionNumber = encodedInstruction.toUint8(index);
144+
gi.action = ExecutorAction(actionNumber);
145+
index += 1;
146+
147+
gi.targetChainId = encodedInstruction.toUint16(index);
148+
index += 2;
149+
150+
gi.executorAddress = encodedInstruction.toAddress(index);
151+
index += 20;
152+
153+
gi.callAddress = encodedInstruction.toAddress(index);
154+
index += 20;
155+
156+
// As solidity performs math operations in a checked mode
157+
// if the length of the encoded instruction be smaller than index
158+
// it will revert. So we don't need any extra check.
159+
gi.callData = encodedInstruction.slice(
160+
index,
161+
encodedInstruction.length - index
162+
);
163+
}
164+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
library ExecutorErrors {
5+
// The provided message is not a valid Wormhole VAA.
6+
error InvalidWormholeVaa();
7+
// The message is coming from an emitter that is not authorized to use this contract.
8+
error UnauthorizedEmitter();
9+
// The sequence number of the provided message is before the last executed message.
10+
error MessageOutOfOrder();
11+
// The call to the provided contract executed without providing its own error.
12+
error ExecutionReverted();
13+
// The message could not be deserialized into an instruction
14+
error DeserializationError();
15+
// The message is not intended for this contract.
16+
error InvalidGovernanceTarget();
17+
}

target_chains/ethereum/contracts/contracts/pyth/PythGovernanceInstructions.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ contract PythGovernanceInstructions {
1919

2020
enum GovernanceModule {
2121
Executor, // 0
22-
Target // 1
22+
Target, // 1
23+
EvmExecutor // 2
2324
}
2425

2526
GovernanceModule constant MODULE = GovernanceModule.Target;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// SPDX-License-Identifier: Apache 2
2+
3+
pragma solidity ^0.8.12;
4+
5+
import "forge-std/Test.sol";
6+
import "@pythnetwork/entropy-sdk-solidity/EntropyStructs.sol";
7+
import "../contracts/executor/Executor.sol";
8+
import "./utils/WormholeTestUtils.t.sol";
9+
10+
contract ExecutorTest is Test, WormholeTestUtils {
11+
Wormhole public wormhole;
12+
Executor public executor;
13+
TestCallable public callable;
14+
15+
uint16 OWNER_CHAIN_ID = 7;
16+
bytes32 OWNER_EMITTER = bytes32(uint256(1));
17+
18+
uint8 NUM_SIGNERS = 1;
19+
20+
function setUp() public {
21+
address _wormhole = setUpWormholeReceiver(NUM_SIGNERS);
22+
executor = new Executor(
23+
_wormhole,
24+
0,
25+
CHAIN_ID,
26+
OWNER_CHAIN_ID,
27+
OWNER_EMITTER
28+
);
29+
callable = new TestCallable();
30+
}
31+
32+
function testExecute(
33+
address callAddress,
34+
bytes memory callData,
35+
uint64 sequence
36+
) internal returns (bytes memory vaa) {
37+
bytes memory payload = abi.encodePacked(
38+
uint32(0x5054474d),
39+
PythGovernanceInstructions.GovernanceModule.EvmExecutor,
40+
Executor.ExecutorAction.Execute,
41+
CHAIN_ID,
42+
address(executor),
43+
callAddress,
44+
callData
45+
);
46+
47+
vaa = generateVaa(
48+
uint32(block.timestamp),
49+
// TODO: make these arguments so we can do adversarial tests
50+
OWNER_CHAIN_ID,
51+
OWNER_EMITTER,
52+
sequence,
53+
payload,
54+
NUM_SIGNERS
55+
);
56+
57+
executor.execute(vaa);
58+
}
59+
60+
function testBasic() public {
61+
callable.reset();
62+
63+
uint32 c = callable.fooCount();
64+
assertEq(callable.lastCaller(), address(bytes20(0)));
65+
testExecute(address(callable), abi.encodeCall(ICallable.foo, ()), 1);
66+
assertEq(callable.fooCount(), c + 1);
67+
assertEq(callable.lastCaller(), address(executor));
68+
// Sanity check to make sure the check above is meaningful.
69+
assert(address(executor) != address(this));
70+
}
71+
72+
function testCallerAddress() public {
73+
uint32 c = callable.fooCount();
74+
testExecute(address(callable), abi.encodeCall(ICallable.foo, ()), 1);
75+
assertEq(callable.fooCount(), c + 1);
76+
}
77+
}
78+
79+
interface ICallable {
80+
function foo() external;
81+
82+
function reset() external;
83+
}
84+
85+
contract TestCallable is ICallable {
86+
uint32 public fooCount = 0;
87+
address public lastCaller = address(bytes20(0));
88+
89+
constructor() {}
90+
91+
function reset() external override {
92+
fooCount = 0;
93+
lastCaller = address(bytes20(0));
94+
}
95+
96+
function foo() external override {
97+
fooCount += 1;
98+
lastCaller = msg.sender;
99+
}
100+
}

target_chains/ethereum/contracts/foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[profile.default]
2-
solc_version = '0.8.4'
2+
solc_version = '0.8.23'
33
optimizer = true
44
optimizer_runs = 200
55
src = 'contracts'

target_chains/ethereum/contracts/truffle-config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module.exports = {
3333

3434
compilers: {
3535
solc: {
36-
version: "0.8.4",
36+
version: "0.8.23",
3737
settings: {
3838
optimizer: {
3939
enabled: true,

0 commit comments

Comments
 (0)