forked from MinaFoundation/mina-fungible-token
-
Notifications
You must be signed in to change notification settings - Fork 0
/
FungibleToken.ts
280 lines (249 loc) · 9.85 KB
/
FungibleToken.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import {
AccountUpdate,
AccountUpdateForest,
assert,
Bool,
DeployArgs,
Field,
Int64,
method,
Permissions,
Provable,
PublicKey,
State,
state,
Struct,
TokenContractV2,
Types,
UInt64,
UInt8,
VerificationKey,
} from "o1js"
import { FungibleTokenAdmin, FungibleTokenAdminBase } from "./FungibleTokenAdmin.js"
interface FungibleTokenDeployProps extends Exclude<DeployArgs, undefined> {
/** The token symbol. */
symbol: string
/** A source code reference, which is placed within the `zkappUri` of the contract account.
* Typically a link to a file on github. */
src: string
}
export const FungibleTokenErrors = {
noAdminKey: "could not fetch admin contract key",
noPermissionToChangeAdmin: "Not allowed to change admin contract",
tokenPaused: "Token is currently paused",
noPermissionToMint: "Not allowed to mint tokens",
noPermissionToPause: "Not allowed to pause token",
noPermissionToResume: "Not allowed to resume token",
noTransferFromCirculation: "Can't transfer to/from the circulation account",
noPermissionChangeAllowed: "Can't change permissions for access or receive on token accounts",
flashMinting:
"Flash-minting or unbalanced transaction detected. Please make sure that your transaction is balanced, and that your `AccountUpdate`s are ordered properly, so that tokens are not received before they are sent.",
unbalancedTransaction: "Transaction is unbalanced",
}
export class FungibleToken extends TokenContractV2 {
@state(UInt8)
decimals = State<UInt8>()
@state(PublicKey)
admin = State<PublicKey>()
@state(Bool)
paused = State<Bool>()
// This defines the type of the contract that is used to control access to administrative actions.
// If you want to have a custom contract, overwrite this by setting FungibleToken.AdminContract to
// your own implementation of FungibleTokenAdminBase.
static AdminContract: new(...args: any) => FungibleTokenAdminBase = FungibleTokenAdmin
readonly events = {
SetAdmin: SetAdminEvent,
Pause: PauseEvent,
Mint: MintEvent,
Burn: BurnEvent,
BalanceChange: BalanceChangeEvent,
}
async deploy(props: FungibleTokenDeployProps) {
await super.deploy(props)
this.paused.set(Bool(true))
this.account.zkappUri.set(props.src)
this.account.tokenSymbol.set(props.symbol)
this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
setPermissions: Permissions.impossible(),
access: Permissions.proof(),
})
}
/** Update the verification key.
* Note that because we have set the permissions for setting the verification key to `impossibleDuringCurrentVersion()`, this will only be possible in case of a protocol update that requires an update.
*/
@method
async updateVerificationKey(vk: VerificationKey) {
this.account.verificationKey.set(vk)
}
/** Initializes the account for tracking total circulation.
* @argument {PublicKey} admin - public key where the admin contract is deployed
* @argument {UInt8} decimals - number of decimals for the token
* @argument {Bool} startPaused - if set to `Bool(true), the contract will start in a mode where token minting and transfers are paused. This should be used for non-atomic deployments
*/
@method
async initialize(
admin: PublicKey,
decimals: UInt8,
startPaused: Bool,
) {
this.account.provedState.requireEquals(Bool(false))
this.admin.set(admin)
this.decimals.set(decimals)
this.paused.set(Bool(false))
this.paused.set(startPaused)
const accountUpdate = AccountUpdate.createSigned(this.address, this.deriveTokenId())
let permissions = Permissions.default()
// This is necessary in order to allow token holders to burn.
permissions.send = Permissions.none()
permissions.setPermissions = Permissions.impossible()
accountUpdate.account.permissions.set(permissions)
}
public async getAdminContract(): Promise<FungibleTokenAdminBase> {
const admin = await Provable.witnessAsync(PublicKey, async () => {
let pk = await this.admin.fetch()
assert(pk !== undefined, FungibleTokenErrors.noAdminKey)
return pk
})
this.admin.requireEquals(admin)
return (new FungibleToken.AdminContract(admin))
}
@method
async setAdmin(admin: PublicKey) {
const adminContract = await this.getAdminContract()
const canChangeAdmin = await adminContract.canChangeAdmin(admin)
canChangeAdmin.assertTrue(FungibleTokenErrors.noPermissionToChangeAdmin)
this.admin.set(admin)
this.emitEvent("SetAdmin", new SetAdminEvent({ adminKey: admin }))
}
@method.returns(AccountUpdate)
async mint(recipient: PublicKey, amount: UInt64): Promise<AccountUpdate> {
this.paused.getAndRequireEquals().assertFalse(FungibleTokenErrors.tokenPaused)
const accountUpdate = this.internal.mint({ address: recipient, amount })
const adminContract = await this.getAdminContract()
const canMint = await adminContract.canMint(accountUpdate)
canMint.assertTrue(FungibleTokenErrors.noPermissionToMint)
recipient.equals(this.address).assertFalse(
FungibleTokenErrors.noTransferFromCirculation,
)
this.approve(accountUpdate)
this.emitEvent("Mint", new MintEvent({ recipient, amount }))
const circulationUpdate = AccountUpdate.create(this.address, this.deriveTokenId())
circulationUpdate.balanceChange = Int64.fromUnsigned(amount)
return accountUpdate
}
@method.returns(AccountUpdate)
async burn(from: PublicKey, amount: UInt64): Promise<AccountUpdate> {
this.paused.getAndRequireEquals().assertFalse(FungibleTokenErrors.tokenPaused)
const accountUpdate = this.internal.burn({ address: from, amount })
const circulationUpdate = AccountUpdate.create(this.address, this.deriveTokenId())
from.equals(this.address).assertFalse(
FungibleTokenErrors.noTransferFromCirculation,
)
circulationUpdate.balanceChange = Int64.fromUnsigned(amount).negV2()
this.emitEvent("Burn", new BurnEvent({ from, amount }))
return accountUpdate
}
@method
async pause() {
const adminContract = await this.getAdminContract()
const canPause = await adminContract.canPause()
canPause.assertTrue(FungibleTokenErrors.noPermissionToPause)
this.paused.set(Bool(true))
this.emitEvent("Pause", new PauseEvent({ isPaused: Bool(true) }))
}
@method
async resume() {
const adminContract = await this.getAdminContract()
const canResume = await adminContract.canResume()
canResume.assertTrue(FungibleTokenErrors.noPermissionToResume)
this.paused.set(Bool(false))
this.emitEvent("Pause", new PauseEvent({ isPaused: Bool(false) }))
}
@method
async transfer(from: PublicKey, to: PublicKey, amount: UInt64) {
this.paused.getAndRequireEquals().assertFalse(FungibleTokenErrors.tokenPaused)
from.equals(this.address).assertFalse(
FungibleTokenErrors.noTransferFromCirculation,
)
to.equals(this.address).assertFalse(
FungibleTokenErrors.noTransferFromCirculation,
)
this.internal.send({ from, to, amount })
}
private checkPermissionsUpdate(update: AccountUpdate) {
let permissions = update.update.permissions
let { access, receive } = permissions.value
let accessIsNone = Provable.equal(Types.AuthRequired, access, Permissions.none())
let receiveIsNone = Provable.equal(Types.AuthRequired, receive, Permissions.none())
let updateAllowed = accessIsNone.and(receiveIsNone)
assert(
updateAllowed.or(permissions.isSome.not()),
FungibleTokenErrors.noPermissionChangeAllowed,
)
}
/** Approve `AccountUpdate`s that have been created outside of the token contract.
*
* @argument {AccountUpdateForest} updates - The `AccountUpdate`s to approve. Note that the forest size is limited by the base token contract, @see TokenContractV2.MAX_ACCOUNT_UPDATES The current limit is 9.
*/
@method
async approveBase(updates: AccountUpdateForest): Promise<void> {
this.paused.getAndRequireEquals().assertFalse(FungibleTokenErrors.tokenPaused)
let totalBalance = Int64.from(0)
this.forEachUpdate(updates, (update, usesToken) => {
// Make sure that the account permissions are not changed
this.checkPermissionsUpdate(update)
this.emitEventIf(
usesToken,
"BalanceChange",
new BalanceChangeEvent({ address: update.publicKey, amount: update.balanceChange }),
)
// Don't allow transfers to/from the account that's tracking circulation
update.publicKey.equals(this.address).and(usesToken).assertFalse(
FungibleTokenErrors.noTransferFromCirculation,
)
totalBalance = Provable.if(usesToken, totalBalance.add(update.balanceChange), totalBalance)
totalBalance.isPositiveV2().assertFalse(
FungibleTokenErrors.flashMinting,
)
})
totalBalance.assertEquals(Int64.zero, FungibleTokenErrors.unbalancedTransaction)
}
@method.returns(UInt64)
async getBalanceOf(address: PublicKey): Promise<UInt64> {
const account = AccountUpdate.create(address, this.deriveTokenId()).account
const balance = account.balance.get()
account.balance.requireEquals(balance)
return balance
}
/** Reports the current circulating supply
* This does take into account currently unreduced actions.
*/
async getCirculating(): Promise<UInt64> {
let circulating = await this.getBalanceOf(this.address)
return circulating
}
@method.returns(UInt8)
async getDecimals(): Promise<UInt8> {
return this.decimals.getAndRequireEquals()
}
}
export class SetAdminEvent extends Struct({
adminKey: PublicKey,
}) {}
export class PauseEvent extends Struct({
isPaused: Bool,
}) {}
export class MintEvent extends Struct({
recipient: PublicKey,
amount: UInt64,
}) {}
export class BurnEvent extends Struct({
from: PublicKey,
amount: UInt64,
}) {}
export class BalanceChangeEvent extends Struct({
address: PublicKey,
amount: Int64,
}) {}