Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
142726e
pubkey account synchronizer added
hlgltvnnk Jan 7, 2026
475e5a7
test fixed
hlgltvnnk Jan 8, 2026
00dc35b
mint tokens method added
hlgltvnnk Jan 8, 2026
8f56a19
mint tokens fn call added
hlgltvnnk Jan 8, 2026
47cb36c
burn intent added
hlgltvnnk Jan 8, 2026
552904f
burn intent added
hlgltvnnk Jan 9, 2026
b940a6f
naming upd
hlgltvnnk Jan 12, 2026
60524fd
mint + burn intents changes
hlgltvnnk Jan 12, 2026
1e277ae
tests updated
hlgltvnnk Jan 12, 2026
3347af8
minor edits
hlgltvnnk Jan 12, 2026
fdf1353
mint intent updated
hlgltvnnk Jan 13, 2026
6ba4a39
fix
hlgltvnnk Jan 13, 2026
e5c3096
fix
hlgltvnnk Jan 13, 2026
3e6ca0b
review changes
hlgltvnnk Jan 13, 2026
a82384f
fix
hlgltvnnk Jan 13, 2026
99e38ee
burn intent removed
hlgltvnnk Jan 15, 2026
8146632
notification added to mt_mint intent
hlgltvnnk Jan 15, 2026
974ab4f
dip5 standart added
hlgltvnnk Jan 16, 2026
6602cee
burn intents added + tests updated
hlgltvnnk Jan 16, 2026
2aa54eb
burn tests added
hlgltvnnk Jan 16, 2026
174654e
typo fix
hlgltvnnk Jan 16, 2026
416ad99
naming updated
hlgltvnnk Jan 16, 2026
18f4268
simulate burn intent test added
hlgltvnnk Jan 16, 2026
b2b2e13
refactoring
hlgltvnnk Jan 22, 2026
1f0037c
Merge branch 'main' into feat/far-acc-management
hlgltvnnk Jan 22, 2026
a1e0f37
imt feature added
hlgltvnnk Jan 22, 2026
e5f8b68
notification min gas upd
hlgltvnnk Jan 23, 2026
38344f4
mint intent updated
hlgltvnnk Jan 23, 2026
a352ecd
minor edit
hlgltvnnk Jan 23, 2026
709aeac
refactoring
hlgltvnnk Jan 23, 2026
deeace5
minter added to burn intent + notification static gas limits updated
hlgltvnnk Jan 26, 2026
6cac2b2
doc fix
hlgltvnnk Jan 26, 2026
d75da27
fix
hlgltvnnk Jan 26, 2026
37a1953
fix
hlgltvnnk Jan 26, 2026
bf617a7
refactorign
hlgltvnnk Jan 26, 2026
ea03a31
fix
hlgltvnnk Jan 26, 2026
60c1c4b
fix
hlgltvnnk Jan 26, 2026
6a3b585
fix
hlgltvnnk Jan 26, 2026
9cc44df
chore
mitinarseny Jan 26, 2026
f54ae8b
fix: allocate more gas for implicit Near account_id transfer
mitinarseny Jan 26, 2026
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
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-all-crates: "true" # Yes, it's a string
- name: Install Cargo Plugins
run: cargo install cargo-make cargo-near --locked
run: cargo install cargo-make cargo-near@0.17.0 --locked
- name: Build
run: cargo make build
- name: Upload Artifacts
Expand Down Expand Up @@ -104,7 +104,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-all-crates: "true" # Yes, it's a string
- name: Install Cargo Plugins
run: cargo install cargo-make cargo-near --locked
run: cargo install cargo-make cargo-near@0.17.0 --locked
- name: Build Reproducible WASM and calculate checksum
run: cargo make build-reproducible
- name: Upload Reproducible Artifacts
Expand Down Expand Up @@ -162,7 +162,7 @@ jobs:
with:
cache: false
- name: Install cargo-audit
run: cargo install cargo-audit --version "^0.21" --locked
run: cargo install cargo-audit --version "^0.22" --locked
- uses: rustsec/audit-check@v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -188,7 +188,8 @@ jobs:
- name: Install Cargo Plugins
run: cargo install cargo-audit --locked
- name: Run security audit
run: cargo audit --deny unsound --deny yanked
# TODO: ignoring lru vulnerability in near-sdk, remove when fixed
run: cargo audit --deny unsound --deny yanked --ignore RUSTSEC-2026-0002
# run: cargo audit --deny warnings # Warnings include: unmaintained, unsound and yanked

contract_analysis:
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defuse-token-id = { workspace = true, features = [
"nep141",
"nep171",
"nep245",
"dip5"
] }
defuse-ton-connect.workspace = true
defuse-webauthn.workspace = true
Expand Down
10 changes: 10 additions & 0 deletions core/src/engine/state/cached.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,16 @@ where

Ok(())
}

#[inline]
fn mt_mint(
&mut self,
owner_id: AccountId,
tokens: Amounts,
_memo: Option<String>,
) -> Result<()> {
self.internal_add_balance(owner_id, tokens)
}
}

#[derive(Debug, Default)]
Expand Down
10 changes: 10 additions & 0 deletions core/src/engine/state/deltas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,16 @@ where
fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()> {
self.state.auth_call(signer_id, auth_call)
}

#[inline]
fn mt_mint(
&mut self,
owner_id: AccountId,
tokens: Amounts,
memo: Option<String>,
) -> Result<()> {
self.state.mt_mint(owner_id, tokens, memo)
}
}

/// Accumulates internal deposits and withdrawals on different tokens
Expand Down
3 changes: 3 additions & 0 deletions core/src/engine/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,7 @@ pub trait State: StateView {
fn set_auth_by_predecessor_id(&mut self, account_id: AccountId, enable: bool) -> Result<bool>;

fn auth_call(&mut self, signer_id: &AccountIdRef, auth_call: AuthCall) -> Result<()>;

fn mt_mint(&mut self, owner_id: AccountId, tokens: Amounts, memo: Option<String>)
-> Result<()>;
}
5 changes: 4 additions & 1 deletion core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
IntentEvent,
account::SetAuthByPredecessorId,
token_diff::TokenDiffEvent,
tokens::{FtWithdraw, MtWithdraw, NativeWithdraw, NftWithdraw, StorageDeposit},
tokens::{FtWithdraw, MtMint, MtWithdraw, NativeWithdraw, NftWithdraw, StorageDeposit},
},
};

Expand Down Expand Up @@ -54,6 +54,9 @@ pub enum DefuseEvent<'a> {
#[event_version("0.3.0")]
StorageDeposit(Cow<'a, [IntentEvent<AccountEvent<'a, Cow<'a, StorageDeposit>>>]>),

#[event_version("0.3.0")]
MtMint(Cow<'a, [IntentEvent<AccountEvent<'a, Cow<'a, MtMint>>>]>),

#[event_version("0.3.0")]
#[from(skip)]
AccountLocked(AccountEvent<'a, ()>),
Expand Down
6 changes: 5 additions & 1 deletion core/src/intents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tokens::{NativeWithdraw, StorageDeposit};
use crate::{
Result,
engine::{Engine, Inspector, State},
intents::{account::SetAuthByPredecessorId, auth::AuthCall},
intents::{account::SetAuthByPredecessorId, auth::AuthCall, tokens::MtMint},
};

use self::{
Expand Down Expand Up @@ -68,6 +68,9 @@ pub enum Intent {

/// See [`AuthCall`]
AuthCall(AuthCall),

// See [`MtMint`]
MtMint(MtMint),
}

pub trait ExecutableIntent {
Expand Down Expand Up @@ -125,6 +128,7 @@ impl ExecutableIntent for Intent {
intent.execute_intent(signer_id, engine, intent_hash)
}
Self::AuthCall(intent) => intent.execute_intent(signer_id, engine, intent_hash),
Self::MtMint(intent) => intent.execute_intent(signer_id, engine, intent_hash),
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions core/src/intents/token_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ impl TokenDiff {
TokenIdType::Nep141 => {}
TokenIdType::Nep245 if amount > 1 => {}

// do not take fees on NFTs and MTs with |delta| <= 1
TokenIdType::Nep171 | TokenIdType::Nep245 => return Pips::ZERO,
// do not take fees on NFTs, Dip5 tokens and MTs with |delta| <= 1
TokenIdType::Nep171 | TokenIdType::Nep245 | TokenIdType::Dip5 => return Pips::ZERO,
}
fee
}
Expand Down
85 changes: 73 additions & 12 deletions core/src/intents/tokens.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{borrow::Cow, collections::BTreeMap};

use defuse_token_id::nep245::Nep245TokenId;
use near_contract_standards::non_fungible_token;
use near_sdk::{
AccountId, AccountIdRef, CryptoHash, Gas, NearToken, json_types::U128, near,
Expand Down Expand Up @@ -39,6 +40,9 @@ pub struct NotifyOnTransfer {
}

impl NotifyOnTransfer {
pub const MT_ON_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(5);
pub const MT_ON_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(30);

pub const fn new(msg: String) -> Self {
Self {
state_init: None,
Expand Down Expand Up @@ -79,11 +83,6 @@ pub struct Transfer {
pub notification: Option<NotifyOnTransfer>,
}

impl Transfer {
pub const MT_ON_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(5);
pub const MT_ON_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(30);
}

impl ExecutableIntent for Transfer {
fn execute_intent<S, I>(
self,
Expand Down Expand Up @@ -123,13 +122,7 @@ impl ExecutableIntent for Transfer {
.state
.internal_add_balance(self.receiver_id.clone(), self.tokens.clone())?;

if let Some(mut notification) = self.notification {
notification.min_gas = Some(
notification
.min_gas
.unwrap_or(Self::MT_ON_TRANSFER_GAS_DEFAULT)
.max(Self::MT_ON_TRANSFER_GAS_MIN),
);
if let Some(notification) = self.notification {
engine
.state
.notify_on_transfer(sender_id, self.receiver_id, self.tokens, notification);
Expand Down Expand Up @@ -516,3 +509,71 @@ impl ExecutableIntent for StorageDeposit {
engine.state.storage_deposit(owner_id, self)
}
}

#[near(serializers = [borsh, json])]
#[derive(Debug, Clone)]
/// Mint a set of tokens from the signer to a specified account id, within the intents contract.
pub struct MtMint {
pub receiver_id: AccountId,

#[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
pub tokens: Amounts,

#[serde(default, skip_serializing_if = "Option::is_none")]
pub memo: Option<String>,

/// Optionally notify receiver_id via `mt_on_transfer()`
///
/// NOTE: `min_gas` is adjusted with following values:
/// * default: 30TGas
/// * minimum: 5TGas
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
pub notification: Option<NotifyOnTransfer>,
}

impl ExecutableIntent for MtMint {
#[inline]
fn execute_intent<S, I>(
mut self,
owner_id: &AccountIdRef,
engine: &mut Engine<S, I>,
intent_hash: CryptoHash,
) -> Result<()>
where
S: State,
I: Inspector,
{
self.tokens = Amounts::new(
self.tokens
.iter()
.map(|(token_id, amount)| {
let token = Nep245TokenId::new(owner_id, token_id.to_string());
(token.into(), *amount)
})
.collect::<BTreeMap<_, _>>(),
);

engine.inspector.on_event(DefuseEvent::MtMint(Cow::Borrowed(
[IntentEvent::new(
AccountEvent::new(owner_id, Cow::Borrowed(&self)),
intent_hash,
)]
.as_slice(),
)));

engine
.state
.mt_mint(self.receiver_id.clone(), self.tokens.clone(), self.memo)?;

if let Some(notification) = self.notification {
engine.state.notify_on_transfer(
owner_id,
self.receiver_id,
self.tokens.clone(),
notification,
);
}

Ok(())
}
}
13 changes: 12 additions & 1 deletion defuse/src/accounts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};

use defuse_core::{Nonce, crypto::PublicKey};
use defuse_serde_utils::base64::AsBase64;
Expand Down Expand Up @@ -82,4 +82,15 @@ pub trait ForceAccountManager: AccessControllable {
///
/// NOTE: MUST attach 1 yⓃ for security purposes.
fn force_enable_auth_by_predecessor_ids(&mut self, account_ids: Vec<AccountId>);

/// Registers or re-activates `public_key` under the user account_id.
///
/// NOTE: MUST attach 1 yⓃ for security purposes.
fn force_add_public_keys(&mut self, public_keys: HashMap<AccountId, HashSet<PublicKey>>);

/// Deactivate `public_key` from the user account_id,
/// i.e. this key can't be used to make any actions unless it's re-created.
///
/// NOTE: MUST attach 1 yⓃ for security purposes.
fn force_remove_public_keys(&mut self, public_keys: HashMap<AccountId, HashSet<PublicKey>>);
}
53 changes: 48 additions & 5 deletions defuse/src/contract/accounts/force.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use std::collections::{HashMap, HashSet};

use defuse_core::{
DefuseError, Result, accounts::AccountEvent, engine::StateView, events::DefuseEvent,
DefuseError, Result, accounts::AccountEvent, crypto::PublicKey, engine::StateView,
events::DefuseEvent,
};
use defuse_near_utils::Lock;
use near_plugins::{AccessControllable, access_control_any};
Expand All @@ -16,7 +19,11 @@ impl ForceAccountManager for Contract {
StateView::is_account_locked(self, account_id)
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountLocker))]
#[access_control_any(roles(
Role::DAO,
Role::UnrestrictedAccountLocker,
Role::UnrestrictedAccountManager
))]
#[payable]
fn force_lock_account(&mut self, account_id: AccountId) -> bool {
assert_one_yocto();
Expand All @@ -31,7 +38,11 @@ impl ForceAccountManager for Contract {
locked
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountUnlocker))]
#[access_control_any(roles(
Role::DAO,
Role::UnrestrictedAccountUnlocker,
Role::UnrestrictedAccountManager
))]
#[payable]
fn force_unlock_account(&mut self, account_id: &AccountId) -> bool {
assert_one_yocto();
Expand All @@ -46,7 +57,11 @@ impl ForceAccountManager for Contract {
unlocked
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountLocker))]
#[access_control_any(roles(
Role::DAO,
Role::UnrestrictedAccountLocker,
Role::UnrestrictedAccountManager
))]
#[payable]
fn force_disable_auth_by_predecessor_ids(&mut self, account_ids: Vec<AccountId>) {
assert_one_yocto();
Expand All @@ -57,7 +72,11 @@ impl ForceAccountManager for Contract {
}
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountUnlocker))]
#[access_control_any(roles(
Role::DAO,
Role::UnrestrictedAccountUnlocker,
Role::UnrestrictedAccountManager
))]
#[payable]
fn force_enable_auth_by_predecessor_ids(&mut self, account_ids: Vec<AccountId>) {
assert_one_yocto();
Expand All @@ -67,6 +86,30 @@ impl ForceAccountManager for Contract {
let _ = self.internal_set_auth_by_predecessor_id(&account_id, true, true);
}
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountManager))]
#[payable]
fn force_add_public_keys(&mut self, entries: HashMap<AccountId, HashSet<PublicKey>>) {
assert_one_yocto();

for (account_id, keys) in entries {
for public_key in keys {
self.add_public_key(account_id.as_ref(), public_key);
}
}
}

#[access_control_any(roles(Role::DAO, Role::UnrestrictedAccountManager))]
#[payable]
fn force_remove_public_keys(&mut self, entries: HashMap<AccountId, HashSet<PublicKey>>) {
assert_one_yocto();

for (account_id, keys) in entries {
for public_key in keys {
self.remove_public_key(account_id.as_ref(), public_key);
}
}
}
}

impl Contract {
Expand Down
Loading
Loading