Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 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
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ args = [
"--manifest-path",
"./defuse/Cargo.toml",
"--features",
"abi,contract",
"abi,contract,imt",
"--out-dir",
"${TARGET_DIR}",
"--no-embed-abi",
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ arbitrary = [
"defuse-token-id/arbitrary",
"defuse-ton-connect/arbitrary",
]
imt = ["defuse-token-id/imt"]

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
chrono = { workspace = true, features = ["now"] }
Expand Down
15 changes: 15 additions & 0 deletions core/src/engine/state/cached.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,21 @@ where

Ok(())
}

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

#[inline]
fn burn(
&mut self,
signer_id: &AccountIdRef,
tokens: Amounts,
_memo: Option<String>,
) -> Result<()> {
self.internal_sub_balance(signer_id, tokens)
}
}

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

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

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

/// Accumulates internal deposits and withdrawals on different tokens
Expand Down
9 changes: 9 additions & 0 deletions core/src/engine/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,13 @@ 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 mint(&mut self, owner_id: AccountId, tokens: Amounts, memo: Option<String>) -> Result<()>;

fn burn(
&mut self,
signer_id: &AccountIdRef,
tokens: Amounts,
memo: Option<String>,
) -> Result<()>;
}
4 changes: 4 additions & 0 deletions core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::{
engine::deltas::InvariantViolated,
intents::tokens::MAX_TOKEN_ID_LEN,
token_id::{TokenId, TokenIdError, nep171::Nep171TokenId},
};
use defuse_crypto::PublicKey;
Expand Down Expand Up @@ -75,4 +76,7 @@ pub enum DefuseError {

#[error("maximum attempts to generate a new salt reached")]
SaltGenerationFailed,

#[error("token ID is too large: {0} bytes (maximum is {MAX_TOKEN_ID_LEN} bytes)")]
TokenIdTooLarge(usize),
}
11 changes: 11 additions & 0 deletions core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ pub enum DefuseEvent<'a> {
#[event_version("0.3.0")]
StorageDeposit(Cow<'a, [IntentEvent<AccountEvent<'a, Cow<'a, StorageDeposit>>>]>),

#[cfg(feature = "imt")]
#[event_version("0.3.0")]
ImtMint(
Cow<'a, [IntentEvent<AccountEvent<'a, Cow<'a, crate::intents::tokens::imt::ImtMint>>>]>,
),

#[cfg(feature = "imt")]
#[event_version("0.3.0")]
ImtBurn(
Cow<'a, [IntentEvent<AccountEvent<'a, Cow<'a, crate::intents::tokens::imt::ImtBurn>>>]>,
),
#[event_version("0.3.0")]
#[from(skip)]
AccountLocked(AccountEvent<'a, ()>),
Expand Down
12 changes: 12 additions & 0 deletions core/src/intents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ pub enum Intent {

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

// See [`ImtMint`]
#[cfg(feature = "imt")]
ImtMint(crate::intents::tokens::imt::ImtMint),

// See [`ImtBurn`]
#[cfg(feature = "imt")]
ImtBurn(crate::intents::tokens::imt::ImtBurn),
}

pub trait ExecutableIntent {
Expand Down Expand Up @@ -125,6 +133,10 @@ impl ExecutableIntent for Intent {
intent.execute_intent(signer_id, engine, intent_hash)
}
Self::AuthCall(intent) => intent.execute_intent(signer_id, engine, intent_hash),
#[cfg(feature = "imt")]
Self::ImtMint(intent) => intent.execute_intent(signer_id, engine, intent_hash),
#[cfg(feature = "imt")]
Self::ImtBurn(intent) => intent.execute_intent(signer_id, engine, intent_hash),
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion core/src/intents/token_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,14 @@ impl TokenDiff {
match token_id {
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,
#[cfg(feature = "imt")]
TokenIdType::Imt => {
if amount <= 1 {
return Pips::ZERO;
}
}
}
fee
}
Expand Down
189 changes: 181 additions & 8 deletions core/src/intents/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ use crate::{

use super::{ExecutableIntent, IntentEvent};

pub const MAX_TOKEN_ID_LEN: usize = 127;

#[derive(thiserror::Error, Debug)]
#[error("token_id is too long: max length is {MAX_TOKEN_ID_LEN}, got {0}")]
pub struct TokenIdTooLarge(pub usize);

const MT_ON_TRANSFER_GAS_MIN: Gas = Gas::from_tgas(5);
const MT_ON_TRANSFER_GAS_DEFAULT: Gas = Gas::from_tgas(30);

#[must_use]
#[near(serializers = [borsh, json])]
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -73,17 +82,12 @@ pub struct Transfer {
/// Optionally notify receiver_id via `mt_on_transfer()`
///
/// NOTE: `min_gas` is adjusted with following values:
/// * default: 30TGas
/// * minimum: 5TGas
/// * default: 30TGas
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
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 @@ -127,9 +131,10 @@ impl ExecutableIntent for Transfer {
notification.min_gas = Some(
notification
.min_gas
.unwrap_or(Self::MT_ON_TRANSFER_GAS_DEFAULT)
.max(Self::MT_ON_TRANSFER_GAS_MIN),
.unwrap_or(MT_ON_TRANSFER_GAS_DEFAULT)
.max(MT_ON_TRANSFER_GAS_MIN),
);

engine
.state
.notify_on_transfer(sender_id, self.receiver_id, self.tokens, notification);
Expand Down Expand Up @@ -516,3 +521,171 @@ impl ExecutableIntent for StorageDeposit {
engine.state.storage_deposit(owner_id, self)
}
}

#[cfg(feature = "imt")]
pub mod imt {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider moving to separate module?

use super::{MT_ON_TRANSFER_GAS_DEFAULT, MT_ON_TRANSFER_GAS_MIN};
use crate::{Result, intents::tokens::MAX_TOKEN_ID_LEN};
use defuse_token_id::TokenId;
use near_sdk::{AccountId, AccountIdRef, CryptoHash, near};
use serde_with::{DisplayFromStr, serde_as};

use std::{borrow::Cow, collections::BTreeMap};

use crate::{
DefuseError,
accounts::AccountEvent,
amounts::Amounts,
engine::{Engine, Inspector, State},
events::DefuseEvent,
intents::{ExecutableIntent, IntentEvent, tokens::NotifyOnTransfer},
};

pub type ImtTokens = Amounts<BTreeMap<defuse_nep245::TokenId, u128>>;

impl ImtTokens {
#[inline]
fn into_generic_tokens(
self,
minter_id: &AccountIdRef,
) -> Result<Amounts<BTreeMap<TokenId, u128>>> {
let tokens = self
.into_iter()
.map(|(token_id, amount)| {
if token_id.len() > MAX_TOKEN_ID_LEN {
return Err(DefuseError::TokenIdTooLarge(token_id.len()));
}

let token = defuse_token_id::imt::ImtTokenId::new(minter_id, token_id).into();

Ok((token, amount))
})
Comment on lines +551 to +558
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move

        if token_id.len() > MAX_TOKEN_ID_LEN {
                        return Err(DefuseError::TokenIdTooLarge(token_id.len()));
        }

into ImtTokenId::new ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

token_id length limits are verifier-specific, since TokenId is reused in other crates, i.e. escrow-swap

.collect::<Result<_, _>>()?;

Ok(Amounts::new(tokens))
}
}

#[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 ImtMint {
pub receiver_id: AccountId,

// The tokens transferred in this call will be wrapped
// in such a way as to bind the token ID to the minter authority.
// The final string representation of the token
// will be as follows: `imt:<minter_id>:<token_id>`
#[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
pub tokens: ImtTokens,

#[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:
/// * minimum: 5TGas
/// * default: 30TGas
#[serde(flatten, default, skip_serializing_if = "Option::is_none")]
pub notification: Option<NotifyOnTransfer>,
}

impl ExecutableIntent for ImtMint {
#[inline]
fn execute_intent<S, I>(
self,
signer_id: &AccountIdRef,
engine: &mut Engine<S, I>,
intent_hash: CryptoHash,
) -> Result<()>
where
S: State,
I: Inspector,
{
if self.tokens.is_empty() {
return Err(DefuseError::InvalidIntent);
}

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

let tokens = self.tokens.into_generic_tokens(signer_id)?;
engine
.state
.mint(self.receiver_id.clone(), tokens.clone(), self.memo)?;

if let Some(mut notification) = self.notification {
notification.min_gas = Some(
notification
.min_gas
.unwrap_or(MT_ON_TRANSFER_GAS_DEFAULT)
.max(MT_ON_TRANSFER_GAS_MIN),
);

engine
.state
.notify_on_transfer(signer_id, self.receiver_id, tokens, notification);
}

Ok(())
}
}

#[near(serializers = [borsh, json])]
#[derive(Debug, Clone)]
/// Burn a set of imt tokens, within the intents contract.
pub struct ImtBurn {
// The minter authority of the imt tokens
pub minter_id: AccountId,

// The tokens transferred in this call will be wrapped
// in such a way as to bind the token ID to the minter authority.
// The final string representation of the token
// will be as follows: `imt:<minter_id>:<token_id>`
#[serde_as(as = "Amounts<BTreeMap<_, DisplayFromStr>>")]
pub tokens: ImtTokens,

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

impl ExecutableIntent for ImtBurn {
#[inline]
fn execute_intent<S, I>(
self,
signer_id: &AccountIdRef,
engine: &mut Engine<S, I>,
intent_hash: CryptoHash,
) -> Result<()>
where
S: State,
I: Inspector,
{
if self.tokens.is_empty() {
return Err(DefuseError::InvalidIntent);
}

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

let tokens = self.tokens.into_generic_tokens(&self.minter_id)?;

engine.state.burn(signer_id, tokens, self.memo)
}
}
}
Loading
Loading