o1js
Version:
TypeScript framework for zk-SNARKs and zkApps
199 lines (173 loc) • 6.82 kB
text/typescript
import { Bool } from '../../provable/wrapped.js';
import { UInt64, Int64 } from '../../provable/int.js';
import { Provable } from '../../provable/provable.js';
import { PublicKey } from '../../provable/crypto/signature.js';
import {
AccountUpdate,
AccountUpdateForest,
AccountUpdateTree,
Permissions,
TokenId,
} from '../account-update.js';
import { DeployArgs, SmartContract } from '../zkapp.js';
import { TokenAccountUpdateIterator } from './forest-iterator.js';
import { tokenMethods } from './token-methods.js';
export { TokenContract, TokenContractV2 };
/**
* Base token contract which
* - implements the `Approvable` API, with the `approveBase()` method left to be defined by subclasses
* - implements the `Transferable` API as a wrapper around the `Approvable` API
*/
abstract class TokenContract extends SmartContract {
// change default permissions - important that token contracts use an access permission
/** The maximum number of account updates using the token in a single
* transaction that this contract supports. */
static MAX_ACCOUNT_UPDATES = 20;
/**
* Deploys a {@link TokenContract}.
*
* In addition to base smart contract deployment, this adds two steps:
* - set the `access` permission to `proofOrSignature()`, to prevent against unauthorized token operations
* - not doing this would imply that anyone can bypass token contract authorization and simply mint themselves tokens
* - require the zkapp account to be new, using the `isNew` precondition.
* this guarantees that the access permission is set from the very start of the existence of this account.
* creating the zkapp account before deployment would otherwise be a security vulnerability that is too easy to introduce.
*
* Note that because of the `isNew` precondition, the zkapp account must not be created prior to calling `deploy()`.
*
* If the contract needs to be re-deployed, you can switch off this behaviour by overriding the `isNew` precondition:
* ```ts
* async deploy() {
* await super.deploy();
* // DON'T DO THIS ON THE INITIAL DEPLOYMENT!
* this.account.isNew.requireNothing();
* }
* ```
*/
async deploy(args?: DeployArgs) {
await super.deploy(args);
// set access permission, to prevent unauthorized token operations
this.account.permissions.set({
...Permissions.default(),
access: Permissions.proofOrSignature(),
});
// assert that this account is new, to ensure unauthorized token operations
// are not possible before this contract is deployed
// see https://github.com/o1-labs/o1js/issues/1439 for details
this.account.isNew.requireEquals(Bool(true));
}
/**
* Returns the `tokenId` of the token managed by this contract.
*/
deriveTokenId() {
return TokenId.derive(this.address, this.tokenId);
}
/**
* Helper methods to use from within a token contract.
*/
get internal() {
return tokenMethods(this.self);
}
// APPROVABLE API has to be specified by subclasses,
// but the hard part is `forEachUpdate()`
abstract approveBase(forest: AccountUpdateForest): Promise<void>;
/**
* Iterate through the account updates in `updates` and apply `callback` to each.
*
* This method is provable and is suitable as a base for implementing `approveUpdates()`.
*/
forEachUpdate(
updates: AccountUpdateForest,
callback: (update: AccountUpdate, usesToken: Bool) => void
) {
let iterator = TokenAccountUpdateIterator.create(
updates,
this.deriveTokenId()
);
// iterate through the forest and apply user-defined logic
for (let i = 0; i < (this.constructor as typeof TokenContract).MAX_ACCOUNT_UPDATES; i++) {
let { accountUpdate, usesThisToken } = iterator.next();
callback(accountUpdate, usesThisToken);
}
// prove that we checked all updates
iterator.assertFinished(
`Number of account updates to approve exceed ` +
`the supported limit of ${(this.constructor as typeof TokenContract).MAX_ACCOUNT_UPDATES}.\n`
);
// skip hashing our child account updates in the method wrapper
// since we just did that in the loop above
this.approve(updates);
}
/**
* Use `forEachUpdate()` to prove that the total balance change of child account updates is zero.
*
* This is provided out of the box as it is both a good example, and probably the most common implementation, of `approveBase()`.
*/
checkZeroBalanceChange(updates: AccountUpdateForest) {
let totalBalanceChange = Int64.zero;
this.forEachUpdate(updates, (accountUpdate, usesToken) => {
totalBalanceChange = totalBalanceChange.add(
Provable.if(usesToken, accountUpdate.balanceChange, Int64.zero)
);
});
// prove that the total balance change is zero
totalBalanceChange.assertEquals(0);
}
/**
* Approve a single account update (with arbitrarily many children).
*/
async approveAccountUpdate(accountUpdate: AccountUpdate | AccountUpdateTree) {
let forest = toForest([accountUpdate]);
await this.approveBase(forest);
}
/**
* Approve a list of account updates (with arbitrarily many children).
*/
async approveAccountUpdates(
accountUpdates: (AccountUpdate | AccountUpdateTree)[]
) {
let forest = toForest(accountUpdates);
await this.approveBase(forest);
}
// TRANSFERABLE API - simple wrapper around Approvable API
/**
* Transfer `amount` of tokens from `from` to `to`.
*/
async transfer(
from: PublicKey | AccountUpdate,
to: PublicKey | AccountUpdate,
amount: UInt64 | number | bigint
) {
// coerce the inputs to AccountUpdate and pass to `approveBase()`
let tokenId = this.deriveTokenId();
if (from instanceof PublicKey) {
from = AccountUpdate.defaultAccountUpdate(from, tokenId);
from.requireSignature();
from.label = `${this.constructor.name}.transfer() (from)`;
}
if (to instanceof PublicKey) {
to = AccountUpdate.defaultAccountUpdate(to, tokenId);
to.label = `${this.constructor.name}.transfer() (to)`;
}
from.balanceChange = Int64.from(amount).neg();
to.balanceChange = Int64.from(amount);
let forest = toForest([from, to]);
await this.approveBase(forest);
}
}
/** Version of `TokenContract` with the precise number of `MAX_ACCOUNT_UPDATES`
*
* The value of 20 in `TokenContract` was a rough upper limit, the precise upper
* bound is 9.
*/
abstract class TokenContractV2 extends TokenContract {
static MAX_ACCOUNT_UPDATES = 9;
}
function toForest(
updates: (AccountUpdate | AccountUpdateTree)[]
): AccountUpdateForest {
let trees = updates.map((a) =>
a instanceof AccountUpdate ? a.extractTree() : a
);
return AccountUpdateForest.fromReverse(trees);
}