Skip to content

Commit 88fa10d

Browse files
authored
Merge pull request #155 from oasisprotocol/csillag/minime-support
Add support for MiniMe tokens
2 parents 18618df + 4ef7b97 commit 88fa10d

32 files changed

Lines changed: 3109 additions & 125 deletions

contracts/src/chains.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ChainDefinition {
2323
}
2424

2525
const INFURA_API_KEY = 'e9b08cc1b55b430494f6cf5259b2a560';
26+
const ALCHEMY_API_KEY = '9d1-4QZ0JTIGOuhZM4VJ6_vy0Z2YAuA1';
2627

2728
// NOTE: many chains are defined in https://github.com/thirdweb-dev/js/tree/main/legacy_packages/chains/chains
2829

@@ -32,9 +33,9 @@ export const chain_info: Record<number, ChainDefinition> = {
3233
chain: 'ETH',
3334
icon: 'ethereum',
3435
rpcUrls: [
35-
//`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`
36+
`https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
3637
//`https://1.rpc.thirdweb.com/${THIRDWEB_API_KEY}`,
37-
`https://mainnet.infura.io/v3/${INFURA_API_KEY}`,
38+
// `https://mainnet.infura.io/v3/${INFURA_API_KEY}`,
3839
],
3940
features: [{ name: 'EIP155' }, { name: 'EIP1559' }],
4041
hardfork: 'cancun',

contracts/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export type { DefaultReturnType } from "./contracts/common"
66
export * from './xchain.js';
77
export * from './types.js';
88
export * from './chains.js';
9-
export * from './eip712.js';
9+
export * from './eip712.js';
10+
export * from "./minime.js";

contracts/src/minime.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import {
2+
Contract,
3+
decodeRlp,
4+
encodeRlp,
5+
ethers, formatEther,
6+
hexlify,
7+
JsonRpcProvider,
8+
keccak256,
9+
solidityPackedKeccak256,
10+
toBeHex, toBigInt,
11+
toQuantity,
12+
zeroPadValue,
13+
} from 'ethers';
14+
import { RLP } from '@ethereumjs/rlp';
15+
import { GetProofResponse, TokenInfo } from './types';
16+
17+
/**
18+
* We need to support the MiniMeToken, because of LIDO.
19+
* See the token contract here:
20+
* https://github.com/aragon/minime/blob/master/contracts/MiniMeToken.sol#L293-L312
21+
*/
22+
const MiniMeTokenAbi = [
23+
'function name() public view returns (string)',
24+
'function symbol() public view returns (string)',
25+
'function decimals() public view returns (uint8)',
26+
'function totalSupply() public view returns (uint256)',
27+
`function balanceOfAt(address _owner, uint _blockNumber) public constant returns (uint)`
28+
];
29+
30+
const testMiniMeOwner = '0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c'
31+
const testMiniMeBlock = 21672028
32+
33+
export async function miniMeTokenDetailsFromProvider(
34+
addr: string,
35+
provider: JsonRpcProvider,
36+
): Promise<TokenInfo> {
37+
const c = new Contract(addr, MiniMeTokenAbi, provider);
38+
const network = await provider.getNetwork();
39+
// Test the presence of this specific function
40+
await c.balanceOfAt(testMiniMeOwner, testMiniMeBlock)
41+
return {
42+
addr: addr,
43+
chainId: network.chainId,
44+
name: await c.name(),
45+
symbol: await c.symbol(),
46+
decimals: await c.decimals(),
47+
totalSupply: await c.totalSupply(),
48+
type: 'MiniMe',
49+
};
50+
}
51+
52+
export async function getMiniMeStorageSlot(
53+
provider: JsonRpcProvider,
54+
account: string,
55+
holder: string,
56+
_isStillFresh: () => boolean = () => true,
57+
progressCallback?: (progress: string) => void | undefined,
58+
): Promise<{
59+
index: number;
60+
balance: bigint;
61+
balanceDecimal?: string;
62+
} | null> {
63+
if (progressCallback) progressCallback("Getting latest block...");
64+
const block = await provider.getBlock('latest')
65+
if (!block) throw new Error("Can't get latest block")
66+
if (progressCallback) progressCallback("Checking balance...");
67+
const contract = new Contract(account, MiniMeTokenAbi, provider);
68+
const balance = await contract.balanceOfAt(holder, block.number)
69+
return {index: 8, balance, balanceDecimal: formatEther(balance)}
70+
}
71+
72+
/// Retrieve RLP encoded block header
73+
export async function getMiniMeBlockHeaderRLP(provider: JsonRpcProvider, wantedBlock: { number?: number; hash?: string}) {
74+
75+
if (!wantedBlock.number && !wantedBlock.hash) throw new Error("Please supply either block number or block hash!")
76+
77+
// Get block data
78+
const block = wantedBlock.hash
79+
? await provider.send('eth_getBlockByHash', [wantedBlock.hash, false])
80+
: await provider.send('eth_getBlockByNumber', [toBeHex(wantedBlock.number!), false])
81+
82+
if (!block) {
83+
throw new Error(`Block ${JSON.stringify(wantedBlock)} not found`)
84+
}
85+
86+
const headerArray = [
87+
block.parentHash,
88+
block.sha3Uncles,
89+
block.miner,
90+
block.stateRoot,
91+
block.transactionsRoot,
92+
block.receiptsRoot,
93+
block.logsBloom,
94+
ethers.toBeHex(block.difficulty),
95+
ethers.toBeHex(block.number),
96+
ethers.toBeHex(block.gasLimit),
97+
ethers.toBeHex(block.gasUsed),
98+
ethers.toBeHex(block.timestamp),
99+
block.extraData,
100+
block.mixHash,
101+
block.nonce,
102+
ethers.toBeHex(block.baseFeePerGas || 0),
103+
// Cancun-specific fields
104+
ethers.toBeHex(block.withdrawalsRoot || '0x'),
105+
ethers.toBeHex(block.blobGasUsed || 0),
106+
ethers.toBeHex(block.excessBlobGas || 0),
107+
ethers.toBeHex(block.parentBeaconBlockRoot || '0x')
108+
];
109+
110+
return hexlify(RLP.encode(headerArray));
111+
}
112+
113+
export async function fetchMiniMeAccountProof(
114+
provider: JsonRpcProvider,
115+
wantedBlock: { number?: number; hash?: string },
116+
address: string,
117+
) {
118+
if (!wantedBlock.number && !wantedBlock.hash) throw new Error("Please supply either block number or block hash!")
119+
const response = (await provider.send('eth_getProof', [
120+
address,
121+
[],
122+
wantedBlock.hash ?? toQuantity(wantedBlock.number!)
123+
])) as GetProofResponse;
124+
125+
return encodeRlp(response.accountProof.map(decodeRlp));
126+
}
127+
128+
export async function getMiniMeAccountBalance(
129+
provider: JsonRpcProvider,
130+
contractAddress: string,
131+
accountAddress: string,
132+
slotNumber: number,
133+
wantedBlock: { number?: number; hash?: string },
134+
withVoteData?: boolean
135+
): Promise<{ balance: bigint, balanceString?: string, voteData?: string }> {
136+
if (!wantedBlock.number && !wantedBlock.hash) throw new Error("Please supply either block number or block hash!")
137+
const baseSlot = solidityPackedKeccak256(
138+
['bytes', 'uint256'],
139+
[zeroPadValue(accountAddress, 32), slotNumber]
140+
);
141+
142+
const blockNumber = wantedBlock.number ?? (await provider.getBlock(wantedBlock.hash!))?.number
143+
if (!blockNumber) {
144+
console.log("Block not found", wantedBlock.hash);
145+
return { balance: 0n }
146+
}
147+
148+
const lengthValue = await provider.send('eth_getStorageAt', [
149+
contractAddress,
150+
baseSlot,
151+
ethers.toQuantity(blockNumber)
152+
]);
153+
154+
if (toBigInt(lengthValue) === 0n) {
155+
// No checkpoints, no balance
156+
return { balance: 0n };
157+
}
158+
159+
const checkpointValue = await provider.send('eth_getStorageAt', [
160+
contractAddress,
161+
keccak256(baseSlot),
162+
toQuantity(blockNumber)
163+
]);
164+
const balance = toBigInt(checkpointValue) >> 128n;
165+
const balanceString = formatEther(balance)
166+
if (!withVoteData) return { balance, balanceString }
167+
168+
// Generate storage proofs
169+
const checkpointSlot = keccak256(baseSlot);
170+
const storageKeys = [baseSlot, checkpointSlot];
171+
172+
const proof = await provider.send('eth_getProof', [
173+
contractAddress,
174+
storageKeys,
175+
ethers.toQuantity(blockNumber)
176+
]);
177+
178+
// return encodeRlp(response.accountProof.map(decodeRlp));
179+
const encodedProofs = proof.storageProof.map((p: any) =>
180+
hexlify(encodeRlp(p.proof.map(decodeRlp)))
181+
);
182+
183+
// Encode vote data
184+
const voteData = ethers.AbiCoder.defaultAbiCoder().encode(['bytes[]'], [encodedProofs]);
185+
186+
return { balance, balanceString, voteData }
187+
}

contracts/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ export type GetProofResponse = {
4646
storageProof: StorageProof[];
4747
};
4848

49-
export type TokenType = 'ERC-20';
49+
export type TokenType = 'ERC-20' | 'MiniMe';
5050
export type NftType = 'ERC-721' | 'ERC-1155';
5151
export type ContractType = TokenType | NftType;
52-
export const isToken = (type: ContractType): boolean => type === 'ERC-20';
52+
export const isToken = (type: ContractType): boolean => type === 'ERC-20' || type === 'MiniMe';
5353

5454
export type TokenInfo = {
5555
chainId: bigint;

frontend/.env.production

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ VITE_CONTRACT_GASLESSVOTING=0x7D208022dE89dC1B27ab556224c4EdF9C2F17134
2828
VITE_CONTRACT_POLLMANAGER_TX=0x2cbdf7a765aa1bda87ef110118b2a8c311af8be52189c1f6f9f391103511bdef
2929
VITE_CONTRACT_POLLMANAGER=0x2aBAb31c2b0B50D243668b32e2c7E227222362D0
3030
VITE_CONTRACT_POLLMANAGER_ACL=0x0fa9a7FAf9bBb8E1Cbfb371E475C37c58B445596
31+
VITE_CONTRACT_MINIME_STORAGE_ORACLE_TX=0xe725930018ce2ecf610f9a94e335b6d72be09ac932799169fb93bbd37c243605
32+
VITE_CONTRACT_MINIME_STORAGE_ORACLE=0xc2b03C07d22A64e4D3729b45b6932Ad7626fED44
33+
VITE_CONTRACT_ACL_MINIME_STORAGE_TX=0x0539c8617106a4be2bd35b044a9c7731c33913d16f4a2e0968f218442e80a048
34+
VITE_CONTRACT_ACL_MINIME_STORAGE=0xE2c4a285025abe616c907d5e5EE98166042DdD64

frontend/.env.staging

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@ VITE_CONTRACT_TESTTOKEN=0x09aD7F70B3CFa12cFF1084d004feA41B9a9e80A1
3030
VITE_CONTRACT_LIB_CALLDATAENCRYPTION_TX=0x1e40062f4e4298a793abce9c22e13b9e16e859053d6e8e64a73342a86d53edf4
3131
VITE_CONTRACT_LIB_CALLDATAENCRYPTION=0xB22c17AC88FD36820eC71f828b0b6Ae372d0FBB5
3232
VITE_TUTORIAL_URL=https://www.youtube.com/live/1f_0h8qeJ-w?si=9ViH8ENERuUe6Rx3&t=1441
33+
VITE_CONTRACT_ACL_MINIME_STORAGE_TX=0x9390ed2205313c01339a96641788836c2860e722a0fc90b216ff838160295cf3
34+
VITE_CONTRACT_ACL_MINIME_STORAGE=0x4773D1BD3c50C77E14972A0b90BfcFbF8cBe91f6
35+
VITE_CONTRACT_MINIME_STORAGE_ORACLE=0x7A882cbEE658Bce5B98a701A47388e99a48CE99d
36+
VITE_CONTRACT_ACL_MINIME_STORAGE=0x99Cb8D3614Cc7D7945996071Aa5dFA73039eaf40

frontend/src/components/ACLs/allowAll.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { denyWithReason, useOneOfField } from '../InputFields'
88

99
export const allowAll = defineACL({
1010
value: 'acl_allowAll',
11-
address: VITE_CONTRACT_ACL_ALLOWALL,
1211
label: 'Everybody',
1312
costEstimation: 0.1,
1413
useConfiguration: active => {
@@ -34,6 +33,7 @@ export const allowAll = defineACL({
3433
},
3534

3635
getAclOptions: () => ({
36+
aclAddress: VITE_CONTRACT_ACL_ALLOWALL,
3737
data: '0x', // Empty bytes is passed
3838
options: { allowAll: true },
3939
flags: 0n,

frontend/src/components/ACLs/allowList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const splitAddresses = (addressSoup: string): string[] =>
1818

1919
export const allowList = defineACL({
2020
value: 'acl_allowList',
21-
address: VITE_CONTRACT_ACL_VOTERALLOWLIST,
2221
label: 'Address Whitelist',
2322
costEstimation: 0.1,
2423
description: 'You can specify a list of addresses that are allowed to vote.',
@@ -78,6 +77,7 @@ export const allowList = defineACL({
7877
getAclOptions: props => {
7978
if (!props.addresses) throw new Error('Internal errors: parameter mismatch, addresses missing.')
8079
return {
80+
aclAddress: VITE_CONTRACT_ACL_VOTERALLOWLIST,
8181
data: abiEncode(['address[]'], [props.addresses]),
8282
options: { allowList: true },
8383
flags: 0n,

frontend/src/components/ACLs/common.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,18 @@ export type ACL<Name, ConfigInputValues, Options extends AclOptions> = Choice<Na
2626
*/
2727
useConfiguration: (selected: boolean) => { fields: FieldConfiguration; values: ConfigInputValues }
2828

29-
/**
30-
* The address of the ACL contract to use
31-
*/
32-
address: string
33-
3429
/**
3530
* Compose the ACL options when creating a poll
3631
*/
3732
getAclOptions:
3833
| ((
3934
config: ConfigInputValues,
4035
context?: ExecutionContext,
41-
) => { data: string; options: Options; flags: bigint })
36+
) => { aclAddress: string; data: string; options: Options; flags: bigint })
4237
| ((
4338
config: ConfigInputValues,
4439
context?: ExecutionContext,
45-
) => Promise<{ data: string; options: Options; flags: bigint }>)
40+
) => Promise<{ aclAddress: string; data: string; options: Options; flags: bigint }>)
4641

4742
/**
4843
* Attempt to recognize if this ACL is managing a given poll, based on ACL options

frontend/src/components/ACLs/tokenHolder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { getLink } from '../../utils/markdown.utils'
1414

1515
export const tokenHolder = defineACL({
1616
value: 'acl_tokenHolder',
17-
address: VITE_CONTRACT_ACL_TOKENHOLDER,
1817
costEstimation: 0.2,
1918
label: `Active Token or NFT balance on ${configuredNetworkName}`,
2019
description:
@@ -101,6 +100,7 @@ export const tokenHolder = defineACL({
101100
getAclOptions: props => {
102101
if (!props.tokenAddress) throw new Error('Internal errors: parameter mismatch, addresses missing.')
103102
return {
103+
aclAddress: VITE_CONTRACT_ACL_TOKENHOLDER,
104104
data: abiEncode(['address'], [props.tokenAddress]),
105105
options: { token: props.tokenAddress },
106106
flags: props.flags,

0 commit comments

Comments
 (0)