Skip to content

Commit

Permalink
Merge pull request #66 from project-serum/armani/realize
Browse files Browse the repository at this point in the history
Program interfaces
  • Loading branch information
armaniferrante authored Feb 8, 2021
2 parents c643438 + a6cc210 commit a903d48
Show file tree
Hide file tree
Showing 29 changed files with 903 additions and 39 deletions.
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

0 comments on commit a903d48

Please sign in to comment.