Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Program interfaces #66

Merged
merged 2 commits into from
Feb 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- pushd examples/errors && anchor test && popd
- pushd examples/spl/token-proxy && anchor test && popd
- pushd examples/multisig && anchor test && popd
- pushd examples/interface && anchor test && popd
- pushd examples/tutorial/basic-0 && anchor test && popd
- pushd examples/tutorial/basic-1 && anchor test && popd
- pushd examples/tutorial/basic-2 && anchor test && popd
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ incremented for features.

## [Unreleased]

### Features

* lang: Adds the ability to create and use CPI program interfaces [(#66)](https://github.com/project-serum/anchor/pull/66/files?file-filters%5B%5D=).

### Breaking Changes

* lang, client, ts: Migrate from rust enum based method dispatch to a variant of sighash [(#64)](https://github.com/project-serum/anchor/pull/64).

## [0.1.0] - 2021-01-31
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions examples/interface/Anchor.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cluster = "localnet"
wallet = "~/.config/solana/id.json"
4 changes: 4 additions & 0 deletions examples/interface/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]
19 changes: 19 additions & 0 deletions examples/interface/programs/counter-auth/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "counter-auth"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"

[lib]
crate-type = ["cdylib", "lib"]
name = "counter_auth"

[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor" }
counter = { path = "../counter", features = ["cpi"] }
2 changes: 2 additions & 0 deletions examples/interface/programs/counter-auth/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
43 changes: 43 additions & 0 deletions examples/interface/programs/counter-auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//! counter-auth is an example of a program *implementing* an external program
//! interface. Here the `counter::Auth` trait, where we only allow a count
//! to be incremented if it changes the counter from odd -> even or even -> odd.
//! Creative, I know. :P.

#![feature(proc_macro_hygiene)]

use anchor_lang::prelude::*;
use counter::Auth;

#[program]
pub mod counter_auth {
use super::*;

#[state]
pub struct CounterAuth {}

// TODO: remove this impl block after addressing
// https://github.com/project-serum/anchor/issues/71.
impl CounterAuth {
pub fn new(_ctx: Context<Empty>) -> Result<Self, ProgramError> {
Ok(Self {})
}
}

impl<'info> Auth<'info, Empty> for CounterAuth {
fn is_authorized(_ctx: Context<Empty>, current: u64, new: u64) -> ProgramResult {
if current % 2 == 0 {
if new % 2 == 0 {
return Err(ProgramError::Custom(50)); // Arbitrary error code.
}
} else {
if new % 2 == 1 {
return Err(ProgramError::Custom(60)); // Arbitrary error code.
}
}
Ok(())
}
}
}

#[derive(Accounts)]
pub struct Empty {}
18 changes: 18 additions & 0 deletions examples/interface/programs/counter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "counter"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"

[lib]
crate-type = ["cdylib", "lib"]
name = "counter"

[features]
no-entrypoint = []
no-idl = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor" }
2 changes: 2 additions & 0 deletions examples/interface/programs/counter/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
73 changes: 73 additions & 0 deletions examples/interface/programs/counter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! counter is an example program that depends on an external interface
//! that another program must implement. This allows our program to depend
//! on another program, without knowing anything about it other than the fact
//! that it implements the `Auth` trait.
//!
//! Here, we have a counter, where, in order to set the count, the `Auth`
//! program must first approve the transaction.

#![feature(proc_macro_hygiene)]

use anchor_lang::prelude::*;

#[program]
pub mod counter {
use super::*;

#[state]
pub struct Counter {
pub count: u64,
pub auth_program: Pubkey,
}

impl Counter {
pub fn new(_ctx: Context<Empty>, auth_program: Pubkey) -> Result<Self> {
Ok(Self {
count: 0,
auth_program,
})
}

#[access_control(SetCount::accounts(&self, &ctx))]
pub fn set_count(&mut self, ctx: Context<SetCount>, new_count: u64) -> Result<()> {
// Ask the auth program if we should approve the transaction.
let cpi_program = ctx.accounts.auth_program.clone();
let cpi_ctx = CpiContext::new(cpi_program, Empty {});
auth::is_authorized(cpi_ctx, self.count, new_count)?;

// Approved, so update.
self.count = new_count;
Ok(())
}
}
}

#[derive(Accounts)]
pub struct Empty {}

#[derive(Accounts)]
pub struct SetCount<'info> {
auth_program: AccountInfo<'info>,
}

impl<'info> SetCount<'info> {
// Auxiliary account validation requiring program inputs. As a convention,
// we separate it from the business logic of the instruction handler itself.
pub fn accounts(counter: &Counter, ctx: &Context<SetCount>) -> Result<()> {
if ctx.accounts.auth_program.key != &counter.auth_program {
return Err(ErrorCode::InvalidAuthProgram.into());
}
Ok(())
}
}

#[interface]
pub trait Auth<'info, T: Accounts<'info>> {
fn is_authorized(ctx: Context<T>, current: u64, new: u64) -> ProgramResult;
}

#[error]
pub enum ErrorCode {
#[msg("Invalid auth program.")]
InvalidAuthProgram,
}
45 changes: 45 additions & 0 deletions examples/interface/tests/interface.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const anchor = require('@project-serum/anchor');
const assert = require("assert");

describe("interface", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.Provider.env());

const counter = anchor.workspace.Counter;
const counterAuth = anchor.workspace.CounterAuth;
it("Is initialized!", async () => {
await counter.state.rpc.new(counterAuth.programId);

const stateAccount = await counter.state();
assert.ok(stateAccount.count.eq(new anchor.BN(0)));
assert.ok(stateAccount.authProgram.equals(counterAuth.programId));
});

it("Should fail to go from even to event", async () => {
await assert.rejects(
async () => {
await counter.state.rpc.setCount(new anchor.BN(4), {
accounts: {
authProgram: counterAuth.programId,
},
});
},
(err) => {
if (err.toString().split("custom program error: 0x32").length !== 2) {
return false;
}
return true;
}
);
});

it("Shold succeed to go from even to odd", async () => {
await counter.state.rpc.setCount(new anchor.BN(3), {
accounts: {
authProgram: counterAuth.programId,
},
});
const stateAccount = await counter.state();
assert.ok(stateAccount.count.eq(new anchor.BN(3)));
});
});
67 changes: 66 additions & 1 deletion examples/lockup/programs/lockup/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub mod lockup {
period_count: u64,
deposit_amount: u64,
nonce: u8,
realizor: Option<Realizor>,
) -> Result<()> {
if end_ts <= ctx.accounts.clock.unix_timestamp {
return Err(ErrorCode::InvalidTimestamp.into());
Expand All @@ -100,12 +101,14 @@ pub mod lockup {
vesting.whitelist_owned = 0;
vesting.grantor = *ctx.accounts.depositor_authority.key;
vesting.nonce = nonce;
vesting.realizor = realizor;

token::transfer(ctx.accounts.into(), deposit_amount)?;

Ok(())
}

#[access_control(is_realized(&ctx))]
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
// Has the given amount vested?
if amount
Expand Down Expand Up @@ -187,7 +190,7 @@ pub mod lockup {
Ok(())
}

// Convenience function for UI's to calculate the withdrawalable amount.
// Convenience function for UI's to calculate the withdrawable amount.
pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
let available = calculator::available_for_withdrawal(
&ctx.accounts.vesting,
Expand Down Expand Up @@ -242,6 +245,8 @@ impl<'info> CreateVesting<'info> {
}
}

// All accounts not included here, i.e., the "remaining accounts" should be
// ordered according to the realization interface.
#[derive(Accounts)]
pub struct Withdraw<'info> {
// Vesting.
Expand Down Expand Up @@ -327,6 +332,29 @@ pub struct Vesting {
pub whitelist_owned: u64,
/// Signer nonce.
pub nonce: u8,
/// The program that determines when the locked account is **realized**.
/// In addition to the lockup schedule, the program provides the ability
/// for applications to determine when locked tokens are considered earned.
/// For example, when earning locked tokens via the staking program, one
/// cannot receive the tokens until unstaking. As a result, if one never
/// unstakes, one would never actually receive the locked tokens.
pub realizor: Option<Realizor>,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
pub struct Realizor {
/// Program to invoke to check a realization condition. This program must
/// implement the `RealizeLock` trait.
pub program: Pubkey,
/// Address of an arbitrary piece of metadata interpretable by the realizor
/// program. For example, when a vesting account is allocated, the program
/// can define its realization condition as a function of some account
/// state. The metadata is the address of that account.
///
/// In the case of staking, the metadata is a `Member` account address. When
/// the realization condition is checked, the staking program will check the
/// `Member` account defined by the `metadata` has no staked tokens.
pub metadata: Pubkey,
}

#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
Expand Down Expand Up @@ -366,6 +394,12 @@ pub enum ErrorCode {
WhitelistEntryNotFound,
#[msg("You do not have sufficient permissions to perform this action.")]
Unauthorized,
#[msg("You are unable to realize projected rewards until unstaking.")]
UnableToWithdrawWhileStaked,
#[msg("The given lock realizor doesn't match the vesting account.")]
InvalidLockRealizor,
#[msg("You have not realized this vesting account.")]
UnrealizedVesting,
}

impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
Expand Down Expand Up @@ -456,3 +490,34 @@ fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
}
Ok(())
}

// Returns Ok if the locked vesting account has been "realized". Realization
// is application dependent. For example, in the case of staking, one must first
// unstake before being able to earn locked tokens.
fn is_realized<'info>(ctx: &Context<Withdraw>) -> Result<()> {
if let Some(realizor) = &ctx.accounts.vesting.realizor {
let cpi_program = {
let p = ctx.remaining_accounts[0].clone();
if p.key != &realizor.program {
return Err(ErrorCode::InvalidLockRealizor.into());
}
p
};
let cpi_accounts = ctx.remaining_accounts.to_vec()[1..].to_vec();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
let vesting = (*ctx.accounts.vesting).clone();
realize_lock::is_realized(cpi_ctx, vesting).map_err(|_| ErrorCode::UnrealizedVesting)?;
}
Ok(())
}

/// RealizeLock defines the interface an external program must implement if
/// they want to define a "realization condition" on a locked vesting account.
/// This condition must be satisfied *even if a vesting schedule has
/// completed*. Otherwise the user can never earn the locked funds. For example,
/// in the case of the staking program, one cannot received a locked reward
/// until one has completely unstaked.
#[interface]
pub trait RealizeLock<'info, T: Accounts<'info>> {
fn is_realized(ctx: Context<T>, v: Vesting) -> ProgramResult;
}
Loading