Skip to content

Commit 3989c03

Browse files
committed
Add VRF cutover preflight guard
1 parent 3f930c7 commit 3989c03

File tree

5 files changed

+355
-0
lines changed

5 files changed

+355
-0
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.1;
3+
4+
contract MockForgeVrfHistory {
5+
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
6+
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
7+
8+
struct RequestInfo {
9+
address user;
10+
uint256 requestId;
11+
uint8 status;
12+
uint256 randomNumber;
13+
uint256[] geodeTokenIds;
14+
uint256[] amountPerToken;
15+
}
16+
17+
mapping(address => RequestInfo) internal s_requests;
18+
19+
function emitSingleGeodeOpen(address user, uint256 id, uint256 value, uint8 status, uint256 requestId) external {
20+
emit TransferSingle(msg.sender, user, address(this), id, value);
21+
22+
uint256[] memory ids = new uint256[](1);
23+
ids[0] = id;
24+
uint256[] memory values = new uint256[](1);
25+
values[0] = value;
26+
27+
s_requests[user] = RequestInfo({
28+
user: user,
29+
requestId: requestId,
30+
status: status,
31+
randomNumber: 0,
32+
geodeTokenIds: ids,
33+
amountPerToken: values
34+
});
35+
}
36+
37+
function emitBatchGeodeOpen(address user, uint256[] calldata ids, uint256[] calldata values, uint8 status, uint256 requestId) external {
38+
emit TransferBatch(msg.sender, user, address(this), ids, values);
39+
40+
s_requests[user] = RequestInfo({
41+
user: user,
42+
requestId: requestId,
43+
status: status,
44+
randomNumber: 0,
45+
geodeTokenIds: ids,
46+
amountPerToken: values
47+
});
48+
}
49+
50+
function getRequestInfo(address user) external view returns (RequestInfo memory) {
51+
require(s_requests[user].user != address(0), "MockForgeVrfHistory: no request");
52+
return s_requests[user];
53+
}
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.1;
3+
4+
contract MockPortalVrfHistory {
5+
event OpenPortals(uint256[] _tokenIds);
6+
event PortalOpened(uint256 indexed tokenId);
7+
8+
function emitOpenPortals(uint256[] calldata tokenIds) external {
9+
emit OpenPortals(tokenIds);
10+
}
11+
12+
function emitPortalOpened(uint256 tokenId) external {
13+
emit PortalOpened(tokenId);
14+
}
15+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { ethers } from "ethers";
2+
3+
const GEODE_MIN = ethers.BigNumber.from("1000000002");
4+
const GEODE_MAX = ethers.BigNumber.from("1000000007");
5+
6+
export interface LegacyVrfAddresses {
7+
aavegotchiDiamond: string;
8+
forgeDiamond: string;
9+
}
10+
11+
export interface LegacyVrfPreflightSummary {
12+
latestBlock: number;
13+
pendingPortalCount: number;
14+
pendingPortalTokenIds: string[];
15+
pendingForgeCount: number;
16+
pendingForge: { user: string; requestId: string }[];
17+
readyToClaimForgeCount: number;
18+
readyToClaimForge: { user: string; requestId: string }[];
19+
}
20+
21+
async function findDeployBlock(
22+
provider: ethers.providers.Provider,
23+
address: string,
24+
latest: number
25+
) {
26+
let lo = 0;
27+
let hi = latest;
28+
29+
while (lo < hi) {
30+
const mid = Math.floor((lo + hi) / 2);
31+
const code = await provider.getCode(address, mid);
32+
if (code && code !== "0x") {
33+
hi = mid;
34+
} else {
35+
lo = mid + 1;
36+
}
37+
}
38+
39+
return lo;
40+
}
41+
42+
async function getLogsByChunks(
43+
provider: ethers.providers.Provider,
44+
filterBase: ethers.providers.Filter,
45+
fromBlock: number,
46+
toBlock: number,
47+
step = 1_000_000
48+
) {
49+
const logs = [];
50+
for (let start = fromBlock; start <= toBlock; start += step) {
51+
const end = Math.min(start + step - 1, toBlock);
52+
const chunk = await provider.getLogs({
53+
...filterBase,
54+
fromBlock: start,
55+
toBlock: end,
56+
});
57+
logs.push(...chunk);
58+
}
59+
return logs;
60+
}
61+
62+
export async function getLegacyVrfPreflightSummary(
63+
provider: ethers.providers.Provider,
64+
addresses: LegacyVrfAddresses
65+
): Promise<LegacyVrfPreflightSummary> {
66+
const latest = await provider.getBlockNumber();
67+
const aavegotchiDeployBlock = await findDeployBlock(
68+
provider,
69+
addresses.aavegotchiDiamond,
70+
latest
71+
);
72+
const forgeDeployBlock = await findDeployBlock(
73+
provider,
74+
addresses.forgeDiamond,
75+
latest
76+
);
77+
78+
const portalInterface = new ethers.utils.Interface([
79+
"event OpenPortals(uint256[] _tokenIds)",
80+
"event PortalOpened(uint256 indexed tokenId)",
81+
]);
82+
const openTopic = portalInterface.getEventTopic("OpenPortals");
83+
const openedTopic = portalInterface.getEventTopic("PortalOpened");
84+
85+
const openLogs = await getLogsByChunks(
86+
provider,
87+
{ address: addresses.aavegotchiDiamond, topics: [openTopic] },
88+
aavegotchiDeployBlock,
89+
latest
90+
);
91+
const openedLogs = await getLogsByChunks(
92+
provider,
93+
{ address: addresses.aavegotchiDiamond, topics: [openedTopic] },
94+
aavegotchiDeployBlock,
95+
latest
96+
);
97+
98+
const pendingPortals = new Set<string>();
99+
for (const log of openLogs) {
100+
const parsed = portalInterface.parseLog(log);
101+
for (const tokenId of parsed.args._tokenIds) {
102+
pendingPortals.add(tokenId.toString());
103+
}
104+
}
105+
for (const log of openedLogs) {
106+
const parsed = portalInterface.parseLog(log);
107+
pendingPortals.delete(parsed.args.tokenId.toString());
108+
}
109+
110+
const forgeInterface = new ethers.utils.Interface([
111+
"event TransferSingle(address indexed operator,address indexed from,address indexed to,uint256 id,uint256 value)",
112+
"event TransferBatch(address indexed operator,address indexed from,address indexed to,uint256[] ids,uint256[] values)",
113+
"function getRequestInfo(address user) view returns (tuple(address user,uint256 requestId,uint8 status,uint256 randomNumber,uint256[] geodeTokenIds,uint256[] amountPerToken))",
114+
]);
115+
const transferSingleTopic = forgeInterface.getEventTopic("TransferSingle");
116+
const transferBatchTopic = forgeInterface.getEventTopic("TransferBatch");
117+
const toTopic = ethers.utils.hexZeroPad(
118+
addresses.forgeDiamond.toLowerCase(),
119+
32
120+
);
121+
122+
const singleLogs = await getLogsByChunks(
123+
provider,
124+
{
125+
address: addresses.forgeDiamond,
126+
topics: [transferSingleTopic, null, null, toTopic],
127+
},
128+
forgeDeployBlock,
129+
latest
130+
);
131+
const batchLogs = await getLogsByChunks(
132+
provider,
133+
{
134+
address: addresses.forgeDiamond,
135+
topics: [transferBatchTopic, null, null, toTopic],
136+
},
137+
forgeDeployBlock,
138+
latest
139+
);
140+
141+
const forgeUsers = new Set<string>();
142+
for (const log of singleLogs) {
143+
const parsed = forgeInterface.parseLog(log);
144+
if (parsed.args.id.gte(GEODE_MIN) && parsed.args.id.lte(GEODE_MAX)) {
145+
forgeUsers.add(parsed.args.from.toLowerCase());
146+
}
147+
}
148+
for (const log of batchLogs) {
149+
const parsed = forgeInterface.parseLog(log);
150+
const hasGeode = parsed.args.ids.some(
151+
(id) => id.gte(GEODE_MIN) && id.lte(GEODE_MAX)
152+
);
153+
if (hasGeode) {
154+
forgeUsers.add(parsed.args.from.toLowerCase());
155+
}
156+
}
157+
158+
const forgeContract = new ethers.Contract(
159+
addresses.forgeDiamond,
160+
forgeInterface,
161+
provider
162+
);
163+
const pendingForge: { user: string; requestId: string }[] = [];
164+
const readyToClaimForge: { user: string; requestId: string }[] = [];
165+
for (const user of forgeUsers) {
166+
try {
167+
const info = await forgeContract.getRequestInfo(user);
168+
const status = Number(info.status);
169+
if (status === 0) {
170+
pendingForge.push({ user, requestId: info.requestId.toString() });
171+
} else if (status === 1) {
172+
readyToClaimForge.push({
173+
user,
174+
requestId: info.requestId.toString(),
175+
});
176+
}
177+
} catch (err) {}
178+
}
179+
180+
return {
181+
latestBlock: latest,
182+
pendingPortalCount: pendingPortals.size,
183+
pendingPortalTokenIds: Array.from(pendingPortals),
184+
pendingForgeCount: pendingForge.length,
185+
pendingForge,
186+
readyToClaimForgeCount: readyToClaimForge.length,
187+
readyToClaimForge,
188+
};
189+
}
190+
191+
export async function assertNoPendingLegacyVrfRequests(
192+
provider: ethers.providers.Provider,
193+
addresses: LegacyVrfAddresses
194+
) {
195+
const summary = await getLegacyVrfPreflightSummary(provider, addresses);
196+
197+
if (summary.pendingPortalCount > 0 || summary.pendingForgeCount > 0) {
198+
throw new Error(
199+
`Legacy PoP VRF requests are still pending. pendingPortals=${summary.pendingPortalCount} pendingForge=${summary.pendingForgeCount}`
200+
);
201+
}
202+
203+
return summary;
204+
}

scripts/upgrades/base/upgrade-chainlinkVrfDirectFunding.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
varsForNetwork,
77
} from "../../../helpers/constants";
88
import { diamondOwner } from "../../helperFunctions";
9+
import { assertNoPendingLegacyVrfRequests } from "./chainlinkVrfPreflight";
910
import {
1011
ChainlinkVrfDirectFundingAdapter,
1112
ForgeVRFFacet,
@@ -99,6 +100,18 @@ export async function upgradeChainlinkVrfDirectFunding() {
99100
console.log("callbackGasLimit:", vrfConfig.callbackGasLimit);
100101
console.log("requestConfirmations:", vrfConfig.requestConfirmations);
101102

103+
const preflight = await assertNoPendingLegacyVrfRequests(ethers.provider, {
104+
aavegotchiDiamond: c.aavegotchiDiamond,
105+
forgeDiamond: c.forgeDiamond,
106+
});
107+
console.log("preflight latestBlock:", preflight.latestBlock);
108+
console.log("preflight pendingPortalCount:", preflight.pendingPortalCount);
109+
console.log("preflight pendingForgeCount:", preflight.pendingForgeCount);
110+
console.log(
111+
"preflight readyToClaimForgeCount:",
112+
preflight.readyToClaimForgeCount
113+
);
114+
102115
const Adapter = await ethers.getContractFactory(
103116
"ChainlinkVrfDirectFundingAdapter",
104117
ownerSigner

test/chainlinkVrfPreflightTest.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { expect } from "chai";
2+
import { ethers } from "hardhat";
3+
4+
import {
5+
assertNoPendingLegacyVrfRequests,
6+
getLegacyVrfPreflightSummary,
7+
} from "../scripts/upgrades/base/chainlinkVrfPreflight";
8+
9+
describe("Chainlink VRF preflight", function () {
10+
it("reports zero pending requests when portals are fulfilled and forge requests are ready to claim", async function () {
11+
const Portal = await ethers.getContractFactory("MockPortalVrfHistory");
12+
const portal = await Portal.deploy();
13+
await portal.deployed();
14+
15+
const Forge = await ethers.getContractFactory("MockForgeVrfHistory");
16+
const forge = await Forge.deploy();
17+
await forge.deployed();
18+
19+
const [user] = await ethers.getSigners();
20+
await portal.emitOpenPortals([1, 2]);
21+
await portal.emitPortalOpened(1);
22+
await portal.emitPortalOpened(2);
23+
24+
await forge.emitBatchGeodeOpen(
25+
user.address,
26+
[1_000_000_002, 1_000_000_003],
27+
[1, 1],
28+
1,
29+
77
30+
);
31+
32+
const summary = await getLegacyVrfPreflightSummary(ethers.provider, {
33+
aavegotchiDiamond: portal.address,
34+
forgeDiamond: forge.address,
35+
});
36+
37+
expect(summary.pendingPortalCount).to.equal(0);
38+
expect(summary.pendingForgeCount).to.equal(0);
39+
expect(summary.readyToClaimForgeCount).to.equal(1);
40+
41+
await expect(
42+
assertNoPendingLegacyVrfRequests(ethers.provider, {
43+
aavegotchiDiamond: portal.address,
44+
forgeDiamond: forge.address,
45+
})
46+
).to.not.be.reverted;
47+
});
48+
49+
it("throws when any legacy portal or forge callback is still pending", async function () {
50+
const Portal = await ethers.getContractFactory("MockPortalVrfHistory");
51+
const portal = await Portal.deploy();
52+
await portal.deployed();
53+
54+
const Forge = await ethers.getContractFactory("MockForgeVrfHistory");
55+
const forge = await Forge.deploy();
56+
await forge.deployed();
57+
58+
const [user] = await ethers.getSigners();
59+
await portal.emitOpenPortals([1234]);
60+
await forge.emitSingleGeodeOpen(user.address, 1_000_000_002, 1, 0, 88);
61+
62+
await expect(
63+
assertNoPendingLegacyVrfRequests(ethers.provider, {
64+
aavegotchiDiamond: portal.address,
65+
forgeDiamond: forge.address,
66+
})
67+
).to.be.rejectedWith("Legacy PoP VRF requests are still pending");
68+
});
69+
});

0 commit comments

Comments
 (0)