diff --git a/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/icon.png b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/icon.png new file mode 100755 index 0000000..408d857 Binary files /dev/null and b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/icon.png differ diff --git a/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/FungibleToken.ts b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/FungibleToken.ts new file mode 100755 index 0000000..a21d850 --- /dev/null +++ b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/FungibleToken.ts @@ -0,0 +1,323 @@ +import { + AccountUpdate, + AccountUpdateForest, + assert, + Bool, + DeployArgs, + Field, + Int64, + method, + Permissions, + Provable, + PublicKey, + State, + state, + Struct, + TokenContract, + Types, + UInt64, + UInt8, + VerificationKey, +} from "o1js"; +import { FungibleTokenAdminBase, HttpzTokenAdmin } from "./HttpzTokenAdmin.js"; + +interface FungibleTokenDeployProps extends Exclude { + /** 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; + /** Setting this to `true` will allow changing the verification key later with a signature from the deployer. This will allow updating the token contract at a later stage, for instance to react to an update of the o1js library. + * Setting it to `false` will make changes to the contract impossible, unless there is a backward incompatible change to the protocol. (see https://docs.minaprotocol.com/zkapps/writing-a-zkapp/feature-overview/permissions#example-impossible-to-upgrade and https://minafoundation.github.io/mina-fungible-token/deploy.html) */ + allowUpdates: boolean; +} + +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 TokenContract { + @state(UInt8) + decimals = State(); + @state(PublicKey) + admin = State(); + @state(Bool) + paused = State(); + + // 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 = + HttpzTokenAdmin; + + 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: props.allowUpdates + ? Permissions.VerificationKey.proofDuringCurrentVersion() + : Permissions.VerificationKey.impossibleDuringCurrentVersion(), + setPermissions: Permissions.impossible(), + access: Permissions.proof(), + }); + } + + /** Update the verification key. + * This will only work when `allowUpdates` has been set to `true` during deployment. + */ + @method + async updateVerificationKey(vk: VerificationKey) { + const adminContract = await this.getAdminContract(); + const canChangeVerificationKey = + await adminContract.canChangeVerificationKey(vk); + canChangeVerificationKey.assertTrue( + FungibleTokenErrors.noPermissionToChangeAdmin + ); + 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 { + 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 { + 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 { + 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).neg(); + 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 TokenContract.MAX_ACCOUNT_UPDATES The current limit is 9. + */ + @method + async approveBase(updates: AccountUpdateForest): Promise { + 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.isPositive().assertFalse(FungibleTokenErrors.flashMinting); + }); + totalBalance.assertEquals( + Int64.zero, + FungibleTokenErrors.unbalancedTransaction + ); + } + + @method.returns(UInt64) + async getBalanceOf(address: PublicKey): Promise { + 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 { + let circulating = await this.getBalanceOf(this.address); + return circulating; + } + + @method.returns(UInt8) + async getDecimals(): Promise { + 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, +}) {} diff --git a/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/HttpzTokenAdmin.ts b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/HttpzTokenAdmin.ts new file mode 100755 index 0000000..9d4fa68 --- /dev/null +++ b/token/mina_mainnet/assets/y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE/source/HttpzTokenAdmin.ts @@ -0,0 +1,106 @@ +import { + AccountUpdate, + assert, + Bool, + method, + Provable, + PublicKey, + SmartContract, + State, + state, + UInt64, + Permissions, + VerificationKey, + DeployArgs, +} from "o1js"; +import { CIRCULATION_MAX } from "./constants.js"; + +export type FungibleTokenAdminBase = SmartContract & { + canMint(accountUpdate: AccountUpdate): Promise; + canChangeAdmin(admin: PublicKey): Promise; + canPause(): Promise; + canResume(): Promise; + canChangeVerificationKey(vk: VerificationKey): Promise; +}; + +export interface FungibleTokenAdminDeployProps + extends Exclude { + adminPublicKey: PublicKey; +} + +export class HttpzTokenAdmin + extends SmartContract + implements FungibleTokenAdminBase +{ + @state(PublicKey) + private adminPublicKey = State(); + + // Total mint amount + @state(UInt64) + private totalMintAmount = State(); + + async deploy(props: FungibleTokenAdminDeployProps) { + await super.deploy(props); + this.adminPublicKey.set(props.adminPublicKey); + this.totalMintAmount.set(UInt64.from(0)); + + this.account.permissions.set({ + ...Permissions.default(), + }); + } + + /** 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 + public async updateVerificationKey(vk: VerificationKey) { + this.account.verificationKey.set(vk); + } + + private async ensureAdminSignature() { + const admin = await Provable.witnessAsync(PublicKey, async () => { + let pk = await this.adminPublicKey.fetch(); + assert(pk !== undefined, "could not fetch admin public key"); + return pk; + }); + this.adminPublicKey.requireEquals(admin); + return AccountUpdate.createSigned(admin); + } + + // Validates if minting is allowed within circulation limit + @method.returns(Bool) + public async canMint(accountUpdate: AccountUpdate) { + await this.ensureAdminSignature(); + const totalMintAmount = this.totalMintAmount.getAndRequireEquals(); + const newTotalMintAmount = totalMintAmount.add( + accountUpdate.body.balanceChange.magnitude + ); + + this.totalMintAmount.set(newTotalMintAmount); + return newTotalMintAmount.lessThanOrEqual(UInt64.from(CIRCULATION_MAX)); + } + + @method.returns(Bool) + public async canChangeAdmin(_admin: PublicKey) { + await this.ensureAdminSignature(); + return Bool(true); + } + + @method.returns(Bool) + public async canPause(): Promise { + await this.ensureAdminSignature(); + return Bool(true); + } + + @method.returns(Bool) + public async canResume(): Promise { + await this.ensureAdminSignature(); + return Bool(true); + } + + @method.returns(Bool) + public async canChangeVerificationKey(_vk: VerificationKey): Promise { + await this.ensureAdminSignature(); + return Bool(true); + } +} diff --git a/token/mina_mainnet/token.json b/token/mina_mainnet/token.json index 585ffe7..d746996 100644 --- a/token/mina_mainnet/token.json +++ b/token/mina_mainnet/token.json @@ -8,5 +8,6 @@ "description": "Auro test token in Mainnet", "website":"https://www.aurowallet.com/", "fungibleTokenVersion": "1.1.0" - } -] \ No newline at end of file + }, + {"id":"y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE","address":"B62qpN6sE9Bg9vzYVfs4ZBajgnv2sobb8fy76wZPB5vWM27s9GgtUTA","name":"Httpz","symbol":"Httpz","decimal":"9","description":"Httpz Token in Mainnet","website":"https://claim.httpz.link/","fungibleTokenVersion":"1.1.0"} +]