Skip to content

Permissioned token example #18

@SpaceManiac

Description

@SpaceManiac

Note: could write in Rust if doing before Starstream language parser.

Note: likely better to use a linked-list design than the Merkle tree suggested below.

  • Prove that a value isn't in a blacklist...
  • Sorted linked list: prove that [..., A, C, ...] is in the list therefore B isn't in it
  • Still requires an offchain indexer to have the full list state somwhere but it's still simpler than a Merkle tree


Base for a Stablecoin example via @/SebastienGllmt: https://discord.com/channels/1325928330494476359/1348881648900374610/1348881727912673291

Details
  1. We define the USDC contract itself. It's a token contract where the token can only be bound/unbound from a UTXO if the address isn't on a blacklist
token USDC {
    abi {
        // represents querying the MerkleTree contract to know if we can send USDC to a specific address
        effect IsBlacklisted(addr: Address) -> UsdcTransferPermission::Intermediate
    }

    // can only bind (send to) this token to a UTXO owned by an address not on the blacklist
    bind() {
        const permission = raise IsBlacklisted(tx.context.caller);
        const caller = raise Caller();
        if (caller != permission.targetAddress) {
            fail "Wrong address for transfer permission"
        }
        permission.burn();
    }
    // can only unbind (send from) this token to a UTXO owned by an address not on the blacklist
    unbind() {
        const permission = raise IsBlacklisted(tx.context.caller);
        const caller = raise Caller();
        if (caller != permission.targetAddress) {
            fail "Wrong address for transfer permission"
        }
        permission.burn();
    }
}
  1. The token used to represent an address not being on a blacklist (returned by IsBlacklisted)
token UsdcTransferPermission {
    storage {
        targetAddr: Address // keep track of who we're giving permission to send USDC to
    }

    mint(targetAddr: Address) {
        const caller = raise Caller();
        // only the USDC permission MerkleTree contract can mint permission tokens
        if (caller !== UsdcPermission::address) {
            fail "Transfer permission can only be minted by UsdcPermission contract"
        }
        return Intermediate { targetAddr }
    }
    burn() {}
    bind() {
        fail "Cannot bind UsdcTransferPermission token"
    }
}
  1. The MerkleTree contract
utxo UsdcPermission {
    abi {
        // assume there exists some "MerkleTree" type implementation in Starstream
        fn add(tree: MerkleTree, addr: Address) -> void,
        fn includes(tree: MerkleTree, addr: Address) -> null | UsdcTransferPermission::Intermediate,
        fn remove(tree: MerkleTree, addr: Address) -> void,
    }

    storage {
        admin: Address,
        merkleRoot: uin256,
    }
    
    main {
        while (true) yield;
    }
    impl UsdcPermission {
        fn add(self, tree: MerkleTree, addr: Address) {
            if (tx.context.caller !== self.admin) {
                fail "Only admin can add new entries"
            }
            const newRoot = tree.add(addr);
            self.merkleRoot = newRoot;
        }
        fn remove(self, tree: MerkleTree, addr: Address) {
            // omitted for simplicity
        }
        // note: &self means this is a readonly input
        fn includes(&self, tree: MerkleTree, addr: Address) -> null | UsdcTransferPermission::Intermediate {
            if (!tree.includes(addr) {
                return null;
            }
            return UsdcTransferPermission::mint(addr);
        }
    }
}
  1. The coordination script
script {
    fn transferUsdc(
        source: PublicKeyHashUtxo, // instance of a pay-to-public-key-hash contract
        target: string, // key hash
        permissionState: UsdcPermission,
        merkleTree: MerkleTree,
        amount: uint256
    ) {
        const fromAddr = address(source.publicKeyHash);
        const targetAddr = address(target);
        const sendFromPermission = permissionState.includes(fromAddr , merkleTree)
        const sendToPermission = permissionState.includes(targetAddr, merkleTree)
        if (sendFromPermission == null || sendToPermission == null) fail "No permission for target"
        try {
            const tokens = yield source; // consumes the source UTXO
            const usdc = tokens.filter(token => token.token_id == USDC::token_id);
            const nonUsdc = tokens.filter(token => token.token_id != USDC::token_id);
            
            // omit: you would need to handle "amount" here to send the right amount
            // I leave it out to simplify
            
            // create the change utxo
            PublicKeyHash::main(source.publicKeyHash, nonUsdc)
            // create new UTXO that contains the USDC transfer
            PublicKeyHash::main(target, nonUsdc)
        } with IsBlacklisted(addr) => {
            // recall: this is called by the `bind`
            if (addr === targetAddr) {
                return sendToPermission;
            }
            // recall: this is called by the `unbind`
            if (addr === fromAddr) {
                return sendFromPermission ;
            }
            return null;
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationlanguageRelating to language parser and compiler

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions