Skip to content

Commit 01577c0

Browse files
add withdrawing delay to allow to catch security issues (#23)
* add withdrawing delay to allow to catch security issues * document withdraw function
1 parent fba19f3 commit 01577c0

File tree

2 files changed

+109
-16
lines changed

2 files changed

+109
-16
lines changed

src/dot-migration/NODLMigration.sol

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ contract NODLMigration {
1111
struct Proposal {
1212
address target;
1313
uint256 amount;
14+
uint256 lastVote;
1415
uint8 totalVotes;
1516
bool executed;
1617
}
1718

1819
NODL public nodl;
1920
mapping(address => bool) public isOracle;
2021
uint8 public threshold;
22+
uint256 public delay;
2123

2224
// We track votes in a seperate mapping to avoid having to write helper functions to
2325
// expose the votes for each proposal.
@@ -28,16 +30,19 @@ contract NODLMigration {
2830
error AlreadyExecuted(bytes32 proposal);
2931
error ParametersChanged(bytes32 proposal);
3032
error NotAnOracle(address user);
33+
error NotYetWithdrawable(bytes32 proposal);
34+
error NotEnoughVotes(bytes32 proposal);
3135

3236
event VoteStarted(bytes32 indexed proposal, address oracle, address indexed user, uint256 amount);
3337
event Voted(bytes32 indexed proposal, address oracle);
34-
event Bridged(bytes32 indexed proposal, address indexed user, uint256 amount);
38+
event Withdrawn(bytes32 indexed proposal, address indexed user, uint256 amount);
3539

3640
/// @param bridgeOracles Array of oracle accounts that will be able to bridge the tokens.
3741
/// @param token Contract address of the NODL token.
3842
/// @param minVotes Minimum number of votes required to bridge the tokens. This needs to be
3943
/// less than or equal to the number of oracles and is expected to be above 1.
40-
constructor(address[] memory bridgeOracles, NODL token, uint8 minVotes) {
44+
/// @param minDelay Minimum delay in blocks before bridged tokens can be minted.
45+
constructor(address[] memory bridgeOracles, NODL token, uint8 minVotes, uint256 minDelay) {
4146
assert(bridgeOracles.length >= minVotes);
4247
assert(minVotes > 1);
4348

@@ -46,10 +51,11 @@ contract NODLMigration {
4651
}
4752
nodl = token;
4853
threshold = minVotes;
54+
delay = minDelay;
4955
}
5056

5157
/// @notice Bridge some tokens from the Nodle Parachain to the ZkSync contracts. This
52-
/// tracks "votes" from each oracle and only bridges the tokens if the threshold is met.
58+
/// tracks "votes" from each oracle and unlocks execution after a withdrawal delay.
5359
/// @param paraTxHash The transaction hash on the Parachain for this transfer.
5460
/// @param user The user address.
5561
/// @param amount The amount of NODL tokens that the user has burnt on the Parachain.
@@ -61,15 +67,22 @@ contract NODLMigration {
6167
_mustNotHaveVotedYet(paraTxHash, msg.sender);
6268
_mustNotBeChangingParameters(paraTxHash, user, amount);
6369
_recordVote(paraTxHash, msg.sender);
64-
65-
if (_enoughVotes(paraTxHash)) {
66-
_mintTokens(paraTxHash, user, amount);
67-
}
6870
} else {
6971
_createVote(paraTxHash, msg.sender, user, amount);
7072
}
7173
}
7274

75+
/// @notice Withdraw the NODL tokens from the contract to the user's address if the
76+
/// proposal has enough votes and has passed the safety delay.
77+
/// @param paraTxHash The transaction hash on the Parachain for this transfer.
78+
function withdraw(bytes32 paraTxHash) external {
79+
_mustNotHaveExecutedYet(paraTxHash);
80+
_mustHaveEnoughVotes(paraTxHash);
81+
_mustBePastSafetyDelay(paraTxHash);
82+
83+
_withdraw(paraTxHash, proposals[paraTxHash].target, proposals[paraTxHash].amount);
84+
}
85+
7386
function _mustBeAnOracle(address maybeOracle) internal view {
7487
if (!isOracle[maybeOracle]) {
7588
revert NotAnOracle(maybeOracle);
@@ -94,6 +107,18 @@ contract NODLMigration {
94107
}
95108
}
96109

110+
function _mustBePastSafetyDelay(bytes32 proposal) internal view {
111+
if (block.number - proposals[proposal].lastVote < delay) {
112+
revert NotYetWithdrawable(proposal);
113+
}
114+
}
115+
116+
function _mustHaveEnoughVotes(bytes32 proposal) internal view {
117+
if (proposals[proposal].totalVotes < threshold) {
118+
revert NotEnoughVotes(proposal);
119+
}
120+
}
121+
97122
function _proposalExists(bytes32 proposal) internal view returns (bool) {
98123
return proposals[proposal].totalVotes > 0 && proposals[proposal].amount > 0;
99124
}
@@ -103,6 +128,7 @@ contract NODLMigration {
103128
proposals[proposal].target = user;
104129
proposals[proposal].amount = amount;
105130
proposals[proposal].totalVotes = 1;
131+
proposals[proposal].lastVote = block.number;
106132

107133
emit VoteStarted(proposal, oracle, user, amount);
108134
}
@@ -111,18 +137,15 @@ contract NODLMigration {
111137
voted[oracle][proposal] = true;
112138
// this is safe since we are unlikely to have maxUint8 oracles to manage
113139
proposals[proposal].totalVotes += 1;
140+
proposals[proposal].lastVote = block.number;
114141

115142
emit Voted(proposal, oracle);
116143
}
117144

118-
function _enoughVotes(bytes32 proposal) internal view returns (bool) {
119-
return proposals[proposal].totalVotes >= threshold;
120-
}
121-
122-
function _mintTokens(bytes32 proposal, address user, uint256 amount) internal {
145+
function _withdraw(bytes32 proposal, address user, uint256 amount) internal {
123146
proposals[proposal].executed = true;
124147
nodl.mint(user, amount);
125148

126-
emit Bridged(proposal, user, amount);
149+
emit Withdrawn(proposal, user, amount);
127150
}
128151
}

test/dot-migration/NODLMigration.t.sol

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ contract NODLMigrationTest is Test {
1010
NODLMigration migration;
1111
NODL nodl;
1212

13+
uint256 delay = 100;
1314
address[] oracles = [vm.addr(1), vm.addr(2), vm.addr(3)];
1415
address user = vm.addr(4);
1516

1617
function setUp() public {
1718
nodl = new NODL();
18-
migration = new NODLMigration(oracles, nodl, 2);
19+
migration = new NODLMigration(oracles, nodl, 2, delay);
1920

2021
nodl.grantRole(nodl.MINTER_ROLE(), address(migration));
2122
}
@@ -25,6 +26,7 @@ contract NODLMigrationTest is Test {
2526
assertEq(migration.isOracle(oracles[i]), true);
2627
}
2728
assertEq(migration.threshold(), 2);
29+
assertEq(migration.delay(), delay);
2830
}
2931

3032
function test_configuredProperToken() public {
@@ -55,6 +57,9 @@ contract NODLMigrationTest is Test {
5557
vm.prank(oracles[1]);
5658
migration.bridge(0x0, user, 100);
5759

60+
vm.roll(block.number + delay + 1);
61+
migration.withdraw(0x0);
62+
5863
vm.expectRevert(abi.encodeWithSelector(NODLMigration.AlreadyExecuted.selector, 0x0));
5964
vm.prank(oracles[2]);
6065
migration.bridge(0x0, user, 100);
@@ -73,18 +78,83 @@ contract NODLMigrationTest is Test {
7378
migration.bridge(0x0, oracles[1], 100);
7479
}
7580

76-
function test_recordsVotesAndMintTokens() public {
81+
function test_recordsVotes() public {
7782
vm.expectEmit();
7883
emit NODLMigration.VoteStarted(0x0, oracles[0], user, 100);
7984
vm.prank(oracles[0]);
8085
migration.bridge(0x0, user, 100);
8186

87+
(address target, uint256 amount, uint256 lastVote, uint256 totalVotes, bool executed) = migration.proposals(0x0);
88+
assertEq(target, user);
89+
assertEq(amount, 100);
90+
assertEq(lastVote, block.number);
91+
assertEq(totalVotes, 1);
92+
assertEq(executed, false);
93+
8294
vm.expectEmit();
8395
emit NODLMigration.Voted(0x0, oracles[1]);
84-
emit NODLMigration.Bridged(0x0, user, 100);
8596
vm.prank(oracles[1]);
8697
migration.bridge(0x0, user, 100);
8798

99+
(target, amount, lastVote, totalVotes, executed) = migration.proposals(0x0);
100+
assertEq(target, user);
101+
assertEq(amount, 100);
102+
assertEq(lastVote, block.number);
103+
assertEq(totalVotes, 2);
104+
assertEq(executed, false);
105+
}
106+
107+
function test_mayNotWithdrawIfNotEnoughVotes() public {
108+
vm.prank(oracles[0]);
109+
migration.bridge(0x0, user, 100);
110+
111+
vm.expectRevert(abi.encodeWithSelector(NODLMigration.NotEnoughVotes.selector, 0x0));
112+
migration.withdraw(0x0);
113+
}
114+
115+
function test_mayNotWithdrawBeforeDelay() public {
116+
vm.prank(oracles[0]);
117+
migration.bridge(0x0, user, 100);
118+
119+
vm.prank(oracles[1]);
120+
migration.bridge(0x0, user, 100);
121+
122+
vm.expectRevert(abi.encodeWithSelector(NODLMigration.NotYetWithdrawable.selector, 0x0));
123+
migration.withdraw(0x0);
124+
}
125+
126+
function test_mayNotWithdrawTwice() public {
127+
vm.prank(oracles[0]);
128+
migration.bridge(0x0, user, 100);
129+
130+
vm.prank(oracles[1]);
131+
migration.bridge(0x0, user, 100);
132+
133+
vm.roll(block.number + delay + 1);
134+
135+
migration.withdraw(0x0);
136+
137+
vm.expectRevert(abi.encodeWithSelector(NODLMigration.AlreadyExecuted.selector, 0x0));
138+
migration.withdraw(0x0);
139+
}
140+
141+
function test_mayWithdrawAfterDelay() public {
142+
vm.prank(oracles[0]);
143+
migration.bridge(0x0, user, 100);
144+
145+
vm.prank(oracles[1]);
146+
migration.bridge(0x0, user, 100);
147+
148+
vm.roll(block.number + delay + 1);
149+
150+
vm.expectEmit();
151+
emit NODLMigration.Withdrawn(0x0, user, 100);
152+
vm.prank(user); // anybody can call withdraw
153+
migration.withdraw(0x0);
154+
155+
(,,,, bool executed) = migration.proposals(0x0);
156+
assertEq(executed, true);
157+
88158
assertEq(nodl.balanceOf(user), 100);
89159
}
90160
}

0 commit comments

Comments
 (0)