diff --git a/Anchor.toml b/Anchor.toml index e618386..c6a5996 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -10,4 +10,4 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [scripts] -test = "mocha -t 1000000 tests/" +test = "yarn mocha -t 1000000 tests/" diff --git a/app/api.js b/app/api.js index 32b4309..1a4fddc 100644 --- a/app/api.js +++ b/app/api.js @@ -153,14 +153,13 @@ function borrow(user, mint, amount) { // we could also have a client approval, and this would be my ideal design // but requiring a third instruction drastically reduces avail bytes for transaction proper // the new tx format might make this viable by cutting address repetition tho - let repayIxn = adobe.instruction.repay(new anchor.BN(amount), { + let repayIxn = adobe.instruction.repay({ accounts: { user: user.publicKey, state: stateKey, pool: poolKey, poolToken: poolTokenKey, userToken: userTokenKey, - instructions: SYSVAR_INSTRUCTIONS_PUBKEY, tokenProgram: TOKEN_PROGRAM_ID, }}); diff --git a/package.json b/package.json index 010f60a..a8a8943 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "dependencies": { - "@project-serum/anchor": "^0.17.0", + "@project-serum/anchor": "0.18.0", "@solana/spl-token": "^0.1.8" }, "devDependencies": { diff --git a/programs/adobe/src/lib.rs b/programs/adobe/src/lib.rs index 24cb553..5bf374a 100644 --- a/programs/adobe/src/lib.rs +++ b/programs/adobe/src/lib.rs @@ -1,14 +1,14 @@ -use core::convert::TryInto; use anchor_lang::prelude::*; -use anchor_lang::Discriminator; -use anchor_spl::token::{self, Mint, TokenAccount, MintTo, Burn, Transfer, Token}; use anchor_lang::solana_program as solana; +use anchor_lang::Discriminator; +use anchor_spl::token::{self, Burn, Mint, MintTo, Token, TokenAccount, Transfer}; +use core::convert::TryInto; declare_id!("VzRKfyFWHZtYWbQWfcnCGBrTg3tqqRV2weUqvrvVhuo"); -const TOKEN_NAMESPACE: &[u8] = b"TOKEN"; +const TOKEN_NAMESPACE: &[u8] = b"TOKEN"; const VOUCHER_NAMESPACE: &[u8] = b"VOUCHER"; -const REPAY_OPCODE: u64 = 0xea674352d0eadba6; +const REPAY_OPCODE: u64 = 0xea674352d0eadba6; #[program] #[deny(unused_must_use)] @@ -32,7 +32,7 @@ pub mod adobe { msg!("adobe add_pool"); ctx.accounts.pool.bump = pool_bump; - ctx.accounts.pool.borrowing = false; + ctx.accounts.pool.borrowing = 0; ctx.accounts.pool.token_mint = ctx.accounts.token_mint.key(); ctx.accounts.pool.pool_token = ctx.accounts.pool_token.key(); ctx.accounts.pool.voucher_mint = ctx.accounts.voucher_mint.key(); @@ -45,10 +45,7 @@ pub mod adobe { pub fn deposit(ctx: Context, amount: u64) -> ProgramResult { msg!("adobe deposit"); - let state_seed: &[&[&[u8]]] = &[&[ - &State::discriminator()[..], - &[ctx.accounts.state.bump], - ]]; + let state_seed: &[&[&[u8]]] = &[&[&State::discriminator()[..], &[ctx.accounts.state.bump]]]; let transfer_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), @@ -82,10 +79,7 @@ pub mod adobe { pub fn withdraw(ctx: Context, amount: u64) -> ProgramResult { msg!("adobe withdraw"); - let state_seed: &[&[&[u8]]] = &[&[ - &State::discriminator()[..], - &[ctx.accounts.state.bump], - ]]; + let state_seed: &[&[&[u8]]] = &[&[&State::discriminator()[..], &[ctx.accounts.state.bump]]]; let burn_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), @@ -119,47 +113,39 @@ pub mod adobe { pub fn borrow(ctx: Context, amount: u64) -> ProgramResult { msg!("adobe borrow"); - if ctx.accounts.pool.borrowing { - return Err(AdobeError::Borrowing.into()); - } - let ixns = ctx.accounts.instructions.to_account_info(); - // make sure this isnt a cpi call - let current_index = solana::sysvar::instructions::load_current_index_checked(&ixns)? as usize; - let current_ixn = solana::sysvar::instructions::load_instruction_at_checked(current_index, &ixns)?; - if current_ixn.program_id != *ctx.program_id { - return Err(AdobeError::CpiBorrow.into()); - } + let current_index = + solana::sysvar::instructions::load_current_index_checked(&ixns)? as usize; // loop through instructions, looking for an equivalent repay to this borrow let mut i = current_index + 1; loop { // get the next instruction, die if theres no more if let Ok(ixn) = solana::sysvar::instructions::load_instruction_at_checked(i, &ixns) { - // check if we have a toplevel repay toward the same pool - // if so, confirm the amount, otherwise next instruction + // check if we have a toplevel repay toward the same pool, + // otherwise next instruction if ixn.program_id == *ctx.program_id - && u64::from_be_bytes(ixn.data[..8].try_into().unwrap()) == REPAY_OPCODE - && ixn.accounts[2].pubkey == ctx.accounts.pool.key() { - if u64::from_le_bytes(ixn.data[8..16].try_into().unwrap()) == amount { - break; - } else { - return Err(AdobeError::IncorrectRepay.into()); - } + && u64::from_be_bytes(ixn.data[..8].try_into().unwrap()) == REPAY_OPCODE + && ixn.accounts[2].pubkey == ctx.accounts.pool.key() + { + break; } else { i += 1; } - } - else { + } else { return Err(AdobeError::NoRepay.into()); } } - let state_seed: &[&[&[u8]]] = &[&[ - &State::discriminator()[..], - &[ctx.accounts.state.bump], - ]]; + ctx.accounts.pool.borrowing = ctx + .accounts + .pool + .borrowing + .checked_add(amount) + .ok_or(AdobeError::ExcessiveBorrow)?; + + let state_seed: &[&[&[u8]]] = &[&[&State::discriminator()[..], &[ctx.accounts.state.bump]]]; let transfer_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), @@ -172,29 +158,16 @@ pub mod adobe { ); token::transfer(transfer_ctx, amount)?; - ctx.accounts.pool.borrowing = true; Ok(()) } // REPAY // receives tokens - pub fn repay(ctx: Context, amount: u64) -> ProgramResult { + pub fn repay(ctx: Context) -> ProgramResult { msg!("adobe repay"); - let ixns = ctx.accounts.instructions.to_account_info(); - - // make sure this isnt a cpi call - let current_index = solana::sysvar::instructions::load_current_index_checked(&ixns)? as usize; - let current_ixn = solana::sysvar::instructions::load_instruction_at_checked(current_index, &ixns)?; - if current_ixn.program_id != *ctx.program_id { - return Err(AdobeError::CpiRepay.into()); - } - - let state_seed: &[&[&[u8]]] = &[&[ - &State::discriminator()[..], - &[ctx.accounts.state.bump], - ]]; + let state_seed: &[&[&[u8]]] = &[&[&State::discriminator()[..], &[ctx.accounts.state.bump]]]; let transfer_ctx = CpiContext::new_with_signer( ctx.accounts.token_program.to_account_info(), @@ -206,8 +179,8 @@ pub mod adobe { state_seed, ); - token::transfer(transfer_ctx, amount)?; - ctx.accounts.pool.borrowing = false; + token::transfer(transfer_ctx, ctx.accounts.pool.borrowing)?; + ctx.accounts.pool.borrowing = 0; Ok(()) } @@ -331,8 +304,6 @@ pub struct Repay<'info> { pub pool_token: Account<'info, TokenAccount>, #[account(mut, constraint = user_token.mint == pool.token_mint)] pub user_token: Account<'info, TokenAccount>, - #[account(address = solana::sysvar::instructions::ID)] - pub instructions: UncheckedAccount<'info>, pub token_program: Program<'info, Token>, } @@ -347,7 +318,7 @@ pub struct State { #[derive(Default)] pub struct Pool { bump: u8, - borrowing: bool, + borrowing: u64, token_mint: Pubkey, pool_token: Pubkey, voucher_mint: Pubkey, @@ -356,13 +327,7 @@ pub struct Pool { #[error] pub enum AdobeError { #[msg("borrow requires an equivalent repay")] - NoRepay, - #[msg("repay exists but in the wrong amount")] - IncorrectRepay, - #[msg("cannot call borrow via cpi")] - CpiBorrow, - #[msg("cannot call repay via cpi")] - CpiRepay, - #[msg("a borrow is already in progress")] - Borrowing, + NoRepay, // 0x12c + #[msg("excessive borrow amount")] + ExcessiveBorrow, // 0x12d } diff --git a/programs/evil/src/lib.rs b/programs/evil/src/lib.rs index 96f5577..7f66a00 100644 --- a/programs/evil/src/lib.rs +++ b/programs/evil/src/lib.rs @@ -1,5 +1,5 @@ +use adobe::cpi::accounts::{Borrow, Repay}; use anchor_lang::prelude::*; -use adobe::cpi::accounts::{ Borrow, Repay}; declare_id!("5zAQ1XhjuHcQtUXJSTjbmyDagmKVHDMi5iADv5PfYEUK"); @@ -25,10 +25,10 @@ pub mod evil { Ok(()) } - pub fn repay_proxy(ctx: Context, amount: u64) -> ProgramResult { + pub fn repay_proxy(ctx: Context) -> ProgramResult { msg!("evil repay_proxy"); - adobe::cpi::repay(ctx.accounts.into_repay_context(), amount)?; + adobe::cpi::repay(ctx.accounts.into_repay_context())?; Ok(()) } @@ -72,7 +72,6 @@ impl<'info> Adobe<'info> { pool: self.pool.clone(), pool_token: self.pool_token.clone(), user_token: self.user_token.clone(), - instructions: self.instructions.clone(), token_program: self.token_program.clone(), }, ) diff --git a/tests/adobe.js b/tests/adobe.js index b1d4561..164fc93 100644 --- a/tests/adobe.js +++ b/tests/adobe.js @@ -1,13 +1,12 @@ import assert from "assert"; import anchor from "@project-serum/anchor"; import spl from "@solana/spl-token"; -import { findAddr, findAssocAddr, discriminator, airdrop } from "../app/util.js"; +import { findAddr, discriminator, airdrop } from "../app/util.js"; import * as api from "../app/api.js"; const LAMPORTS_PER_SOL = anchor.web3.LAMPORTS_PER_SOL; const SYSVAR_INSTRUCTIONS_PUBKEY = anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY; const TOKEN_PROGRAM_ID = spl.TOKEN_PROGRAM_ID; -const ASSOCIATED_TOKEN_PROGRAM_ID = spl.ASSOCIATED_TOKEN_PROGRAM_ID; const TXN_OPTS = {commitment: "processed", preflightCommitment: "processed", skipPreflight: true}; const TOKEN_DECIMALS = 6; @@ -127,17 +126,6 @@ describe("adobe flash loan program", () => { balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); - // dont fully repay - [borrowIxn] = api.borrow(wallet, tokenMint, amount + 1); - txn = new anchor.web3.Transaction; - txn.add(borrowIxn); - txn.add(repayIxn); - - balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "borrow more than repay fails"); - balAfter = await poolBalance(tokenMint); - assert.equal(balAfter, balBefore, "program token balance unchanged"); - // borrow too much [borrowIxn, repayIxn] = api.borrow(wallet, tokenMint, amount * 10); txn = new anchor.web3.Transaction; @@ -150,18 +138,18 @@ describe("adobe flash loan program", () => { assert.equal(balAfter, balBefore, "program token balance unchanged"); // double borrow (raw instruction) + [borrowIxn, repayIxn] = api.borrow(wallet, tokenMint, amount / 10); txn = new anchor.web3.Transaction; txn.add(borrowIxn); txn.add(borrowIxn); txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "multiple borrow fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); - // dounle borrow (direct cpi) - [borrowIxn, repayIxn] = api.borrow(wallet, tokenMint, amount / 10); + // double borrow (direct cpi) let evilIxn = evil.instruction.borrowProxy(new anchor.BN(amount / 10), { accounts: { user: wallet.publicKey, @@ -181,7 +169,7 @@ describe("adobe flash loan program", () => { txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "borrow and cpi fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); @@ -191,7 +179,7 @@ describe("adobe flash loan program", () => { txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "cpi and borrow fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); @@ -201,11 +189,11 @@ describe("adobe flash loan program", () => { txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "cpi and cpi fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); - // dounle borrow (batched cpi) + // double borrow (batched cpi) evilIxn = evil.instruction.borrowDouble(new anchor.BN(amount / 10), { accounts: { user: wallet.publicKey, @@ -224,7 +212,7 @@ describe("adobe flash loan program", () => { txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "cpi double borrow fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); @@ -261,7 +249,7 @@ describe("adobe flash loan program", () => { adobeProgram: adobe.programId, }, }); - let evilRepay = evil.instruction.repayProxy(new anchor.BN(1), { + let evilRepay = evil.instruction.repayProxy({ accounts: { user: wallet.publicKey, state: stateKey, @@ -281,7 +269,7 @@ describe("adobe flash loan program", () => { txn.add(repayIxn); balBefore = await poolBalance(tokenMint); - await assert.rejects(provider.send(txn), "cpi dummy repay fails"); + await provider.send(txn); balAfter = await poolBalance(tokenMint); assert.equal(balAfter, balBefore, "program token balance unchanged"); }); diff --git a/yarn.lock b/yarn.lock index 4cb2baf..6dbd34b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,10 +9,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@project-serum/anchor@^0.17.0": - version "0.17.0" - resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.17.0.tgz#99a2cc59cae2939c1db1197950b7b606319ea7a8" - integrity sha512-vctZHZD5rUP1WXUUaJFdKl+1u0dE+O2+jejmYcb0InAZiwXdGAW8mX47U902voT94PdCr0aNHuyOQ8I75nfinw== +"@project-serum/anchor@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.18.0.tgz#867144282e59482230f797f73ee9f5634f846061" + integrity sha512-WTm+UB93MoxyCbjnHIibv/uUEoO/5gL4GEtE/aMioLF8Z4i0vCMPnvAN0xpk9VBu3t7ld2DcCE/L+6Z7dwU++w== dependencies: "@project-serum/borsh" "^0.2.2" "@solana/web3.js" "^1.17.0"