Repository: dusk-network/rusk (language: rust)

Issue:
Allow stake from contract
Fully implemented by:
- #3027 
- #3148 

# Common Modifications to the Stake Contract 


## STAKE

- Modify the stake contract to allow staking on an existing stake (aka topup) 
- For each additional stake, 10% of the added amount will be moved to the locked balance (*Only if the stake is already eligible*). ~~(Alternatively, consider applying the 10% to the total stake.)~~

Implemented here #3027 

## UNSTAKE

- Add support for partial unstaking.
- If "locked" funds exist, active funds will be prioritized for the unstake operation.

Implemented here #3027 

# STAKE FROM CONTRACT

- Enable staking from a contract (e.g., use case for a referral contract).
- The contract will stake by specifying a provisioner key (public) embedded within the contract, without specifying a funds key.
- For stake/unstake/withdraw operations, signature verification will be bypassed (as there’s no fundsKey). The contract itself will be responsible for invoking the stake contract under predefined conditions. *EDIT: (Signature will be verified only for the first stake operation that binds a contract to a specific provisioner key)*

Implemented here #3148 



Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index a75bc9dce5..49c2bda401 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,6 +3,7 @@ members = [
     # Test contracts
     "contracts/alice",
     "contracts/bob",
+    "contracts/charlie",
     "contracts/host_fn",
 
     # Genesis contracts
@@ -54,8 +55,8 @@ kadcast = "=0.7.0"
 phoenix-circuits = { version = "=0.4.0", default-features = false }
 phoenix-core = { version = "=0.32.0", default-features = false }
 # we leave piecrust open until a stable release is out
-piecrust = "0.26.1-rc.0"
-piecrust-uplink = "=0.17.1"
+piecrust = "=0.27.0-rc.0"
+piecrust-uplink = "0.17.2-rc.0"
 poseidon-merkle = "=0.7.0"
 
 # External dependencies
diff --git a/contracts/Makefile b/contracts/Makefile
index 107e39912b..8e320ae5e3 100644
--- a/contracts/Makefile
+++ b/contracts/Makefile
@@ -1,4 +1,4 @@
-SUBDIRS := alice bob transfer stake host_fn
+SUBDIRS := alice bob charlie transfer stake host_fn
 
 all: $(SUBDIRS) ## Build all the contracts
 
diff --git a/contracts/charlie/Cargo.toml b/contracts/charlie/Cargo.toml
new file mode 100644
index 0000000000..97a6341487
--- /dev/null
+++ b/contracts/charlie/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "charlie"
+version = "0.3.0"
+edition = "2021"
+description = "A proxy staking contract used to test delegation and intermediary functionality"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+execution-core = { workspace = true }
+rusk-abi = { workspace = true, features = ["abi", "dlmalloc"] }
+rkyv = { workspace = true, features = ["size_32"] }
diff --git a/contracts/charlie/LICENSE b/contracts/charlie/LICENSE
new file mode 100644
index 0000000000..a612ad9813
--- /dev/null
+++ b/contracts/charlie/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/contracts/charlie/Makefile b/contracts/charlie/Makefile
new file mode 100755
index 0000000000..7af6d95ce0
--- /dev/null
+++ b/contracts/charlie/Makefile
@@ -0,0 +1,18 @@
+all: wasm clippy
+
+wasm: ## Generate the optimized WASM for the contract given
+	@RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \
+    	cargo build \
+    		--release \
+    		--color=always \
+			-Z build-std=core,alloc \
+    		--target wasm32-unknown-unknown
+
+test:
+
+clippy:
+	@cargo clippy -Z build-std=core,alloc --release --target wasm32-unknown-unknown -- -D warnings
+
+doc:
+
+.PHONY: all test wasm
diff --git a/contracts/charlie/src/lib.rs b/contracts/charlie/src/lib.rs
new file mode 100644
index 0000000000..ad0a0f05cd
--- /dev/null
+++ b/contracts/charlie/src/lib.rs
@@ -0,0 +1,64 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) DUSK NETWORK. All rights reserved.
+
+#![no_std]
+
+mod state;
+use execution_core::transfer::TRANSFER_CONTRACT;
+use state::Charlie;
+
+#[no_mangle]
+static mut STATE: Charlie = Charlie;
+
+#[no_mangle]
+unsafe fn stake(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |stake| STATE.stake(stake))
+}
+
+#[no_mangle]
+unsafe fn unstake(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |withdraw| STATE.unstake(withdraw))
+}
+
+#[no_mangle]
+unsafe fn receive_unstake(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |withdraw| {
+        assert_transfer_caller();
+        // Assert is not called directly by "spend_and_execute"
+        // (it's supposed to be called by
+        // TRANSFER_CONTRACT::contract_to_contract ICC)
+        if rusk_abi::callstack().len() < 2 {
+            panic!("Cannot be called by a root ICC")
+        }
+        STATE.receive_unstake(withdraw)
+    })
+}
+
+#[no_mangle]
+unsafe fn withdraw(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |withdraw| STATE.withdraw(withdraw))
+}
+
+#[no_mangle]
+unsafe fn receive_reward(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |withdraw| {
+        assert_transfer_caller();
+        // Assert is not called directly by "spend_and_execute"
+        // (it's supposed to be called by
+        // TRANSFER_CONTRACT::contract_to_contract ICC)
+        if rusk_abi::callstack().len() < 2 {
+            panic!("Cannot be called by a root ICC")
+        }
+        STATE.receive_reward(withdraw)
+    })
+}
+
+fn assert_transfer_caller() {
+    const PANIC_MSG: &str = "Can only be called from the transfer contract";
+    if rusk_abi::caller().expect(PANIC_MSG) != TRANSFER_CONTRACT {
+        panic!("{PANIC_MSG}");
+    }
+}
diff --git a/contracts/charlie/src/state.rs b/contracts/charlie/src/state.rs
new file mode 100644
index 0000000000..1c6078aa33
--- /dev/null
+++ b/contracts/charlie/src/state.rs
@@ -0,0 +1,110 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) DUSK NETWORK. All rights reserved.
+
+extern crate alloc;
+
+use execution_core::stake::{
+    Stake, Withdraw, WithdrawToContract, STAKE_CONTRACT,
+};
+use execution_core::transfer::{
+    withdraw::Withdraw as TransferWithdraw, ContractToContract,
+    ReceiveFromContract, TRANSFER_CONTRACT,
+};
+
+const SCRATCH_BUF_BYTES: usize = 256;
+
+/// Charlie contract.
+#[derive(Debug, Clone)]
+pub struct Charlie;
+impl Charlie {
+    pub fn stake(&mut self, stake: Stake) {
+        let value = stake.value();
+        let data = rkyv::to_bytes::<_, SCRATCH_BUF_BYTES>(&stake)
+            .expect("stake to be rkyv serialized")
+            .to_vec();
+
+        // make call to transfer contract to transfer balance from the user to
+        // this contract
+        let _: () = rusk_abi::call(TRANSFER_CONTRACT, "deposit", &value)
+            .expect("Depositing funds into contract should succeed");
+
+        let contract_to_contract = ContractToContract {
+            contract: STAKE_CONTRACT,
+            value,
+            data,
+            fn_name: "stake_from_contract".into(),
+        };
+
+        let _: () = rusk_abi::call(
+            TRANSFER_CONTRACT,
+            "contract_to_contract",
+            &contract_to_contract,
+        )
+        .expect("Transferring to stake contract should succeed");
+    }
+
+    pub fn unstake(&mut self, unstake: Withdraw) {
+        let value = unstake.transfer_withdraw().value();
+        let data =
+            rkyv::to_bytes::<_, SCRATCH_BUF_BYTES>(unstake.transfer_withdraw())
+                .expect("withdraw to be rkyv serialized")
+                .to_vec();
+
+        let withdraw_to_contract = WithdrawToContract::new(
+            *unstake.account(),
+            value,
+            "receive_unstake",
+        )
+        .with_data(data);
+
+        let _: () = rusk_abi::call(
+            STAKE_CONTRACT,
+            "unstake_from_contract",
+            &withdraw_to_contract,
+        )
+        .expect("Unstake from stake contract should succeed");
+    }
+
+    pub fn receive_unstake(&mut self, receive: ReceiveFromContract) {
+        let withdraw: TransferWithdraw = rkyv::from_bytes(&receive.data)
+            .expect("withdraw to be rkyv deserialized");
+        // make call to the transfer contract to withdraw funds from this
+        // contract into the receiver specified by the withdrawal.
+        let _: () = rusk_abi::call(TRANSFER_CONTRACT, "withdraw", &withdraw)
+            .expect("Withdrawing stake should succeed");
+    }
+
+    pub fn withdraw(&mut self, unstake: Withdraw) {
+        let value = unstake.transfer_withdraw().value();
+        let data =
+            rkyv::to_bytes::<_, SCRATCH_BUF_BYTES>(unstake.transfer_withdraw())
+                .expect("withdraw to be rkyv serialized")
+                .to_vec();
+
+        let withdraw_to_contract = WithdrawToContract::new(
+            *unstake.account(),
+            value,
+            "receive_reward",
+        )
+        .with_data(data);
+
+        let _: () = rusk_abi::call(
+            STAKE_CONTRACT,
+            "withdraw_from_contract",
+            &withdraw_to_contract,
+        )
+        .expect("Withdraw rewards from stake contract should succeed");
+    }
+
+    pub fn receive_reward(&mut self, receive: ReceiveFromContract) {
+        let withdraw: TransferWithdraw = rkyv::from_bytes(&receive.data)
+            .expect("withdraw to be rkyv deserialized");
+        // make call to the transfer contract to withdraw funds from this
+        // contract into the receiver specified by the withdrawal.
+        let _: () = rusk_abi::call(TRANSFER_CONTRACT, "withdraw", &withdraw)
+            .expect("Withdrawing stake should succeed");
+    }
+}
diff --git a/contracts/stake/Cargo.toml b/contracts/stake/Cargo.toml
index ec39665f95..7e0222b87d 100644
--- a/contracts/stake/Cargo.toml
+++ b/contracts/stake/Cargo.toml
@@ -9,6 +9,7 @@ crate-type = ["cdylib", "rlib"]
 [dependencies]
 execution-core = { workspace = true }
 dusk-bytes = { workspace = true }
+rkyv = { workspace = true, features = ["size_32"] }
 
 [target.'cfg(target_family = "wasm")'.dependencies]
 rusk-abi = { workspace = true, features = ["abi", "dlmalloc"] }
@@ -17,7 +18,6 @@ rusk-abi = { workspace = true, features = ["abi", "dlmalloc"] }
 rusk-abi = { workspace = true, features = ["host"] }
 execution-core = { workspace = true, features = ["zk"] }
 rusk-prover = { workspace = true }
-rkyv = { workspace = true, features = ["size_32"] }
 rand = { workspace = true }
 ff = { workspace = true }
 criterion = { workspace = true }
diff --git a/contracts/stake/src/lib.rs b/contracts/stake/src/lib.rs
index 64535c51be..dd38a86d15 100644
--- a/contracts/stake/src/lib.rs
+++ b/contracts/stake/src/lib.rs
@@ -45,6 +45,33 @@ unsafe fn withdraw(arg_len: u32) -> u32 {
     })
 }
 
+#[no_mangle]
+unsafe fn stake_from_contract(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |receive| {
+        // Assert is called from the transfer contract
+        assert_transfer_caller();
+        // Assert is not called directly by "spend_and_execute"
+        // (it's supposed to be called by
+        // TRANSFER_CONTRACT::contract_to_contract ICC)
+        if rusk_abi::callstack().len() < 2 {
+            panic!("Cannot be called by a root ICC")
+        }
+        STATE.stake_from_contract(receive)
+    })
+}
+
+#[no_mangle]
+unsafe fn unstake_from_contract(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |unstake| STATE.unstake_from_contract(unstake))
+}
+
+#[no_mangle]
+unsafe fn withdraw_from_contract(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |rewards| {
+        STATE.withdraw_from_contract(rewards)
+    })
+}
+
 // Queries
 
 #[no_mangle]
diff --git a/contracts/stake/src/state.rs b/contracts/stake/src/state.rs
index 25f5b925aa..4cddaef655 100644
--- a/contracts/stake/src/state.rs
+++ b/contracts/stake/src/state.rs
@@ -15,10 +15,11 @@ use execution_core::{
     signatures::bls::PublicKey as BlsPublicKey,
     stake::{
         next_epoch, Reward, SlashEvent, Stake, StakeAmount, StakeData,
-        StakeEvent, StakeKeys, Withdraw, EPOCH, MINIMUM_STAKE, STAKE_CONTRACT,
-        STAKE_WARNINGS,
+        StakeEvent, StakeFundOwner, StakeKeys, Withdraw, WithdrawToContract,
+        EPOCH, MINIMUM_STAKE, STAKE_CONTRACT, STAKE_WARNINGS,
     },
-    transfer::TRANSFER_CONTRACT,
+    transfer::{ContractToContract, ReceiveFromContract, TRANSFER_CONTRACT},
+    ContractId,
 };
 
 use crate::*;
@@ -54,6 +55,30 @@ impl StakeState {
         self.previous_block_state.clear()
     }
 
+    fn unwrap_account_owner(owner: &StakeFundOwner) -> BlsPublicKey {
+        match owner {
+            StakeFundOwner::Account(public_key) => {
+                assert!(
+                    public_key.is_valid(),
+                    "Specified owner key is not valid"
+                );
+                *public_key
+            }
+            StakeFundOwner::Contract(_) => {
+                panic!("expect StakeFundOwner::Account")
+            }
+        }
+    }
+
+    fn unwrap_contract_owner(owner: &StakeFundOwner) -> &ContractId {
+        match owner {
+            StakeFundOwner::Account(_) => {
+                panic!("expect StakeFundOwner::Contract")
+            }
+            StakeFundOwner::Contract(id) => id,
+        }
+    }
+
     pub fn stake(&mut self, stake: Stake) {
         let value = stake.value();
         let signature = *stake.signature();
@@ -70,11 +95,13 @@ impl StakeState {
             panic!("The staked value is lower than the minimum amount!");
         }
 
-        let digest = stake.signature_message().to_vec();
-        if !rusk_abi::verify_bls(digest.clone(), keys.funds, signature.funds) {
-            panic!("Invalid funds signature!");
+        let owner = Self::unwrap_account_owner(&keys.owner);
+
+        let msg = stake.signature_message().to_vec();
+        if !rusk_abi::verify_bls(msg.clone(), owner, signature.owner) {
+            panic!("Invalid owner signature!");
         }
-        if !rusk_abi::verify_bls(digest, keys.account, signature.account) {
+        if !rusk_abi::verify_bls(msg, keys.account, signature.account) {
             panic!("Invalid account signature!");
         }
 
@@ -112,6 +139,64 @@ impl StakeState {
             .or_insert((prev_stake, account));
     }
 
+    pub fn stake_from_contract(&mut self, recv: ReceiveFromContract) {
+        let stake: Stake =
+            rkyv::from_bytes(&recv.data).expect("Invalid stake received");
+        let value = stake.value();
+
+        if stake.chain_id() != self.chain_id() {
+            panic!("The stake must target the correct chain");
+        }
+
+        let account = stake.keys().account;
+        let prev_stake = self.get_stake(&stake.keys().account).copied();
+        let (loaded_stake, keys) = self.load_or_create_stake_mut(stake.keys());
+
+        let contract = Self::unwrap_contract_owner(&keys.owner);
+        assert!(contract == &recv.contract, "Invalid contract caller");
+        assert!(value == recv.value, "Stake amount mismatch");
+
+        if loaded_stake.amount.is_none() {
+            if value < MINIMUM_STAKE {
+                panic!("The staked value is lower than the minimum amount!");
+            }
+
+            // We verify the signature only when there is a new stake
+            let signature = stake.signature().account;
+            let msg = stake.signature_message().to_vec();
+            if !rusk_abi::verify_bls(msg, account, signature) {
+                panic!("Invalid account signature!");
+            }
+        }
+
+        let block_height = rusk_abi::block_height();
+        // update the state accordingly
+        let stake_event = match &mut loaded_stake.amount {
+            Some(amount) => {
+                let locked = if block_height >= amount.eligibility {
+                    value / 10
+                } else {
+                    // No penalties applied if the stake is not eligible yet
+                    0
+                };
+                let value = value - locked;
+                amount.locked += locked;
+                amount.value += value;
+                StakeEvent::new(*keys, value).locked(locked)
+            }
+            amount => {
+                let _ = amount.insert(StakeAmount::new(value, block_height));
+                StakeEvent::new(*keys, value)
+            }
+        };
+        rusk_abi::emit("stake", stake_event);
+
+        let key = keys.account.to_bytes();
+        self.previous_block_state
+            .entry(key)
+            .or_insert((prev_stake, account));
+    }
+
     pub fn unstake(&mut self, unstake: Withdraw) {
         let transfer_withdraw = unstake.transfer_withdraw();
         let account = *unstake.account();
@@ -134,12 +219,14 @@ impl StakeState {
             panic!("Value to unstake higher than the staked amount");
         }
 
+        let owner = Self::unwrap_account_owner(&keys.owner);
+
         // check signature is correct
-        let digest = unstake.signature_message();
-        if !rusk_abi::verify_bls(digest.clone(), keys.funds, signature.funds) {
-            panic!("Invalid funds signature!");
+        let msg = unstake.signature_message();
+        if !rusk_abi::verify_bls(msg.clone(), owner, signature.owner) {
+            panic!("Invalid owner signature!");
         }
-        if !rusk_abi::verify_bls(digest, keys.account, signature.account) {
+        if !rusk_abi::verify_bls(msg, keys.account, signature.account) {
             panic!("Invalid account signature!");
         }
 
@@ -177,6 +264,79 @@ impl StakeState {
             .or_insert((prev_stake, account));
     }
 
+    pub fn unstake_from_contract(&mut self, unstake: WithdrawToContract) {
+        let account = unstake.account();
+        let value = unstake.value();
+        let data = unstake.data().to_vec();
+
+        let (loaded_stake, keys) = self
+            .get_stake_mut(account)
+            .expect("A stake should exist in the map to be unstaked!");
+        let prev_stake = Some(*loaded_stake);
+
+        // ensure there is a value staked, and that the withdrawal is not
+        // greater than the available funds
+        let stake = loaded_stake
+            .amount
+            .as_mut()
+            .expect("There must be an amount to unstake");
+
+        if value > stake.total_funds() {
+            panic!("Value to unstake higher than the staked amount");
+        }
+
+        let owner = Self::unwrap_contract_owner(&keys.owner);
+        let caller =
+            rusk_abi::caller().expect("unstake must be called by a contract");
+        assert!(&caller == owner, "Invalid contract caller");
+
+        let to_contract = ContractToContract {
+            contract: caller,
+            fn_name: unstake.fn_name().into(),
+            value,
+            data,
+        };
+
+        let _: () = rusk_abi::call(
+            TRANSFER_CONTRACT,
+            "contract_to_contract",
+            &to_contract,
+        )
+        .expect("Unstaking to contract should succeed");
+
+        let stake_event = if value > stake.value {
+            let from_locked = value - stake.value;
+            let from_stake = stake.value;
+            stake.value = 0;
+            stake.locked -= from_locked;
+            StakeEvent::new(*keys, from_stake).locked(from_locked)
+        } else {
+            stake.value -= value;
+            StakeEvent::new(*keys, value)
+        };
+
+        rusk_abi::emit("unstake", stake_event);
+        if stake.total_funds() == 0 {
+            // update the state accordingly
+            loaded_stake.amount = None;
+            if loaded_stake.reward == 0 {
+                self.stakes.remove(&unstake.account().to_bytes());
+            }
+        }
+        // Note: We no longer enforce the minimum stake condition here to
+        // avoid locked funds exploit for contracts.
+        /*
+            } else if stake.total_funds() < MINIMUM_STAKE {
+                panic!("Stake left is lower than minimum stake");
+        }
+        */
+
+        let key = account.to_bytes();
+        self.previous_block_state
+            .entry(key)
+            .or_insert_with(|| (prev_stake, *account));
+    }
+
     pub fn withdraw(&mut self, withdraw: Withdraw) {
         let transfer_withdraw = withdraw.transfer_withdraw();
         let account = withdraw.account();
@@ -198,12 +358,14 @@ impl StakeState {
             panic!("Value to withdraw is higher than available reward");
         }
 
+        let owner = Self::unwrap_account_owner(&keys.owner);
+
         // check signature is correct
-        let digest = withdraw.signature_message();
-        if !rusk_abi::verify_bls(digest.clone(), keys.funds, signature.funds) {
-            panic!("Invalid funds signature!");
+        let msg = withdraw.signature_message();
+        if !rusk_abi::verify_bls(msg.clone(), owner, signature.owner) {
+            panic!("Invalid owner signature!");
         }
-        if !rusk_abi::verify_bls(digest, keys.account, signature.account) {
+        if !rusk_abi::verify_bls(msg, keys.account, signature.account) {
             panic!("Invalid account signature!");
         }
 
@@ -222,6 +384,51 @@ impl StakeState {
         }
     }
 
+    pub fn withdraw_from_contract(&mut self, withdraw: WithdrawToContract) {
+        let account = withdraw.account();
+        let value = withdraw.value();
+        let data = withdraw.data().to_vec();
+
+        let (loaded_stake, keys) = self
+            .get_stake_mut(account)
+            .expect("A stake should exist in the map to get rewards!");
+
+        // ensure no 0 reward is executed,
+        if value == 0 {
+            panic!("Withdrawing 0 reward is not allowed");
+        }
+
+        // ensure that the withdrawal amount is not greater than the current
+        // reward
+        if value > loaded_stake.reward {
+            panic!("Value to withdraw is higher than available reward");
+        }
+
+        let owner = Self::unwrap_contract_owner(&keys.owner);
+        let caller =
+            rusk_abi::caller().expect("unstake must be called by a contract");
+        assert!(&caller == owner, "Invalid contract caller");
+
+        let to_contract = ContractToContract {
+            contract: caller,
+            fn_name: withdraw.fn_name().into(),
+            value,
+            data,
+        };
+
+        let _: () =
+            rusk_abi::call(TRANSFER_CONTRACT, "mint_to_contract", &to_contract)
+                .expect("Withdrawing reward to contract should succeed");
+
+        // update the state accordingly
+        loaded_stake.reward -= value;
+        rusk_abi::emit("withdraw", StakeEvent::new(*keys, value));
+
+        if loaded_stake.reward == 0 && loaded_stake.amount.is_none() {
+            self.stakes.remove(&account.to_bytes());
+        }
+    }
+
     /// Gets a reference to a stake.
     pub fn get_stake(&self, key: &BlsPublicKey) -> Option<&StakeData> {
         self.stakes.get(&key.to_bytes()).map(|(s, _)| s)
@@ -261,28 +468,33 @@ impl StakeState {
         self.stakes
             .entry(key)
             .and_modify(|(_, loaded_keys)| {
-                assert_eq!(keys, loaded_keys, "Keys mismatch")
+                assert!(keys == loaded_keys, "Keys mismatch")
             })
             .or_insert_with(|| (StakeData::EMPTY, *keys))
     }
 
-    /// Rewards a `account` with the given `value`.
+    /// Rewards multiple accounts with the given rewards.
     ///
-    /// *PANIC* If a stake does not exist in the map
+    /// If a stake does not exist in the map, it is skipped.
     pub fn reward(&mut self, rewards: Vec<Reward>) {
         for reward in &rewards {
-            let (stake, _) = self
-                .get_stake_mut(&reward.account)
-                .expect("Stake to exists to be rewarded");
-
-            // Reset faults counters
-            stake.faults = 0;
-            stake.hard_faults = 0;
+            let stake =
+                if let Some((stake, _)) = self.get_stake_mut(&reward.account) {
+                    // Reset faults counters
+                    stake.faults = 0;
+                    stake.hard_faults = 0;
+                    stake
+                } else {
+                    let keys = StakeKeys::single_key(reward.account);
+                    let (stake, _) = self.load_or_create_stake_mut(&keys);
+                    stake
+                };
 
             stake.reward += reward.value;
         }
-
-        rusk_abi::emit("reward", rewards);
+        if !rewards.is_empty() {
+            rusk_abi::emit("reward", rewards);
+        }
     }
 
     /// Total amount burned since the genesis
diff --git a/contracts/transfer/src/lib.rs b/contracts/transfer/src/lib.rs
index 7840f0c495..03dfdc1871 100644
--- a/contracts/transfer/src/lib.rs
+++ b/contracts/transfer/src/lib.rs
@@ -30,6 +30,11 @@ unsafe fn mint(arg_len: u32) -> u32 {
     rusk_abi::wrap_call(arg_len, |arg| STATE.mint(arg))
 }
 
+#[no_mangle]
+unsafe fn mint_to_contract(arg_len: u32) -> u32 {
+    rusk_abi::wrap_call(arg_len, |arg| STATE.mint_to_contract(arg))
+}
+
 #[no_mangle]
 unsafe fn deposit(arg_len: u32) -> u32 {
     rusk_abi::wrap_call(arg_len, |arg| STATE.deposit(arg))
diff --git a/contracts/transfer/src/state.rs b/contracts/transfer/src/state.rs
index dd4ce5af83..b2873503be 100644
--- a/contracts/transfer/src/state.rs
+++ b/contracts/transfer/src/state.rs
@@ -13,6 +13,7 @@ use alloc::collections::{BTreeMap, BTreeSet};
 use alloc::vec::Vec;
 
 use execution_core::stake::EPOCH;
+use execution_core::transfer::MINT_CONTRACT_TOPIC;
 use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
 
 use execution_core::{
@@ -181,6 +182,43 @@ impl TransferState {
         rusk_abi::emit(MINT_TOPIC, WithdrawEvent::from(mint));
     }
 
+    /// Mint more Dusk to be owned by a contract.
+    ///
+    /// This can only be called by the stake contract, and will increase the
+    /// total amount of circulating Dusk. It is intended to be called during the
+    /// execution of the `withdraw_to_contract` function, and the amount minted
+    /// should conform to the consensus emission schedule.
+    ///
+    /// # Safety
+    /// We assume on trust that the value sent by the stake contract is
+    /// according to consensus rules.
+    pub fn mint_to_contract(&mut self, mint: ContractToContract) {
+        const PANIC_MSG: &str = "Can only be called by the stake contract";
+        let caller = rusk_abi::caller().expect(PANIC_MSG);
+        assert_eq!(caller, STAKE_CONTRACT, "{PANIC_MSG}");
+
+        let receiver_balance =
+            self.contract_balances.entry(mint.contract).or_insert(0);
+
+        *receiver_balance += mint.value;
+
+        let receive = ReceiveFromContract {
+            contract: STAKE_CONTRACT,
+            value: mint.value,
+            data: mint.data,
+        };
+
+        rusk_abi::call::<_, ()>(mint.contract, &mint.fn_name, &receive)
+            .expect("Calling receiver should succeed");
+
+        let mint_event = ContractToContractEvent {
+            sender: STAKE_CONTRACT,
+            receiver: mint.contract,
+            value: mint.value,
+        };
+        rusk_abi::emit(MINT_CONTRACT_TOPIC, mint_event);
+    }
+
     /// Withdraw from a contract's balance to a Phoenix note or a Moonlight
     /// account.
     ///
diff --git a/execution-core/src/stake.rs b/execution-core/src/stake.rs
index bc6355b758..a999606023 100644
--- a/execution-core/src/stake.rs
+++ b/execution-core/src/stake.rs
@@ -6,10 +6,12 @@
 
 //! Types used by Dusk's stake contract.
 
+use alloc::string::String;
 use alloc::vec::Vec;
 
 use bytecheck::CheckBytes;
 use dusk_bytes::Serializable;
+use piecrust_uplink::CONTRACT_ID_BYTES;
 use rkyv::{Archive, Deserialize, Serialize};
 
 use crate::signatures::bls::{
@@ -54,11 +56,37 @@ impl Stake {
     pub fn new(sk: &BlsSecretKey, value: u64, chain_id: u8) -> Self {
         let key = BlsPublicKey::from(sk);
 
-        let keys = StakeKeys {
-            account: key,
-            funds: key,
+        let keys = StakeKeys::single_key(key);
+
+        let mut stake = Stake {
+            chain_id,
+            keys,
+            value,
+            signature: DoubleSignature::default(),
+        };
+
+        let msg = stake.signature_message();
+
+        stake.signature = DoubleSignature {
+            account: sk.sign(&msg),
+            owner: sk.sign(&msg),
         };
 
+        stake
+    }
+
+    /// Create a new stake.
+    #[must_use]
+    pub fn new_from_contract(
+        sk: &BlsSecretKey,
+        contract: ContractId,
+        value: u64,
+        chain_id: u8,
+    ) -> Self {
+        let key = BlsPublicKey::from(sk);
+
+        let keys = StakeKeys::new(key, contract);
+
         let mut stake = Stake {
             chain_id,
             keys,
@@ -70,7 +98,7 @@ impl Stake {
 
         stake.signature = DoubleSignature {
             account: sk.sign(&msg),
-            funds: sk.sign(&msg),
+            owner: sk.sign(&msg),
         };
 
         stake
@@ -118,8 +146,15 @@ impl Stake {
             .copy_from_slice(&self.keys.account.to_bytes());
         offset += BlsPublicKey::SIZE;
 
-        bytes[offset..offset + BlsPublicKey::SIZE]
-            .copy_from_slice(&self.keys.funds.to_bytes());
+        match &self.keys.owner {
+            StakeFundOwner::Account(key) => bytes
+                [offset..offset + BlsPublicKey::SIZE]
+                .copy_from_slice(&key.to_bytes()),
+            StakeFundOwner::Contract(contract_id) => bytes
+                [offset..offset + CONTRACT_ID_BYTES]
+                .copy_from_slice(&contract_id.to_bytes()),
+        }
+
         offset += BlsPublicKey::SIZE;
 
         bytes[offset..offset + u64::SIZE]
@@ -129,6 +164,79 @@ impl Stake {
     }
 }
 
+/// Withdraw some value from the stake contract to a smart contract
+///
+/// This struct contains the information necessary to perform the withdrawal,
+/// including the account initiating the withdrawal, the amount being withdrawn,
+/// and the name of the function to invoke on the target contract.
+#[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize)]
+#[archive_attr(derive(CheckBytes))]
+pub struct WithdrawToContract {
+    account: BlsPublicKey,
+    value: u64,
+    fn_name: String,
+    data: Vec<u8>,
+}
+
+impl WithdrawToContract {
+    /// Creates a new `WithdrawToContract` instance.
+    ///
+    /// # Parameters
+    /// - `account`: The BLS public key of the account initiating the
+    ///   withdrawal.
+    /// - `value`: The amount of the withdrawal in LUX
+    /// - `fn_name`: The name of the function to invoke on the target contract
+    ///   as callback for the `contract_to_contract` method.
+    ///
+    /// # Returns
+    /// A new `WithdrawToContract` instance with the specified account, value,
+    /// and function name.
+    pub fn new(
+        account: BlsPublicKey,
+        value: u64,
+        fn_name: impl Into<String>,
+    ) -> Self {
+        Self {
+            account,
+            value,
+            fn_name: fn_name.into(),
+            data: Vec::new(),
+        }
+    }
+
+    /// Returns the account initiating the withdrawal.
+    #[must_use]
+    pub fn account(&self) -> &BlsPublicKey {
+        &self.account
+    }
+
+    /// Returns the value (amount) of the withdrawal.
+    #[must_use]
+    pub fn value(&self) -> u64 {
+        self.value
+    }
+
+    /// Returns the name of the callback function to invoke on the target
+    /// contract.
+    #[must_use]
+    pub fn fn_name(&self) -> &str {
+        &self.fn_name
+    }
+
+    /// Returns the data to be passed to the target contract.
+    #[must_use]
+    pub fn data(&self) -> &[u8] {
+        &self.data
+    }
+
+    /// Set the data to be passed to the target contract.
+    #[must_use]
+    pub fn with_data(mut self, data: Vec<u8>) -> Self {
+        self.data = data;
+        self
+    }
+}
+
 /// Withdraw some value from the stake contract.
 ///
 /// This is used in both `unstake` and `withdraw`.
@@ -155,7 +263,7 @@ impl Withdraw {
 
         stake_withdraw.signature = DoubleSignature {
             account: sk.sign(&msg),
-            funds: sk.sign(&msg),
+            owner: sk.sign(&msg),
         };
 
         stake_withdraw
@@ -281,10 +389,81 @@ pub struct StakeData {
 )]
 #[archive_attr(derive(CheckBytes))]
 pub struct StakeKeys {
-    /// Key used for the consensus
+    /// Key used for consensus operations, such as voting or producing blocks.
     pub account: BlsPublicKey,
-    /// Key used for handle funds (stake/unstake/withdraw)
-    pub funds: BlsPublicKey,
+
+    /// Key used to manage funds for operations like staking, unstaking, or
+    /// withdrawing.
+    ///
+    /// This field can represent ownership either through an individual account
+    /// (`StakeFundOwner::Account`) or through a smart contract
+    /// (`StakeFundOwner::Contract`).
+    pub owner: StakeFundOwner,
+}
+
+impl StakeKeys {
+    /// Creates a new `StakeKeys` instance where both the consensus key and the
+    /// owner key are set to the same account key.
+    #[must_use]
+    pub fn single_key(account: BlsPublicKey) -> Self {
+        Self::new(account, account)
+    }
+
+    /// Creates a new `StakeKeys` instance with the specified account and funds
+    /// owner.
+    ///
+    /// # Parameters
+    /// - `account`: The BLS public key used for consensus operations, such as
+    ///   voting or producing blocks.
+    /// - `owner`: The owner of the funds, which can be either an account or a
+    ///   contract. This parameter is any type that implements
+    ///   `Into<StakeFundOwner>`, allowing flexibility in how the funds owner is
+    ///   specified.
+    #[must_use]
+    pub fn new<F: Into<StakeFundOwner>>(
+        account: BlsPublicKey,
+        owner: F,
+    ) -> Self {
+        let owner = owner.into();
+        Self { account, owner }
+    }
+}
+
+/// Data used to check ownership of funds
+#[derive(
+    Debug, Clone, Copy, PartialEq, Eq, Archive, Deserialize, Serialize,
+)]
+#[archive_attr(derive(CheckBytes))]
+pub enum StakeFundOwner {
+    /// Represents an account-based owner identified by a BLS public key.
+    ///
+    /// This variant is used when the stake funds are associated directly with
+    /// an individual account.
+    Account(BlsPublicKey),
+
+    /// Represents a contract-based owner identified by a contract ID.
+    ///
+    /// This variant is used when the stake funds are managed or associated
+    /// with a specific smart contract.
+    Contract(ContractId),
+}
+
+impl Default for StakeFundOwner {
+    fn default() -> Self {
+        BlsPublicKey::default().into()
+    }
+}
+
+impl From<BlsPublicKey> for StakeFundOwner {
+    fn from(value: BlsPublicKey) -> Self {
+        Self::Account(value)
+    }
+}
+
+impl From<ContractId> for StakeFundOwner {
+    fn from(value: ContractId) -> Self {
+        Self::Contract(value)
+    }
 }
 
 /// Signature used to handle funds (stake/unstake/withdraw)
@@ -295,8 +474,8 @@ pub struct StakeKeys {
 pub struct DoubleSignature {
     /// Signature created with the account key
     pub account: BlsSignature,
-    /// Signature created with the funds key
-    pub funds: BlsSignature,
+    /// Signature created with the owner key
+    pub owner: BlsSignature,
 }
 
 impl StakeData {
diff --git a/execution-core/src/transfer.rs b/execution-core/src/transfer.rs
index 36541597e6..c61ce0d30c 100644
--- a/execution-core/src/transfer.rs
+++ b/execution-core/src/transfer.rs
@@ -58,6 +58,8 @@ pub const DEPOSIT_TOPIC: &str = "deposit";
 pub const CONVERT_TOPIC: &str = "convert";
 /// Topic for the mint event.
 pub const MINT_TOPIC: &str = "mint";
+/// Topic for the mint to contract event.
+pub const MINT_CONTRACT_TOPIC: &str = "mint_c";
 
 /// The transaction used by the transfer contract.
 #[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
diff --git a/rusk-abi/src/lib.rs b/rusk-abi/src/lib.rs
index e7ead6945f..ab8a29ca4c 100644
--- a/rusk-abi/src/lib.rs
+++ b/rusk-abi/src/lib.rs
@@ -42,6 +42,7 @@ pub use piecrust_uplink::{
     call_raw_with_limit,
     call_with_limit,
     caller,
+    callstack,
     emit,
     emit_raw,
     feed,
diff --git a/rusk-recovery/src/state.rs b/rusk-recovery/src/state.rs
index 5ef81ddf78..5c617824b6 100644
--- a/rusk-recovery/src/state.rs
+++ b/rusk-recovery/src/state.rs
@@ -10,7 +10,9 @@ use std::path::Path;
 
 use dusk_bytes::DeserializableSlice;
 use execution_core::signatures::bls::PublicKey as AccountPublicKey;
-use execution_core::stake::{StakeAmount, StakeData, STAKE_CONTRACT};
+use execution_core::stake::{
+    StakeAmount, StakeData, StakeKeys, STAKE_CONTRACT,
+};
 use execution_core::transfer::phoenix::{Note, PublicKey, Sender};
 use execution_core::transfer::TRANSFER_CONTRACT;
 use execution_core::{ContractId, JubJubScalar};
@@ -208,8 +210,7 @@ fn generate_empty_state<P: AsRef<Path>>(
             STAKE_CONTRACT,
             "insert_stake",
             &(
-                *DUSK_CONSENSUS_KEY,
-                *DUSK_CONSENSUS_KEY,
+                StakeKeys::single_key(*DUSK_CONSENSUS_KEY),
                 StakeData::default(),
             ),
             u64::MAX,
diff --git a/rusk-recovery/src/state/snapshot/stake.rs b/rusk-recovery/src/state/snapshot/stake.rs
index 503c449c84..367b78f252 100644
--- a/rusk-recovery/src/state/snapshot/stake.rs
+++ b/rusk-recovery/src/state/snapshot/stake.rs
@@ -26,9 +26,6 @@ impl GenesisStake {
     }
 
     pub fn to_stake_keys(&self) -> StakeKeys {
-        StakeKeys {
-            account: *self.address(),
-            funds: *self.address(),
-        }
+        StakeKeys::single_key(*self.address())
     }
 }
diff --git a/rusk/Makefile b/rusk/Makefile
index ab6fea8954..0f4cc3cbf1 100644
--- a/rusk/Makefile
+++ b/rusk/Makefile
@@ -72,6 +72,7 @@ recovery-state: ## Build network state
 		--features recovery-state \
 		--release \
 		-- recovery state \
+		--force \
 		--init $(INITFILE)
 
 .PHONY: test help clippy build build-bench bench recovery-keys recovery-state rusk


Test patch:
diff --git a/contracts/stake/tests/events.rs b/contracts/stake/tests/events.rs
index 9756dc4fb2..66c3204f2c 100644
--- a/contracts/stake/tests/events.rs
+++ b/contracts/stake/tests/events.rs
@@ -42,10 +42,7 @@ fn reward_slash() -> Result<(), PiecrustError> {
 
     let stake_sk = BlsSecretKey::random(rng);
     let stake_pk = BlsPublicKey::from(&stake_sk);
-    let stake_pks = StakeKeys {
-        account: stake_pk,
-        funds: stake_pk,
-    };
+    let stake_pks = StakeKeys::single_key(stake_pk);
 
     let mut session = instantiate(rng, vm, &pk, GENESIS_VALUE);
 
@@ -135,10 +132,7 @@ fn stake_hard_slash() -> Result<(), PiecrustError> {
 
     let stake_sk = BlsSecretKey::random(rng);
     let stake_pk = BlsPublicKey::from(&stake_sk);
-    let stake_pks = StakeKeys {
-        account: stake_pk,
-        funds: stake_pk,
-    };
+    let stake_pks = StakeKeys::single_key(stake_pk);
 
     let mut session = instantiate(rng, vm, &pk, GENESIS_VALUE);
 
diff --git a/rusk/tests/common/state.rs b/rusk/tests/common/state.rs
index 1a889cf817..79e7f964e9 100644
--- a/rusk/tests/common/state.rs
+++ b/rusk/tests/common/state.rs
@@ -208,6 +208,7 @@ pub fn generator_procedure2(
     block_gas_limit: u64,
     missed_generators: Vec<BlsPublicKey>,
     expected: Option<ExecuteResult>,
+    generator: Option<BlsPublicKey>,
 ) -> anyhow::Result<(Vec<SpentTransaction>, [u8; 32])> {
     let expected = expected.unwrap_or(ExecuteResult {
         executed: txs.len(),
@@ -219,7 +220,8 @@ pub fn generator_procedure2(
         rusk.preverify(tx)?;
     }
 
-    let generator_pubkey = node_data::bls::PublicKey::new(*DUSK_CONSENSUS_KEY);
+    let generator = generator.unwrap_or(*DUSK_CONSENSUS_KEY);
+    let generator_pubkey = node_data::bls::PublicKey::new(generator);
     let generator_pubkey_bytes = *generator_pubkey.bytes();
     let round = block_height;
 
diff --git a/rusk/tests/config/stake_from_contract.toml b/rusk/tests/config/stake_from_contract.toml
new file mode 100644
index 0000000000..cf0ffb2a4b
--- /dev/null
+++ b/rusk/tests/config/stake_from_contract.toml
@@ -0,0 +1,28 @@
+[[phoenix_balance]]
+address = "ivmscertKgRyX8wNMJJsQcSVEyPsfSMUQXSAgeAPQXsndqFq9Pmknzhm61QvcEEdxPaGgxDS4RHpb6KKccrnSKN"
+seed = 57005
+notes = [
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+    10_000_000_000_000,
+]
+
+[[moonlight_account]]
+address = "qe1FbZxf6YaCAeFNSvL1G82cBhG4Q4gBf4vKYo527Vws3b23jdbBuzKSFsdUHnZeBgsTnyNJLkApEpRyJw87sdzR9g9iESJrG5ZgpCs9jq88m6d4qMY5txGpaXskRQmkzE3"
+balance = 1000_000_000_000_000
+
+# [[stake]]
+# address = "qe1FbZxf6YaCAeFNSvL1G82cBhG4Q4gBf4vKYo527Vws3b23jdbBuzKSFsdUHnZeBgsTnyNJLkApEpRyJw87sdzR9g9iESJrG5ZgpCs9jq88m6d4qMY5txGpaXskRQmkzE3"
+# amount = 1_000_000_000_000
+
+[[stake]]
+address = "25omWWRyfcMjYNbQZVyc3rypYLi8UqZuthoJHqbCEriRX3z2EmnBaXWZLFL2NvzvDnkoYoHLGiSYQpmupNJj1sSdWNstqzfFEpiqvSNYw7gqvoEiU9FsEHUMG1ZyG3XgL8Rv"
+amount = 1_000_000_000_000
+reward = 1_000_000_000_000
diff --git a/rusk/tests/services/contract_stake.rs b/rusk/tests/services/contract_stake.rs
new file mode 100644
index 0000000000..43038d9e07
--- /dev/null
+++ b/rusk/tests/services/contract_stake.rs
@@ -0,0 +1,288 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+//
+// Copyright (c) DUSK NETWORK. All rights reserved.
+
+use std::path::Path;
+use std::sync::{Arc, RwLock};
+
+use execution_core::stake::{self, Stake, EPOCH, MINIMUM_STAKE};
+
+use dusk_bytes::Serializable;
+use execution_core::transfer::data::ContractCall;
+use execution_core::transfer::{self, Transaction};
+use execution_core::ContractId;
+use node_data::ledger::SpentTransaction;
+use rand::prelude::*;
+use rand::rngs::StdRng;
+use rusk::gen_id::gen_contract_id;
+use rusk::{Result, Rusk};
+use std::collections::HashMap;
+use tempfile::tempdir;
+use test_wallet::{self as wallet};
+use tracing::info;
+
+use crate::common::state::{generator_procedure2, new_state};
+use crate::common::wallet::{TestStateClient, TestStore};
+use crate::common::*;
+
+const BLOCK_HEIGHT: u64 = 1;
+const BLOCK_GAS_LIMIT: u64 = 100_000_000_000;
+const GAS_LIMIT: u64 = 10_000_000_000;
+const GAS_PRICE: u64 = 1;
+
+// Creates the Rusk initial state for the tests below
+fn stake_state<P: AsRef<Path>>(dir: P) -> Result<Rusk> {
+    let snapshot =
+        toml::from_str(include_str!("../config/stake_from_contract.toml"))
+            .expect("Cannot deserialize config");
+
+    new_state(dir, &snapshot, u64::MAX)
+}
+
+#[tokio::test(flavor = "multi_thread")]
+pub async fn stake_from_contract() -> Result<()> {
+    // Setup the logger
+    logger();
+
+    let mut rng = StdRng::seed_from_u64(0xdead);
+
+    let tmp = tempdir().expect("Should be able to create temporary directory");
+    let rusk = stake_state(&tmp)?;
+
+    let cache = Arc::new(RwLock::new(HashMap::new()));
+
+    // Create a wallet
+    let wallet = wallet::Wallet::new(
+        TestStore,
+        TestStateClient {
+            rusk: rusk.clone(),
+            cache,
+        },
+    );
+
+    let contract_id = deploy_proxy_contract(&rusk, &wallet);
+
+    info!("Contract ID: {:?}", contract_id);
+
+    let pk = wallet.account_public_key(0).unwrap();
+    let sk = wallet.account_secret_key(0).unwrap();
+
+    let stake = Stake::new_from_contract(
+        &sk,
+        contract_id,
+        MINIMUM_STAKE,
+        rusk.chain_id().unwrap(),
+    );
+    let call = ContractCall::new(contract_id, "stake", &stake)
+        .expect("call to be successful");
+    let stake_from_contract = wallet
+        .moonlight_execute(
+            0,
+            0,
+            MINIMUM_STAKE,
+            GAS_LIMIT,
+            GAS_PRICE,
+            Some(call.clone()),
+        )
+        .expect("stake to be successful");
+    let tx = execute_transaction(stake_from_contract, &rusk, 1, None, None);
+    info!("Stake from contract - gas spent {:?}", tx.gas_spent);
+
+    let stake = wallet.get_stake(0).expect("stake to be found");
+    assert_eq!(
+        MINIMUM_STAKE,
+        stake.amount.expect("stake amount to be found").value
+    );
+
+    let stake_from_contract = wallet
+        .moonlight_execute(
+            0,
+            0,
+            MINIMUM_STAKE,
+            GAS_LIMIT,
+            GAS_PRICE,
+            Some(call.clone()),
+        )
+        .expect("stake to be successful");
+    let tx = execute_transaction(stake_from_contract, &rusk, 2, None, None);
+    info!("Stake from contract - gas spent {:?}", tx.gas_spent);
+
+    let stake = wallet.get_stake(0).expect("stake to be found");
+    assert_eq!(
+        MINIMUM_STAKE * 2,
+        stake.amount.expect("stake amount to be found").value
+    );
+
+    let stake_from_contract = wallet
+        .moonlight_execute(
+            0,
+            0,
+            MINIMUM_STAKE,
+            GAS_LIMIT,
+            GAS_PRICE,
+            Some(call),
+        )
+        .expect("stake to be successful");
+    let tx = execute_transaction(
+        stake_from_contract,
+        &rusk,
+        EPOCH * 2,
+        None,
+        Some(pk),
+    );
+    info!("Stake from contract - gas spent {:?}", tx.gas_spent);
+
+    let stake = wallet.get_stake(0).expect("stake to be found");
+    assert_eq!(
+        MINIMUM_STAKE * 2 + (MINIMUM_STAKE / 10 * 9) as u64,
+        stake.amount.expect("stake amount to be found").value
+    );
+
+    assert_eq!(
+        (MINIMUM_STAKE / 10) as u64,
+        stake.amount.expect("stake amount to be found").locked
+    );
+
+    let stake_from_account = wallet
+        .moonlight_stake(0, 0, 1000, GAS_LIMIT, 1)
+        .expect("stake to be successful");
+    execute_transaction(
+        stake_from_account,
+        &rusk,
+        1,
+        "Panic: Keys mismatch",
+        None,
+    );
+
+    let unstake = wallet
+        .moonlight_unstake(&mut rng, 0, 0, GAS_LIMIT, 1)
+        .expect("stake to be successful");
+
+    execute_transaction(
+        unstake,
+        &rusk,
+        1,
+        "Panic: expect StakeFundOwner::Account",
+        None,
+    );
+
+    let unstake = stake::Withdraw::new(
+        &sk,
+        transfer::withdraw::Withdraw::new(
+            &mut rng,
+            &sk,
+            contract_id,
+            3 * MINIMUM_STAKE,
+            transfer::withdraw::WithdrawReceiver::Moonlight(pk),
+            transfer::withdraw::WithdrawReplayToken::Moonlight(7),
+        ),
+    );
+
+    let prev_balance = wallet.get_account(0).unwrap().balance;
+    assert_eq!(
+        wallet
+            .get_stake(0)
+            .expect("stake to exists")
+            .amount
+            .unwrap()
+            .total_funds(),
+        3 * MINIMUM_STAKE
+    );
+    let call = ContractCall::new(contract_id, "unstake", &unstake)
+        .expect("call to be successful");
+    let unstake_from_contract = wallet
+        .moonlight_execute(0, 0, 0, GAS_LIMIT, GAS_PRICE, Some(call.clone()))
+        .expect("unstakes to be successful");
+    let tx = execute_transaction(unstake_from_contract, &rusk, 1, None, None);
+    info!("UnStake from contract - gas spent {:?}", tx.gas_spent);
+
+    assert_eq!(wallet.get_stake(0).expect("stake to exists").amount, None);
+    let new_balance = wallet.get_account(0).unwrap().balance;
+    let fee_paid = tx.gas_spent * GAS_PRICE;
+    assert_eq!(new_balance, prev_balance + 3 * MINIMUM_STAKE - fee_paid);
+
+    let current_reward = wallet.get_stake(0).expect("Stake to exists").reward;
+
+    let withdraw = stake::Withdraw::new(
+        &wallet.account_secret_key(0).unwrap(),
+        transfer::withdraw::Withdraw::new(
+            &mut rng,
+            &sk,
+            contract_id,
+            current_reward - 1,
+            transfer::withdraw::WithdrawReceiver::Moonlight(
+                wallet.account_public_key(0).unwrap(),
+            ),
+            transfer::withdraw::WithdrawReplayToken::Moonlight(8),
+        ),
+    );
+
+    let call = ContractCall::new(contract_id, "withdraw", &withdraw)
+        .expect("call to be successful");
+    let withdraw_from_contract = wallet
+        .moonlight_execute(0, 0, 0, GAS_LIMIT, GAS_PRICE, Some(call.clone()))
+        .expect("unstakes to be successful");
+    let tx = execute_transaction(withdraw_from_contract, &rusk, 1, None, None);
+    info!("Withdraw from contract - gas spent {:?}", tx.gas_spent);
+
+    assert_eq!(wallet.get_stake(0).expect("stake to exists").reward, 1);
+
+    Ok(())
+}
+
+fn deploy_proxy_contract(
+    rusk: &Rusk,
+    wallet: &wallet::Wallet<TestStore, TestStateClient>,
+) -> ContractId {
+    let deploy_nonce = 0u64;
+    let owner = wallet.account_public_key(0).unwrap();
+    let charlie_byte_code = include_bytes!(
+        "../../../target/wasm32-unknown-unknown/release/charlie.wasm"
+    );
+    let contract_id =
+        gen_contract_id(&charlie_byte_code, deploy_nonce, owner.to_bytes());
+    let tx = wallet
+        .moonlight_deployment(
+            0,
+            charlie_byte_code,
+            &owner,
+            vec![],
+            GAS_LIMIT,
+            20000,
+            0,
+        )
+        .expect("Failed to create a deploy transaction");
+
+    execute_transaction(tx, rusk, BLOCK_HEIGHT, None, None);
+    contract_id
+}
+
+fn execute_transaction<'a, E: Into<Option<&'a str>>>(
+    tx: Transaction,
+    rusk: &Rusk,
+    block_height: u64,
+    expected_error: E,
+    generator: Option<execution_core::signatures::bls::PublicKey>,
+) -> SpentTransaction {
+    let (executed_txs, _) = generator_procedure2(
+        &rusk,
+        &[tx],
+        block_height,
+        BLOCK_GAS_LIMIT,
+        vec![],
+        None,
+        generator,
+    )
+    .expect("generator procedure to succeed");
+    let tx = executed_txs
+        .into_iter()
+        .next()
+        .expect("Transaction must be executed");
+
+    let tx_error = tx.err.as_ref().map(|e| e.as_str());
+    let error = expected_error.into();
+    assert_eq!(tx_error, error, "Output error does not match");
+    tx
+}
diff --git a/rusk/tests/services/finalization.rs b/rusk/tests/services/finalization.rs
index 40337702f8..b8d6550921 100644
--- a/rusk/tests/services/finalization.rs
+++ b/rusk/tests/services/finalization.rs
@@ -53,6 +53,7 @@ fn empty_blocks(rusk: &Rusk, blocks: u64, finalize: bool) -> Vec<[u8; 32]> {
             BLOCK_GAS_LIMIT,
             vec![],
             None,
+            None,
         )
         .expect("block to be created");
         if finalize {
diff --git a/rusk/tests/services/mod.rs b/rusk/tests/services/mod.rs
index 7aaee5cdf7..eb9caef6f9 100644
--- a/rusk/tests/services/mod.rs
+++ b/rusk/tests/services/mod.rs
@@ -5,8 +5,9 @@
 // Copyright (c) DUSK NETWORK. All rights reserved.
 
 pub mod contract_deployment;
+pub mod contract_stake;
 pub mod conversion;
-pub mod deploy;
+//pub mod deploy;
 pub mod finalization;
 pub mod gas_behavior;
 pub mod moonlight_stake;
diff --git a/test-wallet/src/imp.rs b/test-wallet/src/imp.rs
index 31418def9e..8f614f6de5 100644
--- a/test-wallet/src/imp.rs
+++ b/test-wallet/src/imp.rs
@@ -31,7 +31,7 @@ use rkyv::validation::validators::CheckDeserializeError;
 use rusk_prover::LocalProver;
 use wallet_core::keys::{derive_bls_sk, derive_phoenix_sk};
 use wallet_core::transaction::{
-    moonlight_deployment, moonlight_stake, moonlight_stake_reward,
+    moonlight, moonlight_deployment, moonlight_stake, moonlight_stake_reward,
     moonlight_to_phoenix, moonlight_unstake, phoenix as phoenix_transaction,
     phoenix_deployment, phoenix_stake, phoenix_stake_reward,
     phoenix_to_moonlight, phoenix_unstake,
@@ -338,6 +338,48 @@ where
         Ok(notes_openings)
     }
 
+    /// Executes a generic contract call, paying gas from a public account.
+    #[allow(clippy::too_many_arguments)]
+    pub fn moonlight_execute(
+        &self,
+        sender_idx: u8,
+        transfer_value: u64,
+        deposit: u64,
+        gas_limit: u64,
+        gas_price: u64,
+        exec: Option<impl Into<TransactionData>>,
+    ) -> Result<Transaction, Error<S, SC>> {
+        let state = self.state();
+
+        let mut sender_sk = self.account_secret_key(sender_idx)?;
+        let sender = self.account_public_key(sender_idx)?;
+
+        let account = state
+            .fetch_account(&sender)
+            .map_err(Error::from_state_err)?;
+
+        // technically this check is not necessary, but it's nice to not spam
+        // the network with transactions that are unspendable.
+        let nonce = account.nonce + 1;
+
+        let chain_id = state.fetch_chain_id().map_err(Error::from_state_err)?;
+
+        let tx = moonlight(
+            &sender_sk,
+            None,
+            transfer_value,
+            deposit,
+            gas_limit,
+            gas_price,
+            nonce,
+            chain_id,
+            exec,
+        )?;
+
+        sender_sk.zeroize();
+        Ok(tx)
+    }
+
     /// Execute a generic contract call or deployment, using Phoenix notes to
     /// pay for gas.
     #[allow(clippy::too_many_arguments)]