Skip to content

Commit 34d9a19

Browse files
committed
feat: snapShotExecutor oracle deadman switch and transfer
1 parent 5e01785 commit 34d9a19

File tree

4 files changed

+99
-6
lines changed

4 files changed

+99
-6
lines changed

scripts/yearnBorg.s.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ contract YearnBorgDeployScript is Script {
4444
uint256 snapShotWaitingPeriod = 3 days; // TODO Is it still necessary?
4545
uint256 snapShotCancelPeriod = 2 days;
4646
uint256 snapShotPendingProposalLimit = 3;
47+
uint256 snapShotTtl = 30 days;
4748
address oracle = 0xf00c0dE09574805389743391ada2A0259D6b7a00;
4849

4950
SafeTxHelper safeTxHelper;
@@ -108,7 +109,7 @@ contract YearnBorgDeployScript is Script {
108109
// Create SnapShotExecutor
109110

110111
executorAuth = new BorgAuth();
111-
snapShotExecutor = new SnapShotExecutor(executorAuth, address(oracle), snapShotWaitingPeriod, snapShotCancelPeriod, snapShotPendingProposalLimit);
112+
snapShotExecutor = new SnapShotExecutor(executorAuth, address(oracle), snapShotWaitingPeriod, snapShotCancelPeriod, snapShotPendingProposalLimit, snapShotTtl);
112113

113114
// Add modules
114115

src/libs/governance/snapShotExecutor.sol

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@ import "../auth.sol";
55
import "openzeppelin/contracts/utils/Address.sol";
66

77
contract SnapShotExecutor is BorgAuthACL {
8+
uint256 public immutable ORACLE_TTL;
89

910
address public oracle;
11+
address public pendingOracle;
1012
uint256 public waitingPeriod;
1113
uint256 public cancelPeriod;
1214
uint256 public pendingProposalCount;
1315
uint256 public pendingProposalLimit;
16+
uint256 public lastOraclePingTimestamp;
1417

1518
struct proposal {
1619
address target;
@@ -25,6 +28,7 @@ contract SnapShotExecutor is BorgAuthACL {
2528
error SnapShotExecutor_WaitingPeriod();
2629
error SnapShotExeuctor_InvalidParams();
2730
error SnapShotExecutor_TooManyPendingProposals();
31+
error SnapShotExecutor_OracleNotDead();
2832

2933
//events
3034
event ProposalCreated(bytes32 indexed proposalId, address indexed target, uint256 value, bytes cdata, string description, uint256 timestamp);
@@ -33,18 +37,36 @@ contract SnapShotExecutor is BorgAuthACL {
3337

3438
mapping(bytes32 => proposal) public pendingProposals;
3539

40+
/// @dev Check if `msg.sender` is either the oracle or is pending to be one. If it's the latter, transfer it. Also ping for TTL checks.
3641
modifier onlyOracle() {
37-
if (msg.sender != oracle) revert SnapShotExecutor_NotAuthorized();
42+
if (msg.sender != oracle) {
43+
if (msg.sender == pendingOracle) {
44+
// Pending oracle can accept the transfer
45+
oracle = pendingOracle;
46+
pendingOracle = address(0);
47+
} else {
48+
// Not authorized if neither oracle nor pending oracle
49+
revert SnapShotExecutor_NotAuthorized();
50+
}
51+
}
52+
lastOraclePingTimestamp = block.timestamp;
3853
_;
3954
}
4055

41-
constructor(BorgAuth _auth, address _oracle, uint256 _waitingPeriod, uint256 _cancelPeriod, uint256 _pendingProposals) BorgAuthACL(_auth) {
56+
modifier onlyDeadOracle() {
57+
if (block.timestamp < lastOraclePingTimestamp + ORACLE_TTL) revert SnapShotExecutor_OracleNotDead();
58+
_;
59+
}
60+
61+
constructor(BorgAuth _auth, address _oracle, uint256 _waitingPeriod, uint256 _cancelPeriod, uint256 _pendingProposals, uint256 _oracleTtl) BorgAuthACL(_auth) {
4262
oracle = _oracle;
4363
if(_waitingPeriod < 1 minutes) revert SnapShotExeuctor_InvalidParams();
4464
waitingPeriod = _waitingPeriod;
4565
if(_cancelPeriod < 1 minutes) revert SnapShotExeuctor_InvalidParams();
4666
cancelPeriod = _cancelPeriod;
4767
pendingProposalLimit = _pendingProposals;
68+
ORACLE_TTL = _oracleTtl;
69+
lastOraclePingTimestamp = block.timestamp;
4870
}
4971

5072
function propose(address target, uint256 value, bytes calldata cdata, string memory description) external onlyOracle() returns (bytes32) {
@@ -75,4 +97,9 @@ contract SnapShotExecutor is BorgAuthACL {
7597
emit ProposalCanceled(proposalId, p.target, p.value, p.cdata, p.description, p.timestamp);
7698
}
7799

78-
}
100+
function transferOracle(address newOracle) external onlyOwner() onlyDeadOracle() {
101+
pendingOracle = newOracle;
102+
}
103+
104+
function ping() external onlyOracle() {}
105+
}

test/snapShotExecutor.t.sol

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ contract SnapShotExecutorTest is Test {
1313

1414
address owner = vm.addr(1);
1515
address oracle = vm.addr(2);
16-
address alice = vm.addr(3);
16+
address newOracle = vm.addr(3);
17+
address alice = vm.addr(4);
1718

1819
BorgAuth auth;
1920
SnapShotExecutor snapShotExecutor;
@@ -29,7 +30,8 @@ contract SnapShotExecutorTest is Test {
2930
oracle,
3031
3 days, // waitingPeriod
3132
2 days, // cancelPeriod
32-
3 // pendingProposalLimit
33+
3, // pendingProposalLimit
34+
30 days // ttl
3335
);
3436

3537
// Transferring auth ownership
@@ -40,10 +42,13 @@ contract SnapShotExecutorTest is Test {
4042
/// @dev Metadata should meet specs
4143
function testMeta() public view {
4244
assertEq(snapShotExecutor.oracle(), oracle, "Unexpected oracle address");
45+
assertEq(snapShotExecutor.pendingOracle(), address(0), "Unexpected pending oracle address");
4346
assertEq(snapShotExecutor.waitingPeriod(), 3 days, "Unexpected waitingPeriod");
4447
assertEq(snapShotExecutor.cancelPeriod(), 2 days, "Unexpected cancelPeriod");
4548
assertEq(snapShotExecutor.pendingProposalCount(), 0, "Unexpected pendingProposalCount");
4649
assertEq(snapShotExecutor.pendingProposalLimit(), 3, "Unexpected pendingProposalLimit");
50+
assertEq(snapShotExecutor.ORACLE_TTL(), 30 days, "Unexpected ORACLE_TTL");
51+
assertEq(snapShotExecutor.lastOraclePingTimestamp(), block.timestamp, "Unexpected lastOraclePingTimestamp");
4752
}
4853

4954
/// @dev BorgAuth instances should be properly assigned and configured
@@ -203,4 +208,63 @@ contract SnapShotExecutorTest is Test {
203208

204209
vm.stopPrank();
205210
}
211+
212+
/// @dev Ping timestamp should update when oracle is working
213+
function testPing() public {
214+
uint256 lastOraclePingTimestamp = snapShotExecutor.lastOraclePingTimestamp();
215+
216+
// Non-oracle shouldn't be able to ping
217+
vm.expectRevert(abi.encodeWithSelector(SnapShotExecutor.SnapShotExecutor_NotAuthorized.selector));
218+
snapShotExecutor.ping();
219+
220+
// Last timestamp should update after a successful ping
221+
skip(1 days);
222+
vm.prank(oracle);
223+
snapShotExecutor.ping();
224+
assertEq(snapShotExecutor.lastOraclePingTimestamp(), lastOraclePingTimestamp + 1 days);
225+
226+
// Propose should also ping
227+
skip(1 days);
228+
vm.prank(oracle);
229+
snapShotExecutor.propose(
230+
address(alice), // target
231+
0, // value
232+
"", // cdata
233+
"Arbitrary instruction"
234+
);
235+
assertEq(snapShotExecutor.lastOraclePingTimestamp(), lastOraclePingTimestamp + 2 days);
236+
}
237+
238+
/// @dev Owner should be able to replace oracle if it's dead
239+
function testTransferOracle() public {
240+
skip(snapShotExecutor.ORACLE_TTL());
241+
vm.prank(owner);
242+
snapShotExecutor.transferOracle(newOracle);
243+
244+
// Old oracle should still work when the transfer is pending
245+
vm.prank(oracle);
246+
snapShotExecutor.ping();
247+
assertEq(snapShotExecutor.oracle(), oracle);
248+
249+
// Non-oracle should still be unauthorized
250+
vm.expectRevert(abi.encodeWithSelector(SnapShotExecutor.SnapShotExecutor_NotAuthorized.selector));
251+
snapShotExecutor.ping();
252+
253+
// Transfer should be done after the new oracle interacts
254+
vm.prank(newOracle);
255+
snapShotExecutor.ping();
256+
assertEq(snapShotExecutor.oracle(), newOracle);
257+
assertEq(snapShotExecutor.pendingOracle(), address(0));
258+
// Old oracle should no longer be authorized
259+
vm.expectRevert(abi.encodeWithSelector(SnapShotExecutor.SnapShotExecutor_NotAuthorized.selector));
260+
vm.prank(oracle);
261+
snapShotExecutor.ping();
262+
}
263+
264+
/// @dev Owner should not be able to replace oracle if it's not dead
265+
function test_RevertIf_SetOracleNotDead() public {
266+
vm.expectRevert(abi.encodeWithSelector(SnapShotExecutor.SnapShotExecutor_OracleNotDead.selector));
267+
vm.prank(owner);
268+
snapShotExecutor.transferOracle(newOracle);
269+
}
206270
}

test/yearnBorgAcceptance.t.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ contract YearnBorgAcceptanceTest is Test {
157157
assertEq(snapShotExecutor.waitingPeriod(), 3 days, "Unexpected waitingPeriod");
158158
assertEq(snapShotExecutor.cancelPeriod(), 2 days, "Unexpected cancelPeriod");
159159
assertEq(snapShotExecutor.pendingProposalLimit(), 3, "Unexpected pendingProposalLimit");
160+
assertEq(snapShotExecutor.ORACLE_TTL(), 30 days, "Unexpected ORACLE_TTL");
160161
}
161162

162163
function testEjectImplantMeta() public {

0 commit comments

Comments
 (0)