UNPKG

@silvana-one/token

Version:

Silvana Fungible Token Library

226 lines 10.3 kB
import { __decorate, __metadata } from "tslib"; import { AccountUpdate, Bool, method, Permissions, Provable, PublicKey, State, state, TokenContract, UInt64, VerificationKey, Struct, Field, TokenId, } from "o1js"; import { Whitelist } from "@silvana-one/storage"; export class AdvancedAdminData extends Struct({ totalSupply: UInt64, requireAdminSignatureForMint: Bool, anyoneCanMint: Bool, }) { static new(params = {}) { const { totalSupply, requireAdminSignatureForMint, anyoneCanMint } = params; return new AdvancedAdminData({ totalSupply: UInt64.from(totalSupply ?? 0), requireAdminSignatureForMint: Bool(requireAdminSignatureForMint ?? false), anyoneCanMint: Bool(anyoneCanMint ?? false), }); } pack() { const totalSupplyBits = this.totalSupply.value.toBits(64); return Field.fromBits([ ...totalSupplyBits, this.requireAdminSignatureForMint, this.anyoneCanMint, ]); } static unpack(packed) { const bits = packed.toBits(64 + 1 + 1); const totalSupply = UInt64.Unsafe.fromField(Field.fromBits(bits.slice(0, 64))); const requireAdminSignatureForMint = bits[64]; const anyoneCanMint = bits[64 + 1]; return new AdvancedAdminData({ totalSupply, requireAdminSignatureForMint, anyoneCanMint, }); } } /** A contract that grants permissions for administrative actions on a token. * * We separate this out into a dedicated contract. That way, when issuing a token, a user can * specify their own rules for administrative actions, without changing the token contract itself. * * The advantage is that third party applications that only use the token in a non-privileged way * can integrate against the unchanged token contract. */ export class FungibleTokenAdvancedAdmin extends TokenContract { constructor() { super(...arguments); this.adminPublicKey = State(); this.tokenContract = State(); this.whitelist = State(); this.adminData = State(); this.events = { updateWhitelist: Whitelist }; } /** * Overrides the approveBase method to prevent transfers of tokens. * * @param forest - The account update forest. */ async approveBase(forest) { throw Error("Transfer not allowed"); } async deploy(props) { await super.deploy(props); this.adminPublicKey.set(props.adminPublicKey); this.tokenContract.set(props.tokenContract); this.adminData.set(new AdvancedAdminData({ totalSupply: props.totalSupply, requireAdminSignatureForMint: props.requireAdminSignatureForMint, anyoneCanMint: props.anyoneCanMint, }).pack()); this.whitelist.set(props.whitelist); this.account.permissions.set({ ...Permissions.default(), setVerificationKey: Permissions.VerificationKey.proofDuringCurrentVersion(), setPermissions: Permissions.impossible(), }); } /** 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. */ async updateVerificationKey(vk) { await this.ensureAdminSignature(); this.account.verificationKey.set(vk); } async ensureAdminSignature() { // We do not fetch the admin public key here to allow error handling during // the fetching of the admin public key that should be done before calling this method const admin = this.adminPublicKey.getAndRequireEquals(); const adminUpdate = AccountUpdate.createSigned(admin); adminUpdate.body.useFullCommitment = Bool(true); return adminUpdate; } async canMint(_accountUpdate) { // We use many conditional account updates here to allow other contracts to call this method // without hitting the account update limit const address = _accountUpdate.body.publicKey; const balanceChange = _accountUpdate.body.balanceChange; balanceChange.isPositive().assertTrue(); const amount = balanceChange.magnitude; const adminData = AdvancedAdminData.unpack(this.adminData.getAndRequireEquals()); amount.assertLessThanOrEqual(adminData.totalSupply); const tokenContract = this.tokenContract.getAndRequireEquals(); const tokenId = TokenId.derive(tokenContract); // it is NOT this.tokenId const adminTokenId = this.deriveTokenId(); // it is NOT this.tokenId // Does this guarantee that the call is from the token contract? // TODO: If not, consider adding a sync method to handle the case when // the contract will be called not from the token contract // and the totalSupply will run out of sync with the token contract _accountUpdate.body.tokenId.assertEquals(tokenId); // Create a conditional AccountUpdate to check total supply in case it is limited const maxAdditionalSupply = adminData.totalSupply.sub(amount); const tokenUpdate = AccountUpdate.createIf(adminData.totalSupply.equals(UInt64.MAXINT()).not(), this.address, adminTokenId); tokenUpdate.account.balance.requireBetween(UInt64.zero, maxAdditionalSupply); tokenUpdate.balance.addInPlace(amount); this.self.approve(tokenUpdate); const whitelist = this.whitelist.getAndRequireEquals(); const whitelistedAmount = await whitelist.getWhitelistedAmount(address); whitelistedAmount.isSome .or(adminData.anyoneCanMint) .assertTrue("Cannot mint to non-whitelisted address"); const maxMintAmount = Provable.if(adminData.anyoneCanMint, Provable.if(whitelistedAmount.isSome, whitelistedAmount.value, UInt64.MAXINT()), // blacklist whitelistedAmount.value); amount.assertLessThanOrEqual(maxMintAmount); // create a conditional account update to check if the tokens already have been minted // we will keep track of the total amount minted in the admin contract // It is the responsibility of Mina.transaction to fund the new account // We will not handle it here to save one account update const trackMintUpdate = AccountUpdate.createIf(whitelist.isSome(), // we do not track minting if the whitelist is empty address, adminTokenId); trackMintUpdate.account.balance.requireBetween(UInt64.zero, maxMintAmount.sub(amount)); trackMintUpdate.balance.addInPlace(amount); this.self.approve(trackMintUpdate); // This conditional account update will be created only if admin signature is required const adminSignatureUpdate = AccountUpdate.createIf(adminData.requireAdminSignatureForMint, this.adminPublicKey.getAndRequireEquals()); adminSignatureUpdate.requireSignature(); adminSignatureUpdate.body.useFullCommitment = Bool(true); // We return true as we already checked that the mint is allowed return Bool(true); } async canChangeAdmin(_admin) { await this.ensureAdminSignature(); return Bool(true); } async canPause() { await this.ensureAdminSignature(); return Bool(true); } async canResume() { await this.ensureAdminSignature(); return Bool(true); } async updateWhitelist(whitelist) { const admin = this.adminPublicKey.getAndRequireEquals(); const sender = this.sender.getUnconstrained(); const senderUpdate = AccountUpdate.createSigned(sender); senderUpdate.body.useFullCommitment = Bool(true); admin.assertEquals(sender); this.whitelist.set(whitelist); this.emitEvent("updateWhitelist", whitelist); } async canChangeVerificationKey(_vk) { await this.ensureAdminSignature(); return Bool(true); } } __decorate([ state(PublicKey), __metadata("design:type", Object) ], FungibleTokenAdvancedAdmin.prototype, "adminPublicKey", void 0); __decorate([ state(PublicKey), __metadata("design:type", Object) ], FungibleTokenAdvancedAdmin.prototype, "tokenContract", void 0); __decorate([ state(Whitelist), __metadata("design:type", Object) ], FungibleTokenAdvancedAdmin.prototype, "whitelist", void 0); __decorate([ state(Field), __metadata("design:type", Object) ], FungibleTokenAdvancedAdmin.prototype, "adminData", void 0); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey]), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "updateVerificationKey", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [AccountUpdate]), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "canMint", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [PublicKey]), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "canChangeAdmin", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "canPause", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "canResume", null); __decorate([ method, __metadata("design:type", Function), __metadata("design:paramtypes", [Whitelist]), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "updateWhitelist", null); __decorate([ method.returns(Bool), __metadata("design:type", Function), __metadata("design:paramtypes", [VerificationKey]), __metadata("design:returntype", Promise) ], FungibleTokenAdvancedAdmin.prototype, "canChangeVerificationKey", null); //# sourceMappingURL=FungibleTokenAdvancedAdmin.js.map