Skip to content

Commit 8bdc909

Browse files
authored
chore: zeus script for task replay fix (#1632)
**Motivation:** Zeus scripts for `v1.8.1` of the protocol that handles the task replay fix in the TaskMailbox. **Modifications:** New release `v1.8.1` for `preprod`, `testnet-sepolia` and `testnet-base-sepolia` **Result:** Testnet fix ready
1 parent cae0440 commit 8bdc909

File tree

4 files changed

+379
-0
lines changed

4 files changed

+379
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import {EOADeployer} from "zeus-templates/templates/EOADeployer.sol";
5+
import "../Env.sol";
6+
7+
/**
8+
* @title DeployTaskMailboxImpl
9+
* @notice Deploy new TaskMailbox implementation with task replay fix for destination chains.
10+
* This fixes a vulnerability where certificates could be replayed across different tasks
11+
* directed at the same operator set by including the taskHash in the messageHash.
12+
*/
13+
contract DeployTaskMailboxImpl is EOADeployer {
14+
using Env for *;
15+
16+
/// forgefmt: disable-next-item
17+
function _runAsEOA() internal override {
18+
// If we're not on a destination chain and not on version 1.8.0, we don't need to deploy any contracts
19+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
20+
return;
21+
}
22+
23+
vm.startBroadcast();
24+
25+
// Deploy TaskMailbox implementation with the replay fix
26+
deployImpl({
27+
name: type(TaskMailbox).name,
28+
deployedTo: address(
29+
new TaskMailbox({
30+
_bn254CertificateVerifier: address(Env.proxy.bn254CertificateVerifier()),
31+
_ecdsaCertificateVerifier: address(Env.proxy.ecdsaCertificateVerifier()),
32+
_maxTaskSLA: Env.MAX_TASK_SLA(),
33+
_version: Env.deployVersion()
34+
})
35+
)
36+
});
37+
38+
vm.stopBroadcast();
39+
}
40+
41+
function testScript() public virtual {
42+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
43+
return;
44+
}
45+
46+
// Deploy the new TaskMailbox implementation
47+
runAsEOA();
48+
49+
_validateNewImplAddress();
50+
_validateProxyAdmin();
51+
_validateImplConstructor();
52+
_validateImplInitialized();
53+
_validateVersion();
54+
}
55+
56+
/// @dev Validate that the new TaskMailbox impl address is distinct from the current one
57+
function _validateNewImplAddress() internal view {
58+
address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox()));
59+
address newImpl = address(Env.impl.taskMailbox());
60+
61+
assertFalse(currentImpl == newImpl, "TaskMailbox impl should be different from current implementation");
62+
}
63+
64+
/// @dev Validate that the TaskMailbox proxy is still owned by the correct ProxyAdmin
65+
function _validateProxyAdmin() internal view {
66+
address pa = Env.proxyAdmin();
67+
68+
assertTrue(Env._getProxyAdmin(address(Env.proxy.taskMailbox())) == pa, "TaskMailbox proxyAdmin incorrect");
69+
}
70+
71+
/// @dev Validate the immutables set in the new TaskMailbox implementation constructor
72+
function _validateImplConstructor() internal view {
73+
TaskMailbox taskMailboxImpl = Env.impl.taskMailbox();
74+
75+
// Validate version
76+
assertEq(
77+
keccak256(bytes(taskMailboxImpl.version())),
78+
keccak256(bytes(Env.deployVersion())),
79+
"TaskMailbox impl version mismatch"
80+
);
81+
82+
// Validate certificate verifiers
83+
assertTrue(
84+
taskMailboxImpl.BN254_CERTIFICATE_VERIFIER() == address(Env.proxy.bn254CertificateVerifier()),
85+
"TaskMailbox BN254_CERTIFICATE_VERIFIER mismatch"
86+
);
87+
assertTrue(
88+
taskMailboxImpl.ECDSA_CERTIFICATE_VERIFIER() == address(Env.proxy.ecdsaCertificateVerifier()),
89+
"TaskMailbox ECDSA_CERTIFICATE_VERIFIER mismatch"
90+
);
91+
92+
// Validate MAX_TASK_SLA
93+
assertEq(taskMailboxImpl.MAX_TASK_SLA(), Env.MAX_TASK_SLA(), "TaskMailbox MAX_TASK_SLA mismatch");
94+
}
95+
96+
/// @dev Validate that the new implementation cannot be initialized (should revert)
97+
function _validateImplInitialized() internal {
98+
bytes memory errInit = "Initializable: contract is already initialized";
99+
100+
TaskMailbox taskMailboxImpl = Env.impl.taskMailbox();
101+
102+
vm.expectRevert(errInit);
103+
taskMailboxImpl.initialize(
104+
address(0), // owner
105+
0, // feeSplit
106+
address(0) // feeSplitCollector
107+
);
108+
}
109+
110+
/// @dev Validate the version is correctly set
111+
function _validateVersion() internal view {
112+
assertEq(
113+
keccak256(bytes(Env.impl.taskMailbox().version())),
114+
keccak256(bytes(Env.deployVersion())),
115+
"TaskMailbox version should match deploy version"
116+
);
117+
}
118+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import {DeployTaskMailboxImpl} from "./1-deployTaskMailboxImpl.s.sol";
5+
import "../Env.sol";
6+
7+
import {MultisigBuilder} from "zeus-templates/templates/MultisigBuilder.sol";
8+
import {MultisigCall, Encode} from "zeus-templates/utils/Encode.sol";
9+
10+
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
11+
12+
/**
13+
* @title QueueTaskMailboxUpgrade
14+
* @notice Queue the TaskMailbox upgrade transaction in the Timelock via the Operations Multisig.
15+
* This queues the upgrade to fix the task replay vulnerability.
16+
*/
17+
contract QueueTaskMailboxUpgrade is MultisigBuilder, DeployTaskMailboxImpl {
18+
using Env for *;
19+
using Encode for *;
20+
21+
function _runAsMultisig() internal virtual override prank(Env.opsMultisig()) {
22+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
23+
return;
24+
}
25+
26+
bytes memory calldata_to_executor = _getCalldataToExecutor();
27+
28+
TimelockController timelock = Env.timelockController();
29+
timelock.schedule({
30+
target: Env.executorMultisig(),
31+
value: 0,
32+
data: calldata_to_executor,
33+
predecessor: 0,
34+
salt: 0,
35+
delay: timelock.getMinDelay()
36+
});
37+
}
38+
39+
/// @dev Get the calldata to be sent from the timelock to the executor
40+
function _getCalldataToExecutor() internal returns (bytes memory) {
41+
/// forgefmt: disable-next-item
42+
MultisigCall[] storage executorCalls = Encode.newMultisigCalls().append({
43+
to: Env.proxyAdmin(),
44+
data: Encode.proxyAdmin.upgrade({
45+
proxy: address(Env.proxy.taskMailbox()),
46+
impl: address(Env.impl.taskMailbox())
47+
})
48+
});
49+
50+
return Encode.gnosisSafe.execTransaction({
51+
from: address(Env.timelockController()),
52+
to: Env.multiSendCallOnly(),
53+
op: Encode.Operation.DelegateCall,
54+
data: Encode.multiSend(executorCalls)
55+
});
56+
}
57+
58+
function testScript() public virtual override {
59+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
60+
return;
61+
}
62+
63+
// Deploy the new TaskMailbox implementation first
64+
runAsEOA();
65+
66+
TimelockController timelock = Env.timelockController();
67+
bytes memory calldata_to_executor = _getCalldataToExecutor();
68+
bytes32 txHash = timelock.hashOperation({
69+
target: Env.executorMultisig(),
70+
value: 0,
71+
data: calldata_to_executor,
72+
predecessor: 0,
73+
salt: 0
74+
});
75+
76+
// Check that the upgrade does not exist in the timelock
77+
assertFalse(timelock.isOperationPending(txHash), "Transaction should NOT be queued yet");
78+
79+
// Queue the upgrade
80+
execute();
81+
82+
// Check that the upgrade has been added to the timelock
83+
assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued");
84+
assertFalse(timelock.isOperationReady(txHash), "Transaction should NOT be ready immediately");
85+
assertFalse(timelock.isOperationDone(txHash), "Transaction should NOT be done");
86+
87+
// Validate that the TaskMailbox proxy still points to the old implementation
88+
address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox()));
89+
address newImpl = address(Env.impl.taskMailbox());
90+
assertFalse(currentImpl == newImpl, "TaskMailbox proxy should still point to old implementation");
91+
}
92+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity ^0.8.12;
3+
4+
import "../Env.sol";
5+
import {QueueTaskMailboxUpgrade} from "./2-queueTaskMailboxUpgrade.s.sol";
6+
import {Encode} from "zeus-templates/utils/Encode.sol";
7+
8+
/**
9+
* @title ExecuteTaskMailboxUpgrade
10+
* @notice Execute the queued TaskMailbox upgrade after the timelock delay.
11+
* This completes the upgrade to fix the task replay vulnerability where certificates
12+
* could be replayed across different tasks directed at the same operator set.
13+
*/
14+
contract ExecuteTaskMailboxUpgrade is QueueTaskMailboxUpgrade {
15+
using Env for *;
16+
using Encode for *;
17+
18+
function _runAsMultisig() internal override prank(Env.protocolCouncilMultisig()) {
19+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
20+
return;
21+
}
22+
23+
bytes memory calldata_to_executor = _getCalldataToExecutor();
24+
TimelockController timelock = Env.timelockController();
25+
26+
timelock.execute({
27+
target: Env.executorMultisig(),
28+
value: 0,
29+
payload: calldata_to_executor,
30+
predecessor: 0,
31+
salt: 0
32+
});
33+
}
34+
35+
function testScript() public virtual override {
36+
if (!(Env.isDestinationChain() && Env._strEq(Env.envVersion(), "1.8.0"))) {
37+
return;
38+
}
39+
40+
// 1 - Deploy. The new TaskMailbox implementation has been deployed
41+
runAsEOA();
42+
43+
TimelockController timelock = Env.timelockController();
44+
bytes memory calldata_to_executor = _getCalldataToExecutor();
45+
bytes32 txHash = timelock.hashOperation({
46+
target: Env.executorMultisig(),
47+
value: 0,
48+
data: calldata_to_executor,
49+
predecessor: 0,
50+
salt: 0
51+
});
52+
53+
// 2 - Queue. Check that the operation IS ready
54+
QueueTaskMailboxUpgrade._runAsMultisig();
55+
_unsafeResetHasPranked(); // reset hasPranked so we can use it again
56+
57+
assertTrue(timelock.isOperationPending(txHash), "Transaction should be queued");
58+
assertFalse(timelock.isOperationReady(txHash), "Transaction should NOT be ready immediately");
59+
assertFalse(timelock.isOperationDone(txHash), "Transaction should NOT be complete");
60+
61+
// 3 - Warp past the timelock delay
62+
vm.warp(block.timestamp + timelock.getMinDelay());
63+
assertTrue(timelock.isOperationReady(txHash), "Transaction should be ready for execution");
64+
65+
// 4 - Execute the upgrade
66+
execute();
67+
assertTrue(timelock.isOperationDone(txHash), "v1.8.1 TaskMailbox upgrade should be complete");
68+
69+
// 5 - Validate the upgrade was successful
70+
_validateUpgradeComplete();
71+
_validateProxyAdmin();
72+
_validateProxyConstructor();
73+
_validateProxyInitialized();
74+
_validateGetMessageHash();
75+
}
76+
77+
/// @dev Validate that the TaskMailbox proxy now points to the new implementation
78+
function _validateUpgradeComplete() internal view {
79+
address currentImpl = Env._getProxyImpl(address(Env.proxy.taskMailbox()));
80+
address expectedImpl = address(Env.impl.taskMailbox());
81+
82+
assertTrue(currentImpl == expectedImpl, "TaskMailbox proxy should point to new implementation");
83+
}
84+
85+
/// @dev Validate the proxy's constructor values through the proxy
86+
function _validateProxyConstructor() internal view {
87+
TaskMailbox taskMailbox = Env.proxy.taskMailbox();
88+
89+
// Validate version
90+
assertEq(
91+
keccak256(bytes(taskMailbox.version())),
92+
keccak256(bytes(Env.deployVersion())),
93+
"TaskMailbox version mismatch"
94+
);
95+
96+
// Validate certificate verifiers
97+
assertTrue(
98+
taskMailbox.BN254_CERTIFICATE_VERIFIER() == address(Env.proxy.bn254CertificateVerifier()),
99+
"TaskMailbox BN254_CERTIFICATE_VERIFIER mismatch"
100+
);
101+
assertTrue(
102+
taskMailbox.ECDSA_CERTIFICATE_VERIFIER() == address(Env.proxy.ecdsaCertificateVerifier()),
103+
"TaskMailbox ECDSA_CERTIFICATE_VERIFIER mismatch"
104+
);
105+
106+
// Validate MAX_TASK_SLA
107+
assertEq(taskMailbox.MAX_TASK_SLA(), Env.MAX_TASK_SLA(), "TaskMailbox MAX_TASK_SLA mismatch");
108+
}
109+
110+
/// @dev Validate that the proxy cannot be re-initialized
111+
function _validateProxyInitialized() internal {
112+
bytes memory errInit = "Initializable: contract is already initialized";
113+
114+
TaskMailbox taskMailbox = Env.proxy.taskMailbox();
115+
116+
vm.expectRevert(errInit);
117+
taskMailbox.initialize(
118+
address(0), // owner
119+
0, // feeSplit
120+
address(0) // feeSplitCollector
121+
);
122+
}
123+
124+
/// @dev Validate that the new getMessageHash function works correctly
125+
function _validateGetMessageHash() internal view {
126+
TaskMailbox taskMailbox = Env.proxy.taskMailbox();
127+
128+
// Test the new getMessageHash function with sample data
129+
bytes32 taskHash = keccak256("test_task");
130+
bytes memory result = abi.encode("test_result");
131+
132+
// The new implementation should compute messageHash as keccak256(abi.encode(taskHash, result))
133+
bytes32 expectedMessageHash = keccak256(abi.encode(taskHash, result));
134+
bytes32 actualMessageHash = taskMailbox.getMessageHash(taskHash, result);
135+
136+
assertTrue(
137+
expectedMessageHash == actualMessageHash,
138+
"getMessageHash should compute correct message hash with taskHash included"
139+
);
140+
141+
// Verify that different tasks with same result produce different message hashes
142+
bytes32 differentTaskHash = keccak256("different_task");
143+
bytes32 differentMessageHash = taskMailbox.getMessageHash(differentTaskHash, result);
144+
145+
assertFalse(
146+
actualMessageHash == differentMessageHash,
147+
"Different tasks with same result should produce different message hashes"
148+
);
149+
}
150+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "hourglass-testnet-replay-fix-v1.8.1",
3+
"from": "1.8.0",
4+
"to": "1.8.1",
5+
"phases": [
6+
{
7+
"type": "eoa",
8+
"filename": "1-deployTaskMailboxImpl.s.sol"
9+
},
10+
{
11+
"type": "multisig",
12+
"filename": "2-queueTaskMailboxUpgrade.s.sol"
13+
},
14+
{
15+
"type": "multisig",
16+
"filename": "3-executeTaskMailboxUpgrade.s.sol"
17+
}
18+
]
19+
}

0 commit comments

Comments
 (0)