diff --git a/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/icon.png b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/icon.png new file mode 100644 index 0000000..fd32d02 Binary files /dev/null and b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/icon.png differ diff --git a/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleToken.ts b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleToken.ts new file mode 100644 index 0000000..f7bb3da --- /dev/null +++ b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleToken.ts @@ -0,0 +1,288 @@ +import { + AccountUpdate, + AccountUpdateForest, + assert, + Bool, + DeployArgs, + Field, + Int64, + method, + Permissions, + Provable, + PublicKey, + State, + state, + Struct, + TokenContract, + Types, + UInt64, + UInt8, + VerificationKey, +} from "o1js" +import { FungibleTokenAdmin, FungibleTokenAdminBase } from "./FungibleTokenAdmin.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 = 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: 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/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleTokenAdmin.ts b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleTokenAdmin.ts new file mode 100644 index 0000000..1e47938 --- /dev/null +++ b/token/mina_mainnet/assets/y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR/source/FungibleTokenAdmin.ts @@ -0,0 +1,98 @@ +import { + AccountUpdate, + assert, + Bool, + DeployArgs, + method, + Permissions, + Provable, + PublicKey, + SmartContract, + State, + state, + VerificationKey, +} from "o1js" + +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 +} + +/** 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 FungibleTokenAdmin extends SmartContract implements FungibleTokenAdminBase { + @state(PublicKey) + private adminPublicKey = State() + + async deploy(props: FungibleTokenAdminDeployProps) { + await super.deploy(props) + this.adminPublicKey.set(props.adminPublicKey) + this.account.permissions.set({ + ...Permissions.default(), + setVerificationKey: Permissions.VerificationKey.proofOrSignature(), + }) + } + + /** 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) + } + + 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) + } + + @method.returns(Bool) + public async canMint(_accountUpdate: AccountUpdate) { + await this.ensureAdminSignature() + return Bool(true) + } + + @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 d746996..6244134 100644 --- a/token/mina_mainnet/token.json +++ b/token/mina_mainnet/token.json @@ -9,5 +9,15 @@ "website":"https://www.aurowallet.com/", "fungibleTokenVersion": "1.1.0" }, - {"id":"y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE","address":"B62qpN6sE9Bg9vzYVfs4ZBajgnv2sobb8fy76wZPB5vWM27s9GgtUTA","name":"Httpz","symbol":"Httpz","decimal":"9","description":"Httpz Token in Mainnet","website":"https://claim.httpz.link/","fungibleTokenVersion":"1.1.0"} + {"id":"y96qmT865fCMGGHdKAQ448uUwqs7dEfqnGBGVrv3tiRKTC2hxE","address":"B62qpN6sE9Bg9vzYVfs4ZBajgnv2sobb8fy76wZPB5vWM27s9GgtUTA","name":"Httpz","symbol":"Httpz","decimal":"9","description":"Httpz Token in Mainnet","website":"https://claim.httpz.link/","fungibleTokenVersion":"1.1.0"}, + { + "id": "y9udmffu2J1Wrgnwwp4WeYtQcHT1ApQmSDJ1xAwmUKeMooJhDR", + "address": "B62qoNVDNgu3TAjPWE8DXD44Vgz69CWVSDYgZXFz6kFHTy3Pdy1zYee", + "name": "Wrap ETH", + "symbol": "WETH", + "decimal": "9", + "description": "Wrap ETH token in Mainnet", + "website":"https://www.minabridge.io", + "fungibleTokenVersion": "1.1.0" + } ]