Repository: chain4travel/caminogo
Pull request #404: [PVM] Undeposit API
Author: login: evlekht
Created: 2025-02-10T11:21:25Z | Merged: 2025-03-20T11:46:26Z

## Why this should be merged
PR introduces undeposit PVM api, which creates ins and outs that will undeposit specified amounts from deposited utxos belonging to specified deposits.

## How this works
Added new service handler to PVM, it calls utxo handler undeposit method.
Undeposith method was reworked to support amounts arg, but in the end it lead to full rewrite of undeposit and its tests.

## How this was tested
Unit-tests.


Diff:
diff --git a/vms/platformvm/camino_service.go b/vms/platformvm/camino_service.go
index f48d794fc2db..66d20cc069e2 100644
--- a/vms/platformvm/camino_service.go
+++ b/vms/platformvm/camino_service.go
@@ -430,14 +430,116 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
 	}
 
-	response.Signers = make([][]ids.ShortID, len(signers))
-	for i, cred := range signers {
-		response.Signers[i] = make([]ids.ShortID, len(cred))
-		for j, sig := range cred {
-			response.Signers[i][j] = sig.Address()
-		}
+	response.Signers = signersFromKeys(signers)
+
+	bytes, err = txs.Codec.Marshal(txs.Version, owners)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+	return nil
+}
+
+type Undeposit struct {
+	Amount      utilsjson.Uint64 `json:"amount"`
+	DepositTxID ids.ID           `json:"depositTxID"`
+}
+
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	UndepositTo  platformapi.Owner   `json:"undepositTo"`
+	AmountToBurn utilsjson.Uint64    `json:"amountToBurn"`
+	Undeposits   []Undeposit         `json:"undeposits"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+	Owners  string          `json:"owners"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	if len(args.Undeposits) == 0 {
+		return errors.New("no undeposits provided")
 	}
 
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
+
+	undepositTo, err := s.secpOwnerFromAPI(&args.UndepositTo)
+	if err != nil {
+		return err
+	}
+
+	undeposits := make(map[ids.ID]uint64, len(args.Undeposits))
+	for _, undeposit := range args.Undeposits {
+		undeposits[undeposit.DepositTxID] = uint64(undeposit.Amount)
+	}
+
+	depositIns, depositOuts, depositSigners, depositOwners, err := s.vm.txBuilder.UnlockDeposit(
+		s.vm.state,
+		privKeys,
+		undeposits,
+		undepositTo,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins, outs, signers, owners, err := s.vm.txBuilder.Lock(
+		s.vm.state,
+		privKeys,
+		0,
+		uint64(args.AmountToBurn),
+		locked.StateUnlocked,
+		nil,
+		nil,
+		0,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins = append(ins, depositIns...)
+	outs = append(outs, depositOuts...)
+	signers = append(signers, depositSigners...)
+	owners = append(owners, depositOwners...)
+
+	avax.SortTransferableInputsWithSigners(ins, signers)
+	avax.SortTransferableOutputs(outs, txs.Codec)
+
+	bytes, err := txs.Codec.Marshal(txs.Version, ins)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, outs)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	response.Signers = signersFromKeys(signers)
+
 	bytes, err = txs.Codec.Marshal(txs.Version, owners)
 	if err != nil {
 		return fmt.Errorf("%w: %s", errSerializeOwners, err)
@@ -445,6 +547,7 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
 		return fmt.Errorf("%w: %s", errSerializeOwners, err)
 	}
+
 	return nil
 }
 
@@ -1149,3 +1252,14 @@ func (s *CaminoService) GetCurrentSupply(_ *http.Request, args *GetCurrentSupply
 	reply.Supply = utilsjson.Uint64(supply)
 	return err
 }
+
+func signersFromKeys(signersKeys [][]*secp256k1.PrivateKey) [][]ids.ShortID {
+	signers := make([][]ids.ShortID, len(signersKeys))
+	for i, keys := range signersKeys {
+		signers[i] = make([]ids.ShortID, len(keys))
+		for j, key := range keys {
+			signers[i][j] = key.Address()
+		}
+	}
+	return signers
+}
diff --git a/vms/platformvm/deposit/camino_deposit.go b/vms/platformvm/deposit/camino_deposit.go
index 3f95d13a2611..f23c8efe09d5 100644
--- a/vms/platformvm/deposit/camino_deposit.go
+++ b/vms/platformvm/deposit/camino_deposit.go
@@ -70,6 +70,13 @@ func (deposit *Deposit) UnlockableAmount(offer *Offer, unlockTime uint64) uint64
 	return bigTotalUnlockableAmount.Uint64() - deposit.UnlockedAmount
 }
 
+// Returns amount of tokens that can be unlocked from [deposit] at [unlockTime] (seconds).
+//
+// Precondition: all args are valid in conjunction.
+func (deposit *Deposit) UnlockableAmountByTime(offer *Offer, unlockTime time.Time) uint64 {
+	return deposit.UnlockableAmount(offer, uint64(unlockTime.Unix()))
+}
+
 // Returns amount of tokens that can be claimed as reward for [deposit] at [claimetime] (seconds).
 //
 // Precondition: all args are valid in conjunction.
diff --git a/vms/platformvm/test/expect/camino_expect.go b/vms/platformvm/test/expect/camino_expect.go
index a845dcb182ad..01d6888783bd 100644
--- a/vms/platformvm/test/expect/camino_expect.go
+++ b/vms/platformvm/test/expect/camino_expect.go
@@ -221,6 +221,56 @@ func StateVerifyMultisigOwner(
 		return
 	}
 
+	require.Len(t, msigAliasAddresses, len(msigAliases))
+
+	aliases := make(map[ids.ShortID]*multisig.AliasWithNonce)
+	for i := range msigAliasAddresses {
+		aliases[msigAliasAddresses[i]] = msigAliases[i]
+	}
+
+	addresses := set.Set[ids.ShortID]{}
+
+	if collectAddresses {
+		for _, addr := range owner.Addrs {
+			addresses.Add(addr)
+		}
+		for _, alias := range msigAliases {
+			owner, ok := alias.Owners.(*secp256k1fx.OutputOwners)
+			require.True(t, ok)
+			for _, addr := range owner.Addrs {
+				addresses.Add(addr)
+			}
+		}
+	}
+
+	for _, msigAliasAddress := range msigAliasAddresses {
+		addresses.Add(msigAliasAddress)
+	}
+
+	for addr := range addresses {
+		if _, ok := aliases[addr]; ok {
+			s.EXPECT().GetMultisigAlias(addr).Return(aliases[addr], nil)
+		} else {
+			s.EXPECT().GetMultisigAlias(addr).Return(nil, database.ErrNotFound)
+		}
+	}
+}
+
+func VerifyMultisigOwner(
+	t *testing.T,
+	s *state.MockChain,
+	owner *secp256k1fx.OutputOwners,
+	msigAliasAddresses []ids.ShortID,
+	msigAliases []*multisig.AliasWithNonce,
+	collectAddresses bool,
+) {
+	t.Helper()
+	if owner == nil {
+		return
+	}
+
+	require.Len(t, msigAliasAddresses, len(msigAliases))
+
 	aliases := make(map[ids.ShortID]*multisig.AliasWithNonce)
 	for i := range msigAliasAddresses {
 		aliases[msigAliasAddresses[i]] = msigAliases[i]
@@ -367,3 +417,21 @@ func StateGetBondTxIDs(
 		}
 	}
 }
+
+func GetDepositUnlockableAmounts(
+	t *testing.T,
+	s *state.MockChain,
+	depositTxIDs []ids.ID,
+	deposits []*deposit.Deposit,
+	offers []*deposit.Offer,
+) {
+	t.Helper()
+	require.Len(t, depositTxIDs, len(deposits))
+	require.Len(t, depositTxIDs, len(offers))
+	for i := range depositTxIDs {
+		s.EXPECT().GetDeposit(depositTxIDs[i]).Return(deposits[i], nil)
+	}
+	for i := range offers {
+		s.EXPECT().GetDepositOffer(deposits[i].DepositOfferID).Return(offers[i], nil)
+	}
+}
diff --git a/vms/platformvm/test/generate/camino_generate.go b/vms/platformvm/test/generate/camino_generate.go
index ffca0cac0900..5638c0863499 100644
--- a/vms/platformvm/test/generate/camino_generate.go
+++ b/vms/platformvm/test/generate/camino_generate.go
@@ -266,7 +266,7 @@ func InFromUTXO(t *testing.T, utxo *avax.UTXO, sigIndices []uint32, initID bool)
 	return input
 }
 
-func InsFromUTXOs(t *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+func InsFromUTXOs(t *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 	t.Helper()
 	return InsFromUTXOsWithSigIndices(t, utxos, []uint32{0})
 }
diff --git a/vms/platformvm/txs/builder/camino_builder.go b/vms/platformvm/txs/builder/camino_builder.go
index 0979d102800a..966c9ded8629 100644
--- a/vms/platformvm/txs/builder/camino_builder.go
+++ b/vms/platformvm/txs/builder/camino_builder.go
@@ -80,12 +80,6 @@ type CaminoTxBuilder interface {
 		change *secp256k1fx.OutputOwners,
 	) (*txs.Tx, error)
 
-	NewUnlockDepositTx(
-		depositTxIDs []ids.ID,
-		keys []*secp256k1.PrivateKey,
-		change *secp256k1fx.OutputOwners,
-	) (*txs.Tx, error)
-
 	NewClaimTx(
 		claimables []txs.ClaimAmount,
 		claimTo *secp256k1fx.OutputOwners,
@@ -411,56 +405,6 @@ func (b *caminoBuilder) NewDepositTx(
 	return tx, tx.SyntacticVerify(b.ctx)
 }
 
-func (b *caminoBuilder) NewUnlockDepositTx(
-	depositTxIDs []ids.ID,
-	keys []*secp256k1.PrivateKey,
-	change *secp256k1fx.OutputOwners,
-) (*txs.Tx, error) {
-	caminoGenesis, err := b.state.CaminoConfig()
-	if err != nil {
-		return nil, err
-	}
-	if !caminoGenesis.LockModeBondDeposit {
-		return nil, errWrongLockMode
-	}
-
-	// unlocking
-	ins, outs, signers, err := b.UnlockDeposit(b.state, keys, depositTxIDs)
-	if err != nil {
-		return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
-	}
-
-	// burning fee
-	feeIns, feeOuts, feeSigners, _, err := b.Lock(b.state, keys, 0, b.cfg.TxFee, locked.StateUnlocked, nil, change, 0)
-	if err != nil {
-		return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
-	}
-
-	ins = append(ins, feeIns...)
-	outs = append(outs, feeOuts...)
-	signers = append(signers, feeSigners...)
-
-	// we need to sort ins/outs/signers before using them in tx
-	// UnlockDeposit returns unsorted results and we appended arrays
-	avax.SortTransferableInputsWithSigners(ins, signers)
-	avax.SortTransferableOutputs(outs, txs.Codec)
-
-	utx := &txs.UnlockDepositTx{
-		BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-			NetworkID:    b.ctx.NetworkID,
-			BlockchainID: b.ctx.ChainID,
-			Ins:          ins,
-			Outs:         outs,
-		}},
-	}
-
-	tx, err := txs.NewSigned(utx, txs.Codec, signers)
-	if err != nil {
-		return nil, err
-	}
-	return tx, tx.SyntacticVerify(b.ctx)
-}
-
 func (b *caminoBuilder) NewClaimTx(
 	claimables []txs.ClaimAmount,
 	claimTo *secp256k1fx.OutputOwners,
diff --git a/vms/platformvm/txs/builder/camino_builder_test.go b/vms/platformvm/txs/builder/camino_builder_test.go
index 813d30f91878..1cbb13affbbb 100644
--- a/vms/platformvm/txs/builder/camino_builder_test.go
+++ b/vms/platformvm/txs/builder/camino_builder_test.go
@@ -357,94 +357,6 @@ func TestNewAddSubnetValidatorTx(t *testing.T) {
 	}
 }
 
-func TestNewUnlockDepositTx(t *testing.T) {
-	ctx := test.Context(t)
-
-	fundsKey := test.FundedKeys[0]
-	fundsAddr := fundsKey.Address()
-	fundsOwner := secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{fundsAddr}}
-
-	depositTxID := ids.ID{1, 1}
-	bondTxID := ids.ID{2, 2}
-	offerID := ids.ID{3, 3}
-
-	depositedAmount := uint64(100)
-	unlockedAmount := depositedAmount / 2
-
-	unlockedUTXO := generate.UTXO(ids.ID{1}, ctx.AVAXAssetID, test.TxFee, fundsOwner, ids.Empty, ids.Empty, false)
-	depositedUTXO := generate.UTXO(ids.ID{2}, ctx.AVAXAssetID, depositedAmount/4, fundsOwner, depositTxID, ids.Empty, false)
-	depositedBondedUTXO := generate.UTXO(ids.ID{3}, ctx.AVAXAssetID, depositedAmount/4, fundsOwner, depositTxID, bondTxID, false)
-
-	tests := map[string]struct {
-		state        func(*gomock.Controller) state.State
-		depositTxIDs []ids.ID
-		keys         []*secp256k1.PrivateKey
-		change       *secp256k1fx.OutputOwners
-		utx          *txs.UnlockDepositTx
-		signers      [][]*secp256k1.PrivateKey
-		expectedErr  error
-	}{
-		"OK": {
-			state: func(ctrl *gomock.Controller) state.State {
-				s := state.NewMockState(ctrl)
-				s.EXPECT().CaminoConfig().Return(&state.CaminoConfig{LockModeBondDeposit: true}, nil)
-				expect.UnlockDeposit(t, s,
-					map[ids.ID]*deposits.Deposit{
-						depositTxID: {
-							DepositOfferID: offerID,
-							Start:          test.GenesisTimestamp,
-							UnlockedAmount: unlockedAmount,
-							Amount:         depositedAmount,
-						},
-					},
-					[]*deposits.Offer{{ID: offerID}},
-					[]ids.ShortID{fundsAddr},
-					[]*avax.UTXO{depositedUTXO, depositedBondedUTXO},
-					locked.StateDeposited,
-				)
-				expect.Lock(t, s, map[ids.ShortID][]*avax.UTXO{fundsAddr: {unlockedUTXO, depositedUTXO, depositedBondedUTXO}})
-				return s
-			},
-			depositTxIDs: []ids.ID{depositTxID},
-			keys:         []*secp256k1.PrivateKey{fundsKey},
-			utx: &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-				NetworkID:    ctx.NetworkID,
-				BlockchainID: ctx.ChainID,
-				Ins: []*avax.TransferableInput{
-					generate.InFromUTXO(t, unlockedUTXO, []uint32{0}, false),
-					generate.InFromUTXO(t, depositedUTXO, []uint32{0}, false),
-					generate.InFromUTXO(t, depositedBondedUTXO, []uint32{0}, false),
-				},
-				Outs: []*avax.TransferableOutput{
-					generate.OutFromUTXO(t, depositedUTXO, ids.Empty, ids.Empty),
-					generate.OutFromUTXO(t, depositedBondedUTXO, ids.Empty, bondTxID),
-				},
-			}}},
-			signers: [][]*secp256k1.PrivateKey{
-				{fundsKey}, {fundsKey}, {fundsKey},
-			},
-		},
-	}
-	for name, tt := range tests {
-		t.Run(name, func(t *testing.T) {
-			require := require.New(t)
-			ctrl := gomock.NewController(t)
-			b := newCaminoBuilder(t, tt.state(ctrl), nil, test.PhaseLast)
-
-			tx, err := b.NewUnlockDepositTx(tt.depositTxIDs, tt.keys, tt.change)
-			require.ErrorIs(err, tt.expectedErr)
-			if err != nil {
-				require.Nil(tx)
-				return
-			}
-			expectedTx, err := txs.NewSigned(tt.utx, txs.Codec, tt.signers)
-			require.NoError(err)
-			require.NoError(expectedTx.SyntacticVerify(b.ctx))
-			require.Equal(expectedTx, tx)
-		})
-	}
-}
-
 func TestNewClaimTx(t *testing.T) {
 	ctx := test.Context(t)
 
diff --git a/vms/platformvm/txs/executor/camino_tx_executor_test.go b/vms/platformvm/txs/executor/camino_tx_executor_test.go
index a41311f1891f..306a880e81ab 100644
--- a/vms/platformvm/txs/executor/camino_tx_executor_test.go
+++ b/vms/platformvm/txs/executor/camino_tx_executor_test.go
@@ -3624,7 +3624,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, 1, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1-1, owner1, depositTxID1, ids.Empty),
@@ -3647,7 +3647,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 				}}}
 			},
 			phase:       test.PhaseBerlin,
@@ -3665,7 +3665,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{deposit1UTXO, unlockedUTXOWithLargerTxID}),
+					Ins: generate.InsFromUTXOs(t, deposit1UTXO, unlockedUTXOWithLargerTxID),
 				}}}
 			},
 			phase:       test.PhaseBerlin,
@@ -3685,7 +3685,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{deposit2UTXO, deposit1UTXOLargerTxID}),
+					Ins: generate.InsFromUTXOs(t, deposit2UTXO, deposit1UTXOLargerTxID),
 				}}}
 			},
 			phase:       test.PhaseBerlin,
@@ -3713,7 +3713,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, depositAmount1-1, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, 1, owner1, depositTxID1, ids.Empty),
@@ -3749,7 +3749,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, depositAmount1/2+1, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1/2-1, owner1, depositTxID1, ids.Empty),
@@ -3772,7 +3772,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, depositAmount1-1, owner1, ids.Empty, ids.Empty),
 					},
@@ -3806,7 +3806,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, lessFeeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, lessFeeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, lessFeeUTXO.Out.(avax.Amounter).Amount(), feeOwner, ids.Empty, ids.Empty),
 						generate.OutFromUTXO(t, deposit1UTXO, depositTxID1, ids.Empty),
@@ -3838,7 +3838,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(p test.Phase, c *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{moreFeeUTXO}),
+					Ins: generate.InsFromUTXOs(t, moreFeeUTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, moreFeeUTXO.Out.(avax.Amounter).Amount()-test.TxFee, feeOwner, ids.Empty, ids.Empty),
 					},
@@ -3882,7 +3882,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{deposit1WithRewardUTXO}),
+					Ins: generate.InsFromUTXOs(t, deposit1WithRewardUTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, depositAmount1, owner1, ids.Empty, ids.Empty),
 					},
@@ -3934,7 +3934,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 				s.EXPECT().GetDepositOffer(deposit2.DepositOfferID).Return(offer, nil)
 				s.EXPECT().ModifyDeposit(depositTxID2, &deposit.Deposit{
 					DepositOfferID:      deposit2.DepositOfferID,
-					UnlockedAmount:      deposit2.UnlockedAmount + deposit2.UnlockableAmount(offer, uint64(chainTime.Unix())),
+					UnlockedAmount:      deposit2.UnlockedAmount + deposit2.UnlockableAmountByTime(offer, chainTime),
 					ClaimedRewardAmount: deposit2.ClaimedRewardAmount,
 					Start:               deposit2.Start,
 					Duration:            deposit2.Duration,
@@ -3967,9 +3967,9 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 				chainTime := deposit1.StartUnlockTime(offer).Add(offer.UnlockPeriodDurationNano() / 2)
 
 				unlockedDepositAmount1 := depositAmount1 / 2 // unlocking at half of deposit1 unlock period
-				unlockedDepositAmount2 := deposit2.UnlockableAmount(offer, uint64(chainTime.Unix()))
+				unlockedDepositAmount2 := deposit2.UnlockableAmountByTime(offer, chainTime)
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO, deposit2UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO, deposit2UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, unlockedDepositAmount1+unlockedDepositAmount2, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1-unlockedDepositAmount1, owner1, depositTxID1, ids.Empty),
@@ -4017,7 +4017,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, 1, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1-1, owner1, depositTxID1, ids.Empty),
@@ -4054,7 +4054,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, depositAmount1/2+1, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1/2-1, owner1, depositTxID1, ids.Empty),
@@ -4090,7 +4090,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(test.Phase, *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, lessFeeUTXO, deposit1UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, lessFeeUTXO, deposit1UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, lessFeeUTXO.Out.(avax.Amounter).Amount(), feeOwner, ids.Empty, ids.Empty),
 						generate.OutFromUTXO(t, deposit1UTXO, depositTxID1, ids.Empty),
@@ -4118,7 +4118,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(p test.Phase, c *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{moreFeeUTXO}),
+					Ins: generate.InsFromUTXOs(t, moreFeeUTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, moreFeeUTXO.Out.(avax.Amounter).Amount()-test.TxFee, feeOwner, ids.Empty, ids.Empty),
 					},
@@ -4147,7 +4147,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 			},
 			utx: func(p test.Phase, c *config.Config) *txs.UnlockDepositTx {
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins:  generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO}),
+					Ins:  generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO),
 					Outs: generate.OutsFromUTXOs(t, []*avax.UTXO{deposit1UTXO}, ids.Empty, ids.Empty),
 				}}}
 			},
@@ -4199,7 +4199,7 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 				s.EXPECT().GetDepositOffer(deposit2.DepositOfferID).Return(offer, nil)
 				s.EXPECT().ModifyDeposit(depositTxID2, &deposit.Deposit{
 					DepositOfferID:      deposit2.DepositOfferID,
-					UnlockedAmount:      deposit2.UnlockedAmount + deposit2.UnlockableAmount(offer, uint64(chainTime.Unix())),
+					UnlockedAmount:      deposit2.UnlockedAmount + deposit2.UnlockableAmountByTime(offer, chainTime),
 					ClaimedRewardAmount: deposit2.ClaimedRewardAmount,
 					Start:               deposit2.Start,
 					Duration:            deposit2.Duration,
@@ -4232,9 +4232,9 @@ func TestCaminoStandardTxExecutorUnlockDepositTx(t *testing.T) {
 				chainTime := deposit1.StartUnlockTime(offer).Add(offer.UnlockPeriodDurationNano() / 2)
 
 				unlockedDepositAmount1 := depositAmount1 / 2 // unlocking at half of deposit1 unlock period
-				unlockedDepositAmount2 := deposit2.UnlockableAmount(offer, uint64(chainTime.Unix()))
+				unlockedDepositAmount2 := deposit2.UnlockableAmountByTime(offer, chainTime)
 				return &txs.UnlockDepositTx{BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{
-					Ins: generate.InsFromUTXOs(t, []*avax.UTXO{feeUTXO, deposit1UTXO, deposit2UTXO}),
+					Ins: generate.InsFromUTXOs(t, feeUTXO, deposit1UTXO, deposit2UTXO),
 					Outs: []*avax.TransferableOutput{
 						generate.Out(test.AVAXAssetID, unlockedDepositAmount1+unlockedDepositAmount2, owner1, ids.Empty, ids.Empty),
 						generate.Out(test.AVAXAssetID, depositAmount1-unlockedDepositAmount1, owner1, depositTxID1, ids.Empty),
@@ -8796,7 +8796,7 @@ func TestCaminoStandardTxExecutorUnlockExpiredDepositTx(t *testing.T) {
 	require.Greater(t, deposit3.TotalReward(depositOffer3), deposit3.ClaimedRewardAmount)
 
 	unlockExpiredDepositTx := &txs.UnlockExpiredDepositTx{
-		Ins:  generate.InsFromUTXOs(t, []*avax.UTXO{depositedUTXO1}),
+		Ins:  generate.InsFromUTXOs(t, depositedUTXO1),
 		Outs: generate.OutsFromUTXOs(t, []*avax.UTXO{depositedUTXO1}, ids.Empty, ids.Empty),
 	}
 
diff --git a/vms/platformvm/utxo/camino_helpers_test.go b/vms/platformvm/utxo/camino_helpers_test.go
index 50ab80cde20d..6e298e384cd7 100644
--- a/vms/platformvm/utxo/camino_helpers_test.go
+++ b/vms/platformvm/utxo/camino_helpers_test.go
@@ -10,7 +10,6 @@ import (
 
 	"github.com/ava-labs/avalanchego/codec/linearcodec"
 	"github.com/ava-labs/avalanchego/ids"
-	"github.com/ava-labs/avalanchego/snow"
 	"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
 	"github.com/ava-labs/avalanchego/utils/hashing"
 	"github.com/ava-labs/avalanchego/utils/logging"
@@ -51,7 +50,7 @@ func defaultCaminoHandler(t *testing.T) *caminoHandler {
 
 	return &caminoHandler{
 		handler: handler{
-			ctx: snow.DefaultContextTest(),
+			ctx: test.Context(t),
 			clk: clk,
 			fx:  fx,
 		},
diff --git a/vms/platformvm/utxo/camino_locked.go b/vms/platformvm/utxo/camino_locked.go
index e2837ba6f96d..f647db4d5d35 100644
--- a/vms/platformvm/utxo/camino_locked.go
+++ b/vms/platformvm/utxo/camino_locked.go
@@ -49,6 +49,7 @@ var (
 	errInvalidToOwner            = errors.New("invalid to-owner")
 	errInvalidChangeOwner        = errors.New("invalid change owner")
 	errNewBondOwner              = errors.New("can't create bond for new owner")
+	errInsufficientUnlockable    = errors.New("insufficient unlockable amount")
 )
 
 // Creates UTXOs from [outs] and adds them to the UTXO set.
@@ -92,7 +93,7 @@ type CaminoSpender interface {
 	// - [totalAmountToBurn] is the amount of AVAX that should be burned
 	// - [appliedLockState] state to set (except BondDeposit)
 	// - [to] owner of unlocked amounts if appliedLockState is Unlocked
-	// - [change] owner of unlocked amounts resulting from splittig inputs
+	// - [change] owner of unlocked amounts resulting from splitting inputs
 	// - [asOf] timestamp against LockTime is compared
 	// Returns:
 	// - [inputs] the inputs that should be consumed to fund the outputs
@@ -120,7 +121,7 @@ type CaminoSpender interface {
 	// Arguments:
 	// - [state] chainstate which will be used to fetch utxos and deposit data
 	// - [keys] are the owners of the deposits
-	// - [depositTxIDs] ids of deposit transactions
+	// - [amountsToUndeposit] are ids of deposit transactions and amounts to unlock
 	// Returns:
 	// - [inputs] unsorted inputs that should be consumed to fund the outputs
 	// - [outputs] unsorted outputs that should be returned to the UTXO set
@@ -128,11 +129,13 @@ type CaminoSpender interface {
 	UnlockDeposit(
 		state state.Chain,
 		keys []*secp256k1.PrivateKey,
-		depositTxIDs []ids.ID,
+		amountsToUndeposit map[ids.ID]uint64,
+		unlockTo *secp256k1fx.OutputOwners,
 	) (
 		[]*avax.TransferableInput, // inputs
 		[]*avax.TransferableOutput, // outputs
 		[][]*secp256k1.PrivateKey, // signers
+		[]*secp256k1fx.OutputOwners, // owners
 		error,
 	)
 
@@ -259,39 +262,18 @@ func (h *handler) Lock(
 		return nil, nil, nil, nil, errNewBondOwner
 	}
 
-	type Owner struct {
-		secpOwners *secp256k1fx.OutputOwners
-		id         *ids.ID
-	}
-
-	setOwner := func(secpOwner *secp256k1fx.OutputOwners, owner *Owner) error {
-		if secpOwner == nil {
-			return nil
-		}
-
-		if err := secpOwner.Verify(); err != nil {
-			return fmt.Errorf("invalid owner: %w", err)
-		}
-
-		if err := h.fx.VerifyMultisigOwner(secpOwner, utxoDB); err != nil {
-			return fmt.Errorf("%w: %v", errCompositeMultisig, err)
-		}
-
-		id, err := txs.GetOwnerID(secpOwner)
-		if err != nil {
-			return fmt.Errorf("failed to get ownerID: %w", err)
-		}
-		*owner = Owner{secpOwner, &id}
-		return nil
+	aliasGetter, ok := utxoDB.(secp256k1fx.AliasGetter)
+	if !ok {
+		return nil, nil, nil, nil, errors.New("utxoDB is not an alias getter")
 	}
 
-	newOwner := Owner{}
-	if err := setOwner(to, &newOwner); err != nil {
+	newOwner, err := h.GetOwnerWithID(aliasGetter, to)
+	if err != nil {
 		return nil, nil, nil, nil, fmt.Errorf("%w: %s", errInvalidToOwner, err)
 	}
 
-	changeOwner := Owner{}
-	if err := setOwner(change, &changeOwner); err != nil {
+	changeOwner, err := h.GetOwnerWithID(aliasGetter, change)
+	if err != nil {
 		return nil, nil, nil, nil, fmt.Errorf("%w: %s", errInvalidChangeOwner, err)
 	}
 
@@ -302,6 +284,8 @@ func (h *handler) Lock(
 		return nil, nil, nil, nil, fmt.Errorf("couldn't get UTXOs: %w", err)
 	}
 
+	// utxos loop below relies on utxo being sorted in specific order
+	// so loop can do early break, when remaining utxos are not fitting to the requirements
 	sortUTXOs(utxos, h.ctx.AVAXAssetID, appliedLockState)
 
 	kc := secp256k1fx.NewKeychain(signer...) // Keychain consumes UTXOs and creates new ones
@@ -364,7 +348,7 @@ func (h *handler) Lock(
 		}
 
 		// we only consume locked utxos if we are not transferring them to new owner
-		if lockIDs.IsLocked() && newOwner.id != nil {
+		if lockIDs.IsLocked() && newOwner != nil {
 			// if appliedLockState is bond or deposit, utxos are sorted the way,
 			// that unlocked will go after lockable-locked, so we can't break yet
 			continue
@@ -399,7 +383,7 @@ func (h *handler) Lock(
 		remainingValue := in.Amount()
 		amountToBurn := uint64(0)
 
-		toOwner := Owner{&innerOut.OutputOwners, &outOwnerID}
+		toOwner := &ownerWithID{&innerOut.OutputOwners, outOwnerID}
 		remainingOwner := toOwner
 
 		if !lockIDs.IsLocked() { // utxo isn't locked
@@ -411,12 +395,12 @@ func (h *handler) Lock(
 			totalAmountBurned += amountToBurn
 			remainingValue -= amountToBurn
 
-			if newOwner.id != nil {
+			if newOwner != nil {
 				// transferring unlocked utxo spent tokens to new owner
 				toOwner = newOwner
 			}
 
-			if changeOwner.id != nil {
+			if changeOwner != nil {
 				// transferring unlocked utxo remainder to change owner
 				remainingOwner = changeOwner
 			}
@@ -459,13 +443,13 @@ func (h *handler) Lock(
 
 			if amountToLock > 0 {
 				// amounts map that will be owned by locked owner (either original utxo owner or to-owner)
-				ownerLockedAmounts, ok := insAmounts[*toOwner.id]
+				ownerLockedAmounts, ok := insAmounts[toOwner.id]
 				if !ok {
 					ownerLockedAmounts = &OwnerAmounts{
 						amounts:    make(map[ids.ID]lockedAndRemainedAmounts),
 						secpOwners: toOwner.secpOwners,
 					}
-					insAmounts[*toOwner.id] = ownerLockedAmounts
+					insAmounts[toOwner.id] = ownerLockedAmounts
 				}
 
 				// amounts that will be owned by locked owner (either original utxo owner or to-owner)
@@ -485,13 +469,13 @@ func (h *handler) Lock(
 
 			if remainingValue > 0 {
 				// amounts map that will be owned by change owner (either original utxo owner or change-owner)
-				ownerRemainedAmounts, ok := insAmounts[*remainingOwner.id]
+				ownerRemainedAmounts, ok := insAmounts[remainingOwner.id]
 				if !ok {
 					ownerRemainedAmounts = &OwnerAmounts{
 						amounts:    make(map[ids.ID]lockedAndRemainedAmounts),
 						secpOwners: remainingOwner.secpOwners,
 					}
-					insAmounts[*remainingOwner.id] = ownerRemainedAmounts
+					insAmounts[remainingOwner.id] = ownerRemainedAmounts
 				}
 
 				// amounts that will be owned by change owner (either original utxo owner or change-owner)
@@ -695,82 +679,147 @@ func (h *handler) unlockUTXOs(
 func (h *handler) UnlockDeposit(
 	state state.Chain,
 	keys []*secp256k1.PrivateKey,
-	depositTxIDs []ids.ID,
+	amountsToUndeposit map[ids.ID]uint64,
+	undepositToOwner *secp256k1fx.OutputOwners,
 ) (
 	[]*avax.TransferableInput, // inputs
 	[]*avax.TransferableOutput, // outputs
 	[][]*secp256k1.PrivateKey, // signers
+	[]*secp256k1fx.OutputOwners, // owners
 	error,
 ) {
-	addrs := set.NewSet[ids.ShortID](len(keys)) // The addresses controlled by [keys]
+	newUndepositOwner, err := h.GetOwnerWithID(state, undepositToOwner)
+	if err != nil {
+		return nil, nil, nil, nil, fmt.Errorf("%w: %s", errInvalidToOwner, err)
+	}
+
+	addrs := set.NewSet[ids.ShortID](len(keys)) // addresses controlled by [keys]
 	for _, key := range keys {
 		addrs.Add(key.Address())
 	}
 
-	depositTxSet := set.NewSet[ids.ID](len(depositTxIDs))
-	for _, depositTxID := range depositTxIDs {
+	depositTxSet := set.NewSet[ids.ID](len(amountsToUndeposit))
+	for depositTxID := range amountsToUndeposit {
 		depositTxSet.Add(depositTxID)
 	}
 
-	// Minimum time this transaction will be issued at
+	// earliest time this transaction will be issued at
 	currentTimestamp := uint64(h.clk.Time().Unix())
 
 	unlockableAmounts, err := getDepositUnlockableAmounts(
 		state, depositTxSet, currentTimestamp,
 	)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
+	}
+
+	for depositTxID, amount := range amountsToUndeposit {
+		if unlockableAmounts[depositTxID] < amount {
+			return nil, nil, nil, nil, errInsufficientUnlockable
+		}
 	}
 
 	utxos, err := state.LockedUTXOs(depositTxSet, addrs, locked.StateDeposited)
 	if err != nil {
-		return nil, nil, nil, err
+		return nil, nil, nil, nil, err
 	}
 
-	kc := secp256k1fx.NewKeychain(keys...) // Keychain consumes UTXOs and creates new ones
+	// sort utxos to get not-bonded and smallest first
+	// expects deposited or depositedBonded utxos
+	sortUTXOsWithFunc(utxos, func(iUTXO *avax.UTXO, jUTXO *avax.UTXO) bool {
+		iOut := iUTXO.Out
+		iLockIDs := &locked.IDsEmpty
+		if lockedOut, ok := iOut.(*locked.Out); ok {
+			iOut = lockedOut.TransferableOut
+			iLockIDs = &lockedOut.IDs
+		}
+
+		jOut := jUTXO.Out
+		jLockIDs := &locked.IDsEmpty
+		if lockedOut, ok := jOut.(*locked.Out); ok {
+			jOut = lockedOut.TransferableOut
+			jLockIDs = &lockedOut.IDs
+		}
+
+		// utxo with smaller depositTxID goes first
+		switch bytes.Compare(iLockIDs.DepositTxID[:], jLockIDs.DepositTxID[:]) {
+		case -1: // iDepositTxID < jDepositTxID,  i < j
+			return true
+		case 1: // iOtherLockTxID > jOtherLockTxID,  j < i
+			return false
+		}
+
+		// utxo with smaller bondTxID goes first, so we'll have not-bonded utxos first (not bonded == all zeros in bondTxID)
+		switch bytes.Compare(iLockIDs.BondTxID[:], jLockIDs.BondTxID[:]) {
+		case -1: // iBondTxID < jBondTxID,  i < j
+			return true
+		case 1: // iBondTxID > jBondTxID,  j < i
+			return false
+		}
+
+		iAmount := uint64(0)
+		if amounter, ok := iOut.(avax.Amounter); ok {
+			iAmount = amounter.Amount()
+		}
+
+		jAmount := uint64(0)
+		if amounter, ok := jOut.(avax.Amounter); ok {
+			jAmount = amounter.Amount()
+		}
+
+		return iAmount < jAmount
+	})
+
+	kc := secp256k1fx.NewKeychain(keys...)
+
+	type OwnerAmounts struct {
+		amounts    map[locked.IDs]uint64 // depositTxID, bondTxID -> amount ; ids might be empty
+		secpOwners *secp256k1fx.OutputOwners
+	}
+	produced := make(map[ids.ID]*OwnerAmounts) // ownerID -> OwnerAmounts
 
 	ins := []*avax.TransferableInput{}
 	outs := []*avax.TransferableOutput{}
 	signers := [][]*secp256k1.PrivateKey{}
+	owners := []*secp256k1fx.OutputOwners{} // owners of consumed utxos
 
 	for _, utxo := range utxos {
 		out, ok := utxo.Out.(*locked.Out)
 		if !ok {
-			// This output isn't locked
+			// output isn't locked, should never happen
 			continue
 		} else if !out.IDs.Match(locked.StateDeposited, depositTxSet) {
-			// This output isn't deposited by one of give deposit tx ids
+			// output isn't deposited by one of give deposit tx ids
+			// should never happen
 			continue
 		}
 
-		unlockableAmount := unlockableAmounts[out.DepositTxID]
-		if unlockableAmount == 0 {
-			// This deposit tx doesn't have tokens available for unlock
+		amountToUndeposit := amountsToUndeposit[out.DepositTxID]
+		if amountToUndeposit == 0 {
+			// already undeposited all we needed from this deposit
 			continue
 		}
 
 		innerOut, ok := out.TransferableOut.(*secp256k1fx.TransferOutput)
-		if !ok {
-			// We only know how to clone secp256k1 outputs for now
+		if !ok { // should never happen for valid outs
+			continue
+		}
+
+		outOwnerID, err := txs.GetOutputOwnerID(innerOut)
+		if err != nil { // should never happen for valid outs
 			continue
 		}
 
 		inIntf, inSigners, err := kc.SpendMultiSig(innerOut, currentTimestamp, state)
-		if err != nil {
-			// We couldn't spend the output, so move on to the next one
+		if err != nil { // should never happen for valid outs
 			continue
 		}
 
 		in, ok := inIntf.(avax.TransferableIn)
 		if !ok { // should never happen
-			h.ctx.Log.Warn("wrong input type",
-				zap.String("expectedType", "avax.TransferableIn"),
-				zap.String("actualType", fmt.Sprintf("%T", inIntf)),
-			)
 			continue
 		}
 
-		// Add the input to the consumed inputs
 		ins = append(ins, &avax.TransferableInput{
 			UTXOID: avax.UTXOID{
 				TxID:        utxo.TxID,
@@ -783,50 +832,87 @@ func (h *handler) UnlockDeposit(
 			},
 		})
 
-		signers = append(signers, inSigners)
-
 		remainingValue := in.Amount()
-		amountToUnlock := math.Min(unlockableAmount, remainingValue)
-		remainingValue -= amountToUnlock
-		unlockableAmounts[out.DepositTxID] -= amountToUnlock
 
-		if newLockIDs := out.Unlock(locked.StateDeposited); newLockIDs.IsLocked() {
-			outs = append(outs, &avax.TransferableOutput{
-				Asset: avax.Asset{ID: h.ctx.AVAXAssetID},
-				Out: &locked.Out{
-					IDs: newLockIDs,
-					TransferableOut: &secp256k1fx.TransferOutput{
-						Amt:          amountToUnlock,
-						OutputOwners: innerOut.OutputOwners,
-					},
-				},
-			})
-		} else {
-			outs = append(outs, &avax.TransferableOutput{
-				Asset: avax.Asset{ID: h.ctx.AVAXAssetID},
-				Out: &secp256k1fx.TransferOutput{
-					Amt:          amountToUnlock,
-					OutputOwners: innerOut.OutputOwners,
-				},
-			})
+		// amount that will be undeposited from this input, always > 0
+		amountToUndeposit = math.Min(amountToUndeposit, remainingValue)
+		remainingValue -= amountToUndeposit
+
+		undepositOwner := ownerWithID{&innerOut.OutputOwners, outOwnerID}
+		producedLockIDs := out.Unlock(locked.StateDeposited)
+		if !producedLockIDs.IsLocked() && newUndepositOwner != nil {
+			undepositOwner = *newUndepositOwner
+		}
+
+		// undeposited amounts that will be produced by owner
+		// (either original utxo owner or newUndepositOwner)
+		ownerProducedUndepositedAmounts, ok := produced[undepositOwner.id]
+		if !ok {
+			ownerProducedUndepositedAmounts = &OwnerAmounts{
+				amounts:    make(map[locked.IDs]uint64),
+				secpOwners: undepositOwner.secpOwners,
+			}
+			produced[undepositOwner.id] = ownerProducedUndepositedAmounts
+		}
+
+		ownerProducedUndepositedAmounts.amounts[producedLockIDs], err = math.Add64(ownerProducedUndepositedAmounts.amounts[producedLockIDs], amountToUndeposit)
+		if err != nil {
+			return nil, nil, nil, nil, fmt.Errorf("failed to sum undeposit amount (ownerID: %s, lockIDs: %+v): %w", undepositOwner.id, producedLockIDs, err)
 		}
 
-		// This input had extra value, so some of it must be returned
 		if remainingValue > 0 {
+			// remaining deposited amount map that will be produced by original owner
+			ownerProducedDepositedAmounts, ok := produced[outOwnerID]
+			if !ok {
+				ownerProducedDepositedAmounts = &OwnerAmounts{
+					amounts:    make(map[locked.IDs]uint64),
+					secpOwners: &innerOut.OutputOwners,
+				}
+				produced[outOwnerID] = ownerProducedDepositedAmounts
+			}
+
+			ownerProducedDepositedAmounts.amounts[out.IDs], err = math.Add64(ownerProducedDepositedAmounts.amounts[out.IDs], remainingValue)
+			if err != nil {
+				return nil, nil, nil, nil, fmt.Errorf("failed to sum remaining amount (ownerID: %s, lockIDs: %+v): %w", outOwnerID, out.IDs, err)
+			}
+		}
+
+		amountsToUndeposit[out.DepositTxID] -= amountToUndeposit
+		if amountsToUndeposit[out.DepositTxID] == 0 {
+			delete(amountsToUndeposit, out.DepositTxID)
+		}
+
+		owners = append(owners, &innerOut.OutputOwners)
+		signers = append(signers, inSigners)
+	}
+
+	if len(amountsToUndeposit) > 0 {
+		return nil, nil, nil, nil, errInsufficientUnlockable // should never happen
+	}
+
+	for _, ownerAmounts := range produced {
+		for lockIDs, amount := range ownerAmounts.amounts {
+			var out avax.TransferableOut = &secp256k1fx.TransferOutput{
+				Amt:          amount,
+				OutputOwners: *ownerAmounts.secpOwners,
+			}
+			if lockIDs.IsLocked() {
+				out = &locked.Out{
+					IDs:             lockIDs,
+					TransferableOut: out,
+				}
+			}
 			outs = append(outs, &avax.TransferableOutput{
 				Asset: avax.Asset{ID: h.ctx.AVAXAssetID},
-				Out: &locked.Out{
-					IDs: out.IDs,
-					TransferableOut: &secp256k1fx.TransferOutput{
-						Amt:          remainingValue,
-						OutputOwners: innerOut.OutputOwners,
-					},
-				},
+				Out:   out,
 			})
 		}
 	}
 
-	return ins, outs, signers, nil
+	avax.SortTransferableInputsWithSigners(ins, signers) // sort inputs and keys
+	avax.SortTransferableOutputs(outs, txs.Codec)        // sort outputs
+
+	return ins, outs, signers, owners, nil
 }
 
 func (h *handler) VerifyLock(
@@ -1499,6 +1585,48 @@ func (h *handler) verifyUnlockDepositedUTXOsBeforeCairo(
 	return nil
 }
 
+type ownerWithID struct {
+	secpOwners *secp256k1fx.OutputOwners
+	id         ids.ID
+}
+
+func (h *handler) GetOwnerWithID(aliasGetter secp256k1fx.AliasGetter, secpOwner *secp256k1fx.OutputOwners) (*ownerWithID, error) {
+	if secpOwner == nil {
+		return nil, nil
+	}
+
+	if err := secpOwner.Verify(); err != nil {
+		return nil, fmt.Errorf("invalid owner: %w", err)
+	}
+
+	if err := h.fx.VerifyMultisigOwner(secpOwner, aliasGetter); err != nil {
+		return nil, fmt.Errorf("%w: %v", errCompositeMultisig, err)
+	}
+
+	id, err := txs.GetOwnerID(secpOwner)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get ownerID: %w", err)
+	}
+
+	return &ownerWithID{secpOwner, id}, nil
+}
+
+// will not retain order by lockTxID if lockState is unlocked
+// Sorts utxos in descending order with following rules:
+//
+// - utxo with allowedAssetID go first
+//
+// - if lockState is unlocked, unlocked utxo go first
+//
+// - utxo that are not locked with lockState go first
+//
+// - utxo with smaller opposite of lockState lockTxID go last (so we'll have not locked utxos in the end)
+//
+// - utxo with smaller amount go first
+func sortUTXOs(utxos []*avax.UTXO, allowedAssetID ids.ID, lockState locked.State) {
+	sort.Sort(&innerSortUTXOs{utxos: utxos, allowedAssetID: allowedAssetID, lockState: lockState})
+}
+
 type innerSortUTXOs struct {
 	utxos          []*avax.UTXO
 	allowedAssetID ids.ID
@@ -1587,8 +1715,27 @@ func (sort *innerSortUTXOs) Swap(i, j int) {
 }
 
 // will not retain order by lockTxID if lockState is unlocked
-func sortUTXOs(utxos []*avax.UTXO, allowedAssetID ids.ID, lockState locked.State) {
-	sort.Sort(&innerSortUTXOs{utxos: utxos, allowedAssetID: allowedAssetID, lockState: lockState})
+// Sorts utxos in descending order with given Less(i,j) func.
+func sortUTXOsWithFunc(utxos []*avax.UTXO, less func(iUTXO *avax.UTXO, jUTXO *avax.UTXO) bool) {
+	sort.Sort(&innerSortUTXOs1{utxos: utxos, less: less})
+}
+
+type innerSortUTXOs1 struct {
+	utxos []*avax.UTXO
+	less  func(iUTXO *avax.UTXO, jUTXO *avax.UTXO) bool
+}
+
+func (sort *innerSortUTXOs1) Less(i, j int) bool {
+	return sort.less(sort.utxos[i], sort.utxos[j])
+}
+
+func (sort *innerSortUTXOs1) Len() int {
+	return len(sort.utxos)
+}
+
+func (sort *innerSortUTXOs1) Swap(i, j int) {
+	u := sort.utxos
+	u[j], u[i] = u[i], u[j]
 }
 
 func getUTXOs(utxoDB avax.UTXOGetter, ins []*avax.TransferableInput) ([]*avax.UTXO, error) {
diff --git a/vms/platformvm/utxo/camino_locked_test.go b/vms/platformvm/utxo/camino_locked_test.go
index 02470e8d2442..25e0eced83dd 100644
--- a/vms/platformvm/utxo/camino_locked_test.go
+++ b/vms/platformvm/utxo/camino_locked_test.go
@@ -11,7 +11,7 @@ import (
 
 	"github.com/ava-labs/avalanchego/database"
 	"github.com/ava-labs/avalanchego/ids"
-	"github.com/ava-labs/avalanchego/snow"
+	"github.com/ava-labs/avalanchego/utils"
 	"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
 	"github.com/ava-labs/avalanchego/utils/set"
 	"github.com/ava-labs/avalanchego/vms/components/avax"
@@ -146,8 +146,6 @@ func TestUnlockUTXOs(t *testing.T) {
 }
 
 func TestLock(t *testing.T) {
-	ctx := snow.DefaultContextTest()
-
 	utxoOwner := secp256k1fx.OutputOwners{
 		Locktime:  0,
 		Threshold: 1,
@@ -373,7 +371,7 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateBonded,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedErr: errInsufficientBalance,
 		},
@@ -384,7 +382,7 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateDeposited,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedErr: errInsufficientBalance,
 		},
@@ -395,7 +393,7 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateBonded,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 10, utxoOwner, ids.Empty, existingTxID, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 10, utxoOwner, ids.Empty, existingTxID, true),
 			},
 			expectedErr: errInsufficientBalance,
 		},
@@ -406,7 +404,7 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateDeposited,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 1, utxoOwner, existingTxID, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 1, utxoOwner, existingTxID, ids.Empty, true),
 			},
 			expectedErr: errInsufficientBalance,
 		},
@@ -434,8 +432,8 @@ func TestLock(t *testing.T) {
 			to:                 &recipientOwner,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 2, utxoOwner, ids.Empty, ids.Empty, true),
-				generate.UTXO(ids.ID{9, 9}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, existingTxID, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 2, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{9, 9}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, existingTxID, true),
 			},
 			expectedErr: errInsufficientBalance,
 		},
@@ -448,7 +446,7 @@ func TestLock(t *testing.T) {
 			to:                 &recipientOwner,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -456,8 +454,8 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 1, recipientOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 1, recipientOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]},
@@ -473,7 +471,7 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateUnlocked,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -481,7 +479,7 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]},
@@ -498,7 +496,7 @@ func TestLock(t *testing.T) {
 			change:             &changeOwner,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -506,8 +504,8 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 1, utxoOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 1, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]},
@@ -523,8 +521,8 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateBonded,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
-				generate.UTXO(ids.ID{9, 9}, ctx.AVAXAssetID, 10, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{9, 9}, test.AVAXAssetID, 10, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -533,8 +531,8 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 9, utxoOwner, ids.Empty, locked.ThisTxID),
+				generate.Out(test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 9, utxoOwner, ids.Empty, locked.ThisTxID),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]}, {test.Keys[0]},
@@ -550,8 +548,8 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateBonded,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
-				generate.UTXO(ids.ID{9, 9}, ctx.AVAXAssetID, 10, utxoOwner, existingTxID, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{9, 9}, test.AVAXAssetID, 10, utxoOwner, existingTxID, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -560,9 +558,9 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 1, utxoOwner, existingTxID, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 9, utxoOwner, existingTxID, locked.ThisTxID),
+				generate.Out(test.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 1, utxoOwner, existingTxID, ids.Empty),
+				generate.Out(test.AVAXAssetID, 9, utxoOwner, existingTxID, locked.ThisTxID),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]}, {test.Keys[0]},
@@ -578,8 +576,8 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateDeposited,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
-				generate.UTXO(ids.ID{9, 9}, ctx.AVAXAssetID, 10, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{9, 9}, test.AVAXAssetID, 10, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -588,8 +586,8 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 9, utxoOwner, locked.ThisTxID, ids.Empty),
+				generate.Out(test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 9, utxoOwner, locked.ThisTxID, ids.Empty),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]}, {test.Keys[0]},
@@ -605,8 +603,8 @@ func TestLock(t *testing.T) {
 			appliedLockState:   locked.StateDeposited,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
-				generate.UTXO(ids.ID{9, 9}, ctx.AVAXAssetID, 10, utxoOwner, ids.Empty, existingTxID, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{9, 9}, test.AVAXAssetID, 10, utxoOwner, ids.Empty, existingTxID, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -615,9 +613,9 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 1, utxoOwner, ids.Empty, existingTxID),
-				generate.Out(ctx.AVAXAssetID, 9, utxoOwner, locked.ThisTxID, existingTxID),
+				generate.Out(test.AVAXAssetID, 4, utxoOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 1, utxoOwner, ids.Empty, existingTxID),
+				generate.Out(test.AVAXAssetID, 9, utxoOwner, locked.ThisTxID, existingTxID),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]}, {test.Keys[0]},
@@ -635,7 +633,7 @@ func TestLock(t *testing.T) {
 			change:             &changeOwner,
 			keys:               []*secp256k1.PrivateKey{test.Keys[0]},
 			utxos: []*avax.UTXO{
-				generate.UTXO(ids.ID{8, 8}, ctx.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
+				generate.UTXO(ids.ID{8, 8}, test.AVAXAssetID, 5, utxoOwner, ids.Empty, ids.Empty, true),
 			},
 			expectedIns: func(utxos []*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
@@ -643,8 +641,8 @@ func TestLock(t *testing.T) {
 				}
 			},
 			expectedOuts: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, 1, recipientOwner, locked.ThisTxID, ids.Empty),
+				generate.Out(test.AVAXAssetID, 3, changeOwner, ids.Empty, ids.Empty),
+				generate.Out(test.AVAXAssetID, 1, recipientOwner, locked.ThisTxID, ids.Empty),
 			},
 			expectedSigners: [][]*secp256k1.PrivateKey{
 				{test.Keys[0]},
@@ -718,7 +716,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 	tests := map[string]struct {
 		state            func(*gomock.Controller) *state.MockChain
 		utxos            []*avax.UTXO
-		ins              func(*testing.T, []*avax.UTXO) []*avax.TransferableInput
+		ins              func(*testing.T, ...*avax.UTXO) []*avax.TransferableInput
 		outs             []*avax.TransferableOutput
 		creds            []verify.Verifiable
 		mintedAmount     uint64
@@ -733,7 +731,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 		},
 		"Fail: Inputs length not equal credentials length": {
 			state: noMsigState,
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.In(test.AVAXAssetID, 10, ids.Empty, ids.Empty, []uint32{0}),
 				}
@@ -744,7 +742,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 		},
 		"Fail: Inputs length not equal UTXOs length": {
 			state: noMsigState,
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.In(test.AVAXAssetID, 10, ids.Empty, ids.Empty, []uint32{0}),
 				}
@@ -778,7 +776,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 			utxos: []*avax.UTXO{
 				generate.UTXO(ids.ID{1}, test.AVAXAssetID, 10, outputOwners1, ids.Empty, ids.Empty, true),
 			},
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.In(test.OtherAssetID, 10, ids.Empty, ids.Empty, []uint32{0}),
 				}
@@ -823,7 +821,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 			utxos: []*avax.UTXO{
 				generate.UTXO(ids.ID{1}, test.AVAXAssetID, 10, outputOwners1, ids.Empty, ids.Empty, true),
 			},
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.StakeableIn(test.AVAXAssetID, 10, 0, []uint32{0}),
 				}
@@ -857,7 +855,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 			utxos: []*avax.UTXO{
 				generate.UTXO(ids.ID{1}, test.AVAXAssetID, 10, outputOwners1, depositTxID1, ids.Empty, true),
 			},
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.In(test.AVAXAssetID, 10, depositTxID2, ids.Empty, []uint32{0}),
 				}
@@ -871,7 +869,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 			utxos: []*avax.UTXO{
 				generate.UTXO(ids.ID{1}, test.AVAXAssetID, 10, outputOwners1, depositTxID1, ids.Empty, true),
 			},
-			ins: func(_ *testing.T, utxos []*avax.UTXO) []*avax.TransferableInput {
+			ins: func(_ *testing.T, utxos ...*avax.UTXO) []*avax.TransferableInput {
 				return []*avax.TransferableInput{
 					generate.In(test.AVAXAssetID, 10, ids.Empty, ids.Empty, []uint32{0}),
 				}
@@ -1110,7 +1108,7 @@ func TestVerifyLockUTXOs(t *testing.T) {
 
 			var ins []*avax.TransferableInput
 			if tt.ins != nil {
-				ins = tt.ins(t, tt.utxos)
+				ins = tt.ins(t, tt.utxos...)
 			}
 
 			err := testHandler.VerifyLockUTXOs(
@@ -1246,144 +1244,434 @@ func TestGetDepositUnlockableAmounts(t *testing.T) {
 }
 
 func TestUnlockDeposit(t *testing.T) {
-	testHandler := defaultCaminoHandler(t)
-	ctx := testHandler.ctx
-	testHandler.clk.Set(time.Now())
+	depositOwnerKey1, depositOwnerAddr1, depositOwner1 := generate.KeyAndOwner(t, test.Keys[0])
+	depositOwnerKey2, depositOwnerAddr2, depositOwner2 := generate.KeyAndOwner(t, test.Keys[1])
 
-	testID := ids.GenerateTestID()
-	txID := ids.GenerateTestID()
-	depositedAmount := uint64(2000)
-	outputOwners := secp256k1fx.OutputOwners{
-		Locktime:  0,
-		Threshold: 1,
-		Addrs:     []ids.ShortID{test.FundedKeys[0].Address()},
-	}
-	depositedUTXOs := []*avax.UTXO{
-		generate.UTXO(txID, ctx.AVAXAssetID, depositedAmount, outputOwners, testID, ids.Empty, true),
-	}
+	newOwner := &secp256k1fx.OutputOwners{Threshold: 1, Addrs: []ids.ShortID{{1}}}
 
-	nowMinus10m := uint64(testHandler.clk.Time().Add(-10 * time.Minute).Unix())
+	depositTxID1 := ids.ID{2}
+	depositTxID2 := ids.ID{3}
+	bondTxID := ids.ID{4}
+	offerID1 := ids.ID{5}
+	offerID2 := ids.ID{6}
 
-	type args struct {
-		state        func(*gomock.Controller) state.Chain
-		keys         []*secp256k1.PrivateKey
-		depositTxIDs []ids.ID
+	depositAmount1 := uint64(2000)
+	depositAmount2 := uint64(5000)
+	deposit1UTXO1Amount := depositAmount1 / 4
+	deposit1UTXO2Amount := depositAmount1 / 4
+	deposit1UTXO3Amount := depositAmount1 / 2
+	deposit2UTXO1Amount := depositAmount2 / 4
+	deposit2UTXO2Amount := depositAmount2 / 4
+	deposit2BondedUTXO3Amount := depositAmount2 / 4
+	deposit2BondedUTXO4Amount := depositAmount2 / 4
+
+	deposit1UTXO1 := generate.UTXO(ids.ID{7}, test.AVAXAssetID, deposit1UTXO1Amount, depositOwner1, depositTxID1, ids.Empty, true)       // 25% of depositAmount1
+	deposit1UTXO2 := generate.UTXO(ids.ID{8}, test.AVAXAssetID, deposit1UTXO2Amount, depositOwner1, depositTxID1, ids.Empty, true)       // 25% of depositAmount1
+	deposit1UTXO3 := generate.UTXO(ids.ID{9}, test.AVAXAssetID, deposit1UTXO3Amount, depositOwner1, depositTxID1, ids.Empty, true)       // 50% of depositAmount1
+	deposit1BondedUTXO3 := generate.UTXO(ids.ID{10}, test.AVAXAssetID, deposit1UTXO3Amount, depositOwner1, depositTxID1, bondTxID, true) // 50% of depositAmount1, alternate utxo
+	deposit1UTXOs := []*avax.UTXO{deposit1UTXO1, deposit1UTXO2, deposit1UTXO3}
+	deposit1UTXOsWithBond := []*avax.UTXO{deposit1UTXO1, deposit1UTXO2, deposit1BondedUTXO3} // alternate utxo set
+
+	deposit2UTXO1 := generate.UTXO(ids.ID{11}, test.AVAXAssetID, deposit2UTXO1Amount, depositOwner2, depositTxID2, ids.Empty, true)            // 25% of depositAmount2
+	deposit2UTXO2 := generate.UTXO(ids.ID{12}, test.AVAXAssetID, deposit2UTXO2Amount, depositOwner2, depositTxID2, ids.Empty, true)            // 25% of depositAmount2
+	deposit2BondedUTXO3 := generate.UTXO(ids.ID{13}, test.AVAXAssetID, deposit2BondedUTXO3Amount, depositOwner2, depositTxID2, bondTxID, true) // 25% of depositAmount2
+	deposit2BondedUTXO4 := generate.UTXO(ids.ID{14}, test.AVAXAssetID, deposit2BondedUTXO4Amount, depositOwner2, depositTxID2, bondTxID, true) // 25% of depositAmount2
+	deposit2UTXOs := []*avax.UTXO{deposit2UTXO1, deposit2UTXO2, deposit2BondedUTXO3, deposit2BondedUTXO4}
+
+	depositUnlockDuration1 := uint32(100)
+	depositUnlockDuration2 := uint32(200)
+
+	offer1 := &deposit.Offer{UnlockPeriodDuration: depositUnlockDuration1}
+	offer2 := &deposit.Offer{UnlockPeriodDuration: depositUnlockDuration2}
+
+	deposit1 := func(now time.Time, unlockPercent uint32) *deposit.Deposit {
+		return &deposit.Deposit{
+			DepositOfferID: offerID1,
+			Start:          uint64(now.Add(-time.Duration(depositUnlockDuration1*unlockPercent/100) * time.Second).Unix()),
+			Duration:       depositUnlockDuration1,
+			Amount:         depositAmount1,
+		}
 	}
-	sigIndices := []uint32{0}
+
+	deposit2 := func(now time.Time, unlockPercent uint32) *deposit.Deposit {
+		return &deposit.Deposit{
+			DepositOfferID: offerID2,
+			Start:          uint64(now.Add(-time.Duration(depositUnlockDuration2*unlockPercent/100) * time.Second).Unix()),
+			Duration:       depositUnlockDuration2,
+			Amount:         depositAmount2,
+		}
+	}
+
+	// verifying that deposit funcs are correct
+	now := time.Now()
+	require.Equal(t, depositAmount1/2, deposit1(now, 50).UnlockableAmountByTime(offer1, now))
+	require.Equal(t, depositAmount2/2, deposit2(now, 50).UnlockableAmountByTime(offer2, now))
 
 	tests := map[string]struct {
-		args  args
-		want  []*avax.TransferableInput
-		want1 []*avax.TransferableOutput
-		want2 [][]*secp256k1.PrivateKey
-		err   error
+		state              func(*gomock.Controller, time.Time, []*deposit.Deposit) state.Chain
+		keys               []*secp256k1.PrivateKey
+		amountsToUndeposit func(time.Time, []*deposit.Deposit) map[ids.ID]uint64
+		undepositToOwner   *secp256k1fx.OutputOwners
+		deposits           func(time.Time) []*deposit.Deposit
+		expectedIns        []*avax.TransferableInput
+		expectedOuts       func(time.Time, []*deposit.Deposit) []*avax.TransferableOutput
+		expectedSigners    [][]*secp256k1.PrivateKey
+		expectedOwners     []*secp256k1fx.OutputOwners
+		expectedErr        error
 	}{
-		"Error retrieving unlockable amounts": {
-			args: args{
-				state: func(ctrl *gomock.Controller) state.Chain {
-					s := state.NewMockChain(ctrl)
-					deposit1 := deposit.Deposit{
-						DepositOfferID: testID,
-						Start:          nowMinus10m,
-						Duration:       uint32((10 * time.Minute).Seconds()),
-					}
-					depositTxSet := set.NewSet[ids.ID](1)
-					depositTxSet.Add(testID)
-
-					s.EXPECT().GetDeposit(testID).Return(&deposit1, nil)
-					s.EXPECT().GetDepositOffer(testID).Return(&deposit.Offer{
-						Start:                nowMinus10m,
-						UnlockPeriodDuration: uint32((10 * time.Minute).Seconds()),
-					}, nil)
-					s.EXPECT().LockedUTXOs(depositTxSet, gomock.Any(), locked.StateDeposited).Return(nil, fmt.Errorf("%w: %s", state.ErrMissingParentState, testID))
-					return s
-				},
-				keys:         test.FundedKeys,
-				depositTxIDs: []ids.ID{testID},
+		"Syntactically invalid to-owner": {
+			undepositToOwner: &secp256k1fx.OutputOwners{
+				Addrs:     []ids.ShortID{{100}},
+				Threshold: 2,
 			},
-			err: fmt.Errorf("%w: %s", state.ErrMissingParentState, testID),
+			expectedErr: errInvalidToOwner,
 		},
-		"Successful unlock of 50% deposited funds": {
-			args: args{
-				state: func(ctrl *gomock.Controller) state.Chain {
-					s := state.NewMockChain(ctrl)
-					deposit1 := deposit.Deposit{
-						DepositOfferID: testID,
-						Start:          nowMinus10m,
-						Duration:       uint32((15 * time.Minute).Seconds()),
-						Amount:         depositedAmount,
-					}
-					depositTxSet := set.NewSet[ids.ID](1)
-					depositTxSet.Add(testID)
-
-					s.EXPECT().GetDeposit(testID).Return(&deposit1, nil)
-					s.EXPECT().GetDepositOffer(testID).Return(&deposit.Offer{
-						Start:                nowMinus10m,
-						UnlockPeriodDuration: uint32((10 * time.Minute).Seconds()),
-					}, nil)
-					s.EXPECT().LockedUTXOs(depositTxSet, gomock.Any(), locked.StateDeposited).Return(depositedUTXOs, nil)
-					s.EXPECT().GetMultisigAlias(test.FundedKeys[0].Address()).Return(nil, database.ErrNotFound)
-					return s
-				},
-				keys:         []*secp256k1.PrivateKey{test.FundedKeys[0]},
-				depositTxIDs: []ids.ID{testID},
+		"Nested multisig to-owner": {
+			state: func(ctrl *gomock.Controller, _ time.Time, _ []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.VerifyMultisigOwner(
+					t, s, newOwner,
+					[]ids.ShortID{newOwner.Addrs[0], {100}},
+					[]*multisig.AliasWithNonce{
+						{Alias: multisig.Alias{
+							Owners: &secp256k1fx.OutputOwners{Addrs: []ids.ShortID{{100}}},
+						}},
+						{Alias: multisig.Alias{Owners: &secp256k1fx.OutputOwners{}}},
+					},
+					false,
+				)
+				return s
+			},
+			undepositToOwner: newOwner,
+			expectedErr:      errInvalidToOwner,
+		},
+		"Composite multisig to-owner": {
+			state: func(ctrl *gomock.Controller, _ time.Time, _ []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.VerifyMultisigOwner(
+					t, s, newOwner,
+					[]ids.ShortID{{100}},
+					[]*multisig.AliasWithNonce{{Alias: multisig.Alias{Owners: &secp256k1fx.OutputOwners{}}}},
+					false,
+				)
+				return s
+			},
+			undepositToOwner: &secp256k1fx.OutputOwners{
+				Addrs:     []ids.ShortID{{100}, {101}},
+				Threshold: 1,
 			},
-			want: []*avax.TransferableInput{
-				generate.InFromUTXO(t, depositedUTXOs[0], sigIndices, false),
+			expectedErr: errInvalidToOwner,
+		},
+		"Non-existing deposit": {
+			state: func(ctrl *gomock.Controller, _ time.Time, _ []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				s.EXPECT().GetDeposit(depositTxID1).Return(nil, database.ErrNotFound)
+				return s
 			},
-			want1: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, depositedAmount/2, outputOwners, ids.Empty, ids.Empty),
-				generate.Out(ctx.AVAXAssetID, depositedAmount/2, outputOwners, testID, ids.Empty),
+			amountsToUndeposit: func(now time.Time, _ []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: 0}
 			},
-			want2: [][]*secp256k1.PrivateKey{{test.FundedKeys[0]}},
+			expectedErr: errFailToGetDeposit,
 		},
-		"Successful full unlock": {
-			args: args{
-				state: func(ctrl *gomock.Controller) state.Chain {
-					s := state.NewMockChain(ctrl)
-					deposit1 := deposit.Deposit{
-						DepositOfferID: testID,
-						Start:          nowMinus10m,
-						Duration:       uint32((10 * time.Minute).Seconds()),
-						Amount:         depositedAmount,
-					}
-					depositTxSet := set.NewSet[ids.ID](1)
-					depositTxSet.Add(testID)
+		"Not enough unlockable": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				return s
+			},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now) + 1} // 35% unlockable + 1
+			},
+			deposits:    func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 35)} }, // 35% unlockable
+			expectedErr: errInsufficientUnlockable,
+		},
+		"OK: all available unlockable (35%), use 2 out of 3 utxos": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}},
+					locked.StateDeposited,
+				).Return(deposit1UTXOs, nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound).Times(2) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now)} // 35% unlockable
+			},
+			deposits:    func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 35)} }, // 35% unlockable
+			expectedIns: generate.InsFromUTXOs(t, deposit1UTXO1, deposit1UTXO2),
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				unlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now)             // 35% unlockable
+				remainingAmount := deposit1UTXO1Amount + deposit1UTXO2Amount - unlockedAmount // utxo3 is not used
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, unlockedAmount, depositOwner1, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, remainingAmount, depositOwner1, depositTxID1, ids.Empty),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{{depositOwnerKey1}, {depositOwnerKey1}},
+			expectedOwners:  []*secp256k1fx.OutputOwners{&depositOwner1, &depositOwner1},
+		},
+		"OK: almost all available unlockable (80%-1), use 3 out of 3 utxos": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}},
+					locked.StateDeposited,
+				).Return(deposit1UTXOs, nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound).Times(3) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now) - 1} // 80% unlockable - 1
+			},
+			deposits:    func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 80)} }, // 80% unlockable
+			expectedIns: generate.InsFromUTXOs(t, deposit1UTXOs...),
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				unlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now) - 1 // 80% unlockable - 1
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, unlockedAmount, depositOwner1, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, depositAmount1-unlockedAmount, depositOwner1, depositTxID1, ids.Empty),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{{depositOwnerKey1}, {depositOwnerKey1}, {depositOwnerKey1}},
+			expectedOwners:  []*secp256k1fx.OutputOwners{&depositOwner1, &depositOwner1, &depositOwner1},
+		},
+		"OK: almost all available unlockable (80%-1), use 3 out of 3 utxos, new owner": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				s.EXPECT().GetMultisigAlias(newOwner.Addrs[0]).Return(nil, database.ErrNotFound)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}},
+					locked.StateDeposited,
+				).Return(deposit1UTXOs, nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound).Times(3) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now) - 1} // 80% unlockable - 1
+			},
+			undepositToOwner: newOwner,
+			deposits:         func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 80)} }, // 80% unlockable
+			expectedIns:      generate.InsFromUTXOs(t, deposit1UTXOs...),
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				unlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now) - 1 // 80% unlockable - 1
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, unlockedAmount, *newOwner, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, depositAmount1-unlockedAmount, depositOwner1, depositTxID1, ids.Empty),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{{depositOwnerKey1}, {depositOwnerKey1}, {depositOwnerKey1}},
+			expectedOwners:  []*secp256k1fx.OutputOwners{&depositOwner1, &depositOwner1, &depositOwner1},
+		},
+		"OK: 1 nCAM, use 1 out of 3 utxo": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}},
+					locked.StateDeposited,
+				).Return(deposit1UTXOs, nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: 1}
+			},
+			deposits:    func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 35)} }, // 35% unlockable
+			expectedIns: generate.InsFromUTXOs(t, deposit1UTXO1),
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, 1, depositOwner1, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, deposit1UTXO1Amount-1, depositOwner1, depositTxID1, ids.Empty),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{{depositOwnerKey1}},
+			expectedOwners:  []*secp256k1fx.OutputOwners{&depositOwner1},
+		},
+		"OK: all from 1 utxo using all unlockable (25%)": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1}, deposits, []*deposit.Offer{offer1})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}},
+					locked.StateDeposited,
+				).Return([]*avax.UTXO{deposit1UTXO1}, nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now)} // 25% unlockable
+			},
+			deposits:    func(now time.Time) []*deposit.Deposit { return []*deposit.Deposit{deposit1(now, 25)} }, // 25% unlockable
+			expectedIns: generate.InsFromUTXOs(t, deposit1UTXO1),
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				unlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now) // 25% unlockable
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, unlockedAmount, depositOwner1, ids.Empty, ids.Empty),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{{depositOwnerKey1}},
+			expectedOwners:  []*secp256k1fx.OutputOwners{&depositOwner1},
+		},
+		"OK: 50%-1 and 2 out of 3 utxos from deposit1, 99%-1 from deposit2, 4 out of 4 utxos, half bond": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1, depositTxID2}, deposits, []*deposit.Offer{offer1, offer2})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}, depositTxID2: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}, depositOwnerAddr2: struct{}{}},
+					locked.StateDeposited,
+				).Return(append(deposit1UTXOs, deposit2UTXOs...), nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound).Times(2) // per number of used utxos
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr2).Return(nil, database.ErrNotFound).Times(4) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1, depositOwnerKey2},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{
+					depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now) - 1, // 50% unlockable - 1
+					depositTxID2: deposits[1].UnlockableAmountByTime(offer2, now) - 1, // 99% unlockable - 1
+				}
+			},
+			deposits: func(now time.Time) []*deposit.Deposit {
+				return []*deposit.Deposit{deposit1(now, 50), deposit2(now, 99)} // 50% and 99% unlockable
+			},
+			expectedIns: generate.InsFromUTXOs(t, append([]*avax.UTXO{deposit1UTXO1, deposit1UTXO2}, deposit2UTXOs...)...), // all 6 utxos except deposit1UTXO3
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				deposit1UnlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now) - 1          // 50% unlockable - 1
+				remainingAmount1 := deposit1UTXO1Amount + deposit1UTXO2Amount - deposit1UnlockedAmount // utxo3 is not used
 
-					s.EXPECT().GetDeposit(testID).Return(&deposit1, nil)
-					s.EXPECT().GetDepositOffer(testID).Return(&deposit.Offer{
-						Start:                nowMinus10m,
-						UnlockPeriodDuration: uint32((2 * time.Minute).Seconds()),
-					}, nil)
-					s.EXPECT().LockedUTXOs(depositTxSet, gomock.Any(), locked.StateDeposited).Return(depositedUTXOs, nil)
-					s.EXPECT().GetMultisigAlias(test.FundedKeys[0].Address()).Return(nil, database.ErrNotFound)
-					return s
-				},
-				keys:         []*secp256k1.PrivateKey{test.FundedKeys[0]},
-				depositTxIDs: []ids.ID{testID},
+				deposit2UnlockedAmount := deposits[1].UnlockableAmountByTime(offer2, now) - 1 // 99% unlockable - 1
+				unlockedAmount2NotBonded := deposit2UTXO1Amount + deposit2UTXO2Amount         // 50% unlockable, all not bonded utxos
+				require.GreaterOrEqual(t, deposit2UnlockedAmount, unlockedAmount2NotBonded)
+				unlockedAmount2Bonded := deposit2UnlockedAmount - unlockedAmount2NotBonded // 49% unlockable - 1, all bonded utxos
+				remainingAmount2 := depositAmount2 - deposit2UnlockedAmount                // 1% deposited, bonded
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, deposit1UnlockedAmount, depositOwner1, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, unlockedAmount2NotBonded, depositOwner2, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, unlockedAmount2Bonded, depositOwner2, ids.Empty, bondTxID),
+					generate.Out(test.AVAXAssetID, remainingAmount1, depositOwner1, depositTxID1, ids.Empty),
+					generate.Out(test.AVAXAssetID, remainingAmount2, depositOwner2, depositTxID2, bondTxID),
+				}
 			},
-			want: []*avax.TransferableInput{
-				generate.InFromUTXO(t, depositedUTXOs[0], sigIndices, false),
+			expectedSigners: [][]*secp256k1.PrivateKey{
+				{depositOwnerKey1}, {depositOwnerKey1}, // deposit1 utxos
+				{depositOwnerKey2}, {depositOwnerKey2}, {depositOwnerKey2}, {depositOwnerKey2}, // deposit2 utxos
 			},
-			want1: []*avax.TransferableOutput{
-				generate.Out(ctx.AVAXAssetID, depositedAmount, outputOwners, ids.Empty, ids.Empty),
+			expectedOwners: []*secp256k1fx.OutputOwners{
+				&depositOwner1, &depositOwner1,
+				&depositOwner2, &depositOwner2, &depositOwner2, &depositOwner2,
+			},
+		},
+		"OK: 99%-1 and 2 out of 3 utxos from deposit1, 99%-1 from deposit2, 4 out of 4 utxos, half bond, new owner": {
+			state: func(ctrl *gomock.Controller, now time.Time, deposits []*deposit.Deposit) state.Chain {
+				s := state.NewMockChain(ctrl)
+				s.EXPECT().GetMultisigAlias(newOwner.Addrs[0]).Return(nil, database.ErrNotFound)
+				expect.GetDepositUnlockableAmounts(t, s, []ids.ID{depositTxID1, depositTxID2}, deposits, []*deposit.Offer{offer1, offer2})
+				s.EXPECT().LockedUTXOs(
+					set.Set[ids.ID]{depositTxID1: struct{}{}, depositTxID2: struct{}{}},
+					set.Set[ids.ShortID]{depositOwnerAddr1: struct{}{}, depositOwnerAddr2: struct{}{}},
+					locked.StateDeposited,
+				).Return(append(deposit1UTXOsWithBond, deposit2UTXOs...), nil)
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr1).Return(nil, database.ErrNotFound).Times(3) // per number of used utxos
+				s.EXPECT().GetMultisigAlias(depositOwnerAddr2).Return(nil, database.ErrNotFound).Times(4) // per number of used utxos
+				return s
+			},
+			keys: []*secp256k1.PrivateKey{depositOwnerKey1, depositOwnerKey2},
+			amountsToUndeposit: func(now time.Time, deposits []*deposit.Deposit) map[ids.ID]uint64 {
+				return map[ids.ID]uint64{
+					depositTxID1: deposits[0].UnlockableAmountByTime(offer1, now) - 1, // 99% unlockable - 1
+					depositTxID2: deposits[1].UnlockableAmountByTime(offer2, now) - 1, // 99% unlockable - 1
+				}
+			},
+			undepositToOwner: newOwner,
+			deposits: func(now time.Time) []*deposit.Deposit {
+				return []*deposit.Deposit{deposit1(now, 99), deposit2(now, 99)} // 99% and 99% unlockable
+			},
+			expectedIns: generate.InsFromUTXOs(t, append(deposit1UTXOsWithBond, deposit2UTXOs...)...), // all 7 utxos
+			expectedOuts: func(now time.Time, deposits []*deposit.Deposit) []*avax.TransferableOutput {
+				deposit1UnlockedAmount := deposits[0].UnlockableAmountByTime(offer1, now) - 1 // 99% unlockable - 1
+				unlockedAmount1NotBonded := deposit1UTXO1Amount + deposit1UTXO2Amount         // 50% unlockable, all not bonded utxos
+				require.GreaterOrEqual(t, deposit1UnlockedAmount, unlockedAmount1NotBonded)
+				unlockedAmount1Bonded := deposit1UnlockedAmount - unlockedAmount1NotBonded // 49% unlockable - 1, all bonded utxos
+				remainingAmount1 := depositAmount1 - deposit1UnlockedAmount                // 1% deposited, bonded
+
+				deposit2UnlockedAmount := deposits[1].UnlockableAmountByTime(offer2, now) - 1 // 99% unlockable - 1
+				unlockedAmount2NotBonded := deposit2UTXO1Amount + deposit2UTXO2Amount         // 50% unlockable, all not bonded utxos
+				require.GreaterOrEqual(t, deposit2UnlockedAmount, unlockedAmount2NotBonded)
+				unlockedAmount2Bonded := deposit2UnlockedAmount - unlockedAmount2NotBonded // 49% unlockable - 1, all bonded utxos
+				remainingAmount2 := depositAmount2 - deposit2UnlockedAmount                // 1% deposited, bonded
+				return []*avax.TransferableOutput{
+					generate.Out(test.AVAXAssetID, unlockedAmount1NotBonded+unlockedAmount2NotBonded, *newOwner, ids.Empty, ids.Empty),
+					generate.Out(test.AVAXAssetID, unlockedAmount1Bonded, depositOwner1, ids.Empty, bondTxID),
+					generate.Out(test.AVAXAssetID, unlockedAmount2Bonded, depositOwner2, ids.Empty, bondTxID),
+					generate.Out(test.AVAXAssetID, remainingAmount1, depositOwner1, depositTxID1, bondTxID),
+					generate.Out(test.AVAXAssetID, remainingAmount2, depositOwner2, depositTxID2, bondTxID),
+				}
+			},
+			expectedSigners: [][]*secp256k1.PrivateKey{
+				{depositOwnerKey1}, {depositOwnerKey1}, {depositOwnerKey1}, // deposit1 utxos
+				{depositOwnerKey2}, {depositOwnerKey2}, {depositOwnerKey2}, {depositOwnerKey2}, // deposit2 utxos
+			},
+			expectedOwners: []*secp256k1fx.OutputOwners{
+				&depositOwner1, &depositOwner1, &depositOwner1,
+				&depositOwner2, &depositOwner2, &depositOwner2, &depositOwner2,
 			},
-			want2: [][]*secp256k1.PrivateKey{{test.FundedKeys[0]}},
 		},
 	}
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
-			ctrl := gomock.NewController(t)
-			got, got1, got2, err := testHandler.UnlockDeposit(tt.args.state(ctrl), tt.args.keys, tt.args.depositTxIDs)
-			if tt.err != nil {
-				require.ErrorContains(t, err, tt.err.Error())
-				return
+			handler := defaultCaminoHandler(t)
+			now := handler.clk.Time()
+
+			var deposits []*deposit.Deposit
+			if tt.deposits != nil {
+				deposits = tt.deposits(now)
 			}
 
-			require.NoError(t, err)
-			require.Equal(t, tt.want, got, "Error asserting TransferableInputs: got = %v, want %v", got, tt.want)
-			require.Equal(t, tt.want1, got1, "Error asserting TransferableOutputs: got = %v, want %v", got1, tt.want2)
-			require.Equal(t, tt.want2, got2, "UnlockDeposit() got = %v, want %v", got2, tt.want2)
+			var expectedOuts []*avax.TransferableOutput
+			if tt.expectedOuts != nil {
+				expectedOuts = tt.expectedOuts(now, deposits)
+			}
+
+			var amountsToUndeposit map[ids.ID]uint64
+			if tt.amountsToUndeposit != nil {
+				amountsToUndeposit = tt.amountsToUndeposit(now, deposits)
+			}
+
+			var state state.Chain
+			if tt.state != nil {
+				state = tt.state(gomock.NewController(t), now, deposits)
+			}
+
+			require.True(t, utils.IsSortedAndUniqueSortable(tt.expectedIns))
+			require.True(t, avax.IsSortedTransferableOutputs(expectedOuts, txs.Codec))
+
+			ins, outs, signers, owners, err := handler.UnlockDeposit(
+				state,
+				tt.keys,
+				amountsToUndeposit,
+				tt.undepositToOwner,
+			)
+
+			require.ErrorIs(t, err, tt.expectedErr)
+			require.Equal(t, tt.expectedIns, ins)
+			require.Equal(t, expectedOuts, outs)
+			require.Equal(t, tt.expectedSigners, signers)
+			require.Equal(t, tt.expectedOwners, owners)
 		})
 	}
 }
@@ -2033,7 +2321,7 @@ func TestVerifyUnlockDepositedUTXOs(t *testing.T) {
 			args: args{
 				tx: tx,
 				utxos: []*avax.UTXO{
-					generate.UTXO(ids.ID{100}, test.AVAXAssetID, 100, owner1, ids.Empty, ids.Empty, true), // TODO@ refactor to be able to call without bool
+					generate.UTXO(ids.ID{100}, test.AVAXAssetID, 100, owner1, ids.Empty, ids.Empty, true),
 					generate.UTXO(ids.ID{101}, test.AVAXAssetID, 50, owner1, depositTxID1, ids.Empty, true),
 				},
 				ins: []*avax.TransferableInput{


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>vms/platformvm/camino_service.go (1)</summary><blockquote>

`464-511`: **Enhance error logging.**

Consider adding error logging to help with debugging and monitoring. The method currently only logs the initial call.

Apply this diff to add error logging:

```diff
 func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
 	s.vm.ctx.Log.Debug("Platform: Undeposit called")
 
 	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
 	if err != nil {
+		s.vm.ctx.Log.Error("failed to get fake keys",
+			zap.Error(err),
+		)
 		return err
 	}
 	if len(privKeys) == 0 {
+		s.vm.ctx.Log.Error("no keys found")
 		return errNoKeys
 	}
 
 	ins, outs, signers, err := s.vm.txBuilder.UnlockDeposit(
 		s.vm.state,
 		privKeys,
 		args.DepositTxIDs,
 	)
 	if err != nil {
+		s.vm.ctx.Log.Error("failed to unlock deposit",
+			zap.Error(err),
+			zap.Stringer("depositTxIDs", args.DepositTxIDs),
+		)
 		return fmt.Errorf("%w: %s", errCreateTransferables, err)
 	}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a6d1efea06368b1faa0cdc2b4a20b6eaeabb0616 and fdea7cc430a88abfb39c8fe97da35e16f031ad7b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `vms/platformvm/camino_service.go` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (6)</summary>

* GitHub Check: Unit (ubuntu-22.04)
* GitHub Check: Lint
* GitHub Check: Test docker image building
* GitHub Check: e2e
* GitHub Check: Fuzz
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>vms/platformvm/camino_service.go (3)</summary>

`451-462`: **LGTM! Type definitions are well-structured.**

The `UndepositArgs` and `UndepositReply` types are well-designed and consistent with other API types in the codebase.

---

`475-482`: **LGTM! Core undeposit logic is well-implemented.**

The core functionality correctly:
1. Uses the transaction builder to unlock deposits
2. Passes the required state, keys and deposit IDs
3. Handles errors appropriately

---

`484-509`: **LGTM! Response handling is thorough.**

The response handling:
1. Properly marshals transaction inputs and outputs
2. Uses the specified encoding format
3. Correctly formats the signers list

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-10T11:24:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>vms/platformvm/camino_service.go (1)</summary><blockquote>

`508-540`: **Improve error messages for encoding failures.**

The error messages should specify which encoding operation failed (inputs, outputs, or owners) to help with debugging.

Apply this diff to improve error messages:

```diff
 	bytes, err := txs.Codec.Marshal(txs.Version, ins)
 	if err != nil {
-		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+		return fmt.Errorf("failed to serialize inputs: %w", err)
 	}
 
 	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
-		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+		return fmt.Errorf("failed to encode inputs: %w", err)
 	}
 
 	bytes, err = txs.Codec.Marshal(txs.Version, outs)
 	if err != nil {
-		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+		return fmt.Errorf("failed to serialize outputs: %w", err)
 	}
 
 	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
-		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+		return fmt.Errorf("failed to encode outputs: %w", err)
 	}
 
 	bytes, err = txs.Codec.Marshal(txs.Version, owners)
 	if err != nil {
-		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+		return fmt.Errorf("failed to serialize owners: %w", err)
 	}
 	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
-		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+		return fmt.Errorf("failed to encode owners: %w", err)
 	}
```

</blockquote></details>
<details>
<summary>vms/platformvm/utxo/camino_locked_test.go (1)</summary><blockquote>

`1379-1379`: **Address the TODO comment regarding owners parameter.**

The TODO comment suggests that the `owners` parameter needs attention. This change might affect functionality if the owners information is needed for authorization or validation purposes elsewhere in the codebase.

Let's check if the owners parameter is used in other parts of the codebase:




Would you like me to:
1. Generate a solution that properly handles the owners parameter?
2. Open a new issue to track this TODO item?

<details>
<summary>❌ Verification inconclusive</summary>

🏁 Script executed:

```shell
#!/bin/bash
# Search for references to UnlockDeposit and its owners parameter
rg -A 5 "UnlockDeposit.*owners" --type go
```

Length of output: 513

---

**Address the TODO for the unused `owners` return value in UnlockDeposit.**  

Our search confirms that the only occurrence mentioning an "owners" parameter is in the test call at line 1379 of `vms/platformvm/utxo/camino_locked_test.go`. There are no other references in the codebase handling or validating the owners output. This suggests that either the owners data isn't required in subsequent logic or its processing is pending implementation. Please confirm whether the owners information is needed for further validation/authorization in deposit unlocking. If it is, we should implement the corresponding functionality; if not, consider removing the TODO comment and cleaning up the unused return value.  



</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fdea7cc430a88abfb39c8fe97da35e16f031ad7b and a9fb35d780618b9ded9620579c8a9b8410f7e590.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `vms/platformvm/camino_service.go` (1 hunks)
* `vms/platformvm/txs/builder/camino_builder.go` (0 hunks)
* `vms/platformvm/txs/builder/camino_builder_test.go` (0 hunks)
* `vms/platformvm/utxo/camino_locked.go` (5 hunks)
* `vms/platformvm/utxo/camino_locked_test.go` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* vms/platformvm/txs/builder/camino_builder_test.go
* vms/platformvm/txs/builder/camino_builder.go

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>vms/platformvm/camino_service.go (2)</summary>

`451-464`: **LGTM! Well-structured type definitions.**

The `UndepositArgs` and `UndepositReply` types are well-designed with all necessary fields for the undeposit operation.

---

`466-475`: **Add validation for DepositTxIDs.**

The method should validate that `DepositTxIDs` is not empty before proceeding with the undeposit operation.

</details>
<details>
<summary>vms/platformvm/utxo/camino_locked.go (1)</summary>

`136-137`: **LGTM! Interface update is consistent.**

The addition of `[]*secp256k1fx.OutputOwners` as a return value is well-integrated and maintains consistency between the interface and implementation.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-12T14:29:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>tools/genesis/genesis_test.go (2)</summary><blockquote>

`37-51`: **Improve test output handling and fix typo.**

1. Using `fmt.Printf` in tests makes it harder to verify output. Consider using a buffer or test logger.
2. There's a typo in "aliasAddrses".

Consider this improvement:

```diff
-func printAlias(t *testing.T, ma *genesis.MultisigAlias, hrp string) {
+func printAlias(t *testing.T, ma *genesis.MultisigAlias, hrp string) string {
+    var output strings.Builder
     aliasStr, err := address.Format("X", hrp, ma.Alias.Bytes())
     require.NoError(t, err)
 
     aliasAddrsStrs, err := formatAddresses(ma.Addresses, hrp)
     require.NoError(t, err)
 
-    fmt.Printf("alias: %s\n", aliasStr)
-    fmt.Printf("aliasAddrses:\n")
+    fmt.Fprintf(&output, "alias: %s\n", aliasStr)
+    fmt.Fprintf(&output, "aliasAddresses:\n")
     for i, aliasAddrStr := range aliasAddrsStrs {
-        fmt.Printf("  [%d] %s\n", i, aliasAddrStr)
+        fmt.Fprintf(&output, "  [%d] %s\n", i, aliasAddrStr)
     }
-    fmt.Printf("threshold: %d\n", ma.Threshold)
-    fmt.Printf("memo: %s\n", ma.Memo)
+    fmt.Fprintf(&output, "threshold: %d\n", ma.Threshold)
+    fmt.Fprintf(&output, "memo: %s\n", ma.Memo)
+    return output.String()
 }
```

---

`53-63`: **Optimize slice allocation.**

Pre-allocate the slice to improve performance and avoid unnecessary allocations.

```diff
 func formatAddresses(addrs []ids.ShortID, hrp string) ([]string, error) {
-    formattedAddrs := []string{}
+    formattedAddrs := make([]string, 0, len(addrs))
     for _, addr := range addrs {
         aliasStr, err := address.Format("X", hrp, addr.Bytes())
         if err != nil {
             return nil, err
         }
         formattedAddrs = append(formattedAddrs, aliasStr)
     }
     return formattedAddrs, nil
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between a9fb35d780618b9ded9620579c8a9b8410f7e590 and 7b71e614213277ca396160a23538c9dc87ad0cf9.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `tools/genesis/generated/genesis_kopernikus.json` is excluded by `!**/generated/**`

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tools/genesis/genesis_test.go` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Lint
* GitHub Check: Fuzz
* GitHub Check: e2e
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>tools/genesis/genesis_test.go (1)</summary>

`26-26`: **Clarify the relationship with Undeposit API.**

This test file focuses on multisig alias creation, but its relationship with the Undeposit API is unclear. Additionally, the `newMultisigAlias` function used in the tests is not defined.

Could you please:
1. Explain how this test relates to the Undeposit API changes?
2. Provide the implementation of the `newMultisigAlias` function?


Also applies to: 32-32

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-13T16:39:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 961123df318626a4725ef69b24dea1d4dc8765d0 and 6b3fc42e1f42a0afd24226b6acbfba4aa17375a7.

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `node/node.go` (1 hunks)
* `tools/genesis/genesis_test.go` (1 hunks)
* `version/compatibility.json` (1 hunks)
* `version/constants.go` (4 hunks)
* `vms/platformvm/camino_service.go` (1 hunks)
* `vms/platformvm/camino_vm_test.go` (1 hunks)
* `vms/platformvm/config/config.go` (2 hunks)
* `vms/platformvm/test/camino_defaults.go` (2 hunks)
* `vms/platformvm/test/camino_phase.go` (3 hunks)
* `vms/platformvm/txs/builder/camino_builder.go` (0 hunks)
* `vms/platformvm/txs/builder/camino_builder_test.go` (0 hunks)
* `vms/platformvm/txs/executor/dac/camino_dac.go` (3 hunks)
* `vms/platformvm/txs/executor/dac/camino_dac_test.go` (9 hunks)
* `vms/platformvm/utxo/camino_locked.go` (5 hunks)
* `vms/platformvm/utxo/camino_locked_test.go` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* vms/platformvm/txs/builder/camino_builder.go
* vms/platformvm/txs/builder/camino_builder_test.go

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* vms/platformvm/camino_vm_test.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* vms/platformvm/utxo/camino_locked_test.go
* tools/genesis/genesis_test.go
* vms/platformvm/utxo/camino_locked.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (5)</summary>

* GitHub Check: Unit (ubuntu-22.04)
* GitHub Check: Lint
* GitHub Check: e2e
* GitHub Check: Fuzz
* GitHub Check: Analyze (go)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>vms/platformvm/config/config.go (1)</summary>

`124-125`: **LGTM!**

The new field and method follow the established patterns and are correctly implemented.



Also applies to: 172-174

</details>
<details>
<summary>vms/platformvm/test/camino_defaults.go (1)</summary>

`80-80`: **LGTM!**

The changes follow the established patterns and are correctly implemented.



Also applies to: 84-85, 131-131

</details>
<details>
<summary>version/constants.go (1)</summary>

`37-37`: **LGTM!**

The version updates, phase time maps, and getter function follow the established patterns and are correctly implemented.



Also applies to: 47-47, 52-52, 214-223, 331-336

</details>
<details>
<summary>vms/platformvm/txs/executor/dac/camino_dac.go (1)</summary>

`31-31`: **LGTM! Phase terminology update is consistent.**

The changes from "Cairo" to "D" phase are consistently applied across error messages and phase activation checks.



Also applies to: 96-97, 365-366

</details>
<details>
<summary>vms/platformvm/camino_service.go (3)</summary>

`451-457`: **LGTM! The argument structure is well-defined.**

The `UndepositArgs` structure clearly defines the required fields for the undeposit operation.

---

`466-475`: **Add validation for DepositTxIDs.**

The method should validate that `DepositTxIDs` is not empty before proceeding with the undeposit operation.

---

`466-543`: **Add validation for encoding format.**

The method should validate the `Encoding` field to ensure it's a supported format before attempting to encode the results.

</details>
<details>
<summary>vms/platformvm/txs/executor/dac/camino_dac_test.go (1)</summary>

`30-31`: **LGTM! Test cases are properly updated for the phase terminology change.**

The test cases have been consistently updated to reflect the change from "Cairo" to "D" phase, including:
- Test configurations using `test.PhaseD`
- Test case names and error expectations using `errNotDPhase`



Also applies to: 65-83, 929-947

</details>
<details>
<summary>node/node.go (1)</summary>

`827-827`: <details>
<summary>✅ Verification successful</summary>

**LGTM! Verify phase time configuration.**

The addition of `DPhaseTime` to the Platform VM configuration follows the established pattern for phase time configuration.


Please verify the phase time configuration by checking the `GetDPhaseTime` implementation:

---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Check the implementation of GetDPhaseTime
ast-grep --pattern $'func GetDPhaseTime(networkID uint32) time.Time {
  $$$
}'
```

Length of output: 391

---



</details>

**LGTM – DPhaseTime configuration verified**

The addition of `DPhaseTime` in the Platform VM configuration correctly follows the existing pattern. The implementation of `GetDPhaseTime` has been verified in `version/constants.go` (lines 331-336) to correctly return the phase time based on the provided network ID.

</details>
<details>
<summary>version/compatibility.json (1)</summary>

`3-3`: <details>
<summary>❓ Verification inconclusive</summary>

**LGTM! Verify version compatibility.**

The addition of version "v1.2.0" follows semantic versioning and maintains backward compatibility.


Please verify the version compatibility:

---

🌐 Web query:

```web
What are the breaking changes between v1.1.0 and v1.2.0 of CaminoGo?
```

💡 Result:

The available search results do not explicitly list breaking changes between CaminoGo v1.1.0 and v1.2.0 in their visible content. However, we can infer some key details from the documentation:

1. **Repository Transition**: Development officially moved from `camino-node` to `caminogo` repository starting with v1.1.0[4][7], suggesting architectural changes that might affect node operators.

2. **Versioning Scheme**:
   - Patch versions (`vx.x.[Patch]`) indicate client upgrades without network protocol changes[2]
   - Network upgrades occur through `vx.[Upgrade].x` versions[2]

3. **API Compatibility**: caminogo maintains backward compatibility unless explicitly deprecated[2], but no specific deprecated features are mentioned in the visible results.

For precise details about v1.2.0 changes:
- The latest release notes mention "many minor bug fixes" in Camino Suite v1.2.0[10]
- The official recommendation is to consult the Full Changelog linked in release notes[10]

Key recommendation: Always check network upgrade announcements and test node upgrades on Columbus Testnet before production deployment[5][8].

Citations:

- 1: https://caminobrowser.org/releases/1.0.1/
- 2: https://github.com/chain4travel/caminogo
- 3: https://www.youtube.com/watch?v=XoB5LvaqZBs
- 4: https://docs.camino.network/release-notes/camino-node
- 5: https://aws.amazon.com/marketplace/pp/prodview-ijjmgxohjdjbc
- 6: https://jquery.com/upgrade-guide/3.0/
- 7: https://docs.camino.network/release-notes/caminogo/index.html
- 8: https://github.com/chain4travel/camino-node
- 9: https://discuss.hashicorp.com/t/best-practice-upgrading-k-v-to-v2-in-production/5532
- 10: https://docs.camino.network/release-notes/camino-suite

---



</details>

**LGTM! The addition of version "v1.2.0" adheres to semantic versioning and maintains backward compatibility.** 

Based on the latest release information, there aren’t any explicitly documented breaking changes between v1.1.0 and v1.2.0. While the repository transition (from `camino-node` to `caminogo`) introduced in v1.1.0 suggests some architectural updates, v1.2.0 appears to contain only minor bug fixes. Still, please verify version compatibility by reviewing the full changelog and testing upgrades (e.g., on the Columbus Testnet) to ensure that any subtle changes don’t affect node operations.

- **File:** `version/compatibility.json` (line 3)

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-02-19T15:39:02Z
user: login: evlekht
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T10:47:01Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:47:24Z
user: login: evlekht
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T10:47:57Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:48:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>vms/platformvm/camino_service.go (2)</summary><blockquote>

`451-455`: **Struct naming consistency**  
The `Undeposit` struct name is clear, but consider aligning it with existing deposit-related naming (e.g. `DepositUnlock`) if desired for consistency.

---

`492-495`: **Potential collision when map-building**  
Using `undeposits[undeposit.DepositTxID] = uint64(undeposit.Amount)` overwrites amounts if the same `DepositTxID` appears multiple times. If you intend to sum them, consider adding them instead of overwriting.

</blockquote></details>
<details>
<summary>vms/platformvm/utxo/camino_locked.go (1)</summary><blockquote>

`691-916`: **Prevent partial deposit duplication**  
The `UnlockDeposit` logic correctly checks if the user is trying to undeposit more than the available unlockable amount. However, it doesn't aggregate repeated depositTxIDs if they appear more than once in `amountsToUndeposit`. If the same depositTxID is repeated, the second reference won't be recognized as continuing the same deposit.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6b3fc42e1f42a0afd24226b6acbfba4aa17375a7 and bbeb66cc0259877839ef379ed657e36cff2d8e8c.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `vms/platformvm/camino_service.go` (1 hunks)
* `vms/platformvm/deposit/camino_deposit.go` (1 hunks)
* `vms/platformvm/test/expect/camino_expect.go` (2 hunks)
* `vms/platformvm/test/generate/camino_generate.go` (1 hunks)
* `vms/platformvm/txs/builder/camino_builder.go` (0 hunks)
* `vms/platformvm/txs/builder/camino_builder_test.go` (0 hunks)
* `vms/platformvm/txs/executor/camino_tx_executor_test.go` (20 hunks)
* `vms/platformvm/utxo/camino_helpers_test.go` (1 hunks)
* `vms/platformvm/utxo/camino_locked.go` (14 hunks)
* `vms/platformvm/utxo/camino_locked_test.go` (28 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* vms/platformvm/txs/builder/camino_builder.go
* vms/platformvm/txs/builder/camino_builder_test.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* vms/platformvm/utxo/camino_locked_test.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>vms/platformvm/test/generate/camino_generate.go (1)</summary><blockquote>

<details>
<summary>vms/secp256k1fx/camino_keychain_test.go (1) (1)</summary>

* `t` (23:35)

</details>

</blockquote></details>
<details>
<summary>vms/platformvm/utxo/camino_helpers_test.go (1)</summary><blockquote>

<details>
<summary>vms/platformvm/test/camino_defaults.go (1) (1)</summary>

* `Context` (213:241)

</details>

</blockquote></details>
<details>
<summary>vms/platformvm/camino_service.go (4)</summary><blockquote>

<details>
<summary>api/common_args_responses.go (1) (1)</summary>

* `JSONFromAddrs` (62:65)

</details>
<details>
<summary>api/camino.go (1) (1)</summary>

* `Encoding` (8:10)

</details>
<details>
<summary>vms/platformvm/state/camino_state.go (6) (6)</summary>

* `s` (21:43)
* `s` (45:47)
* `s` (49:51)
* `s` (53:55)
* `s` (57:59)
* `s` (61:63)

</details>
<details>
<summary>vms/platformvm/camino_client.go (1) (1)</summary>

* `secpOwnerFromAPI` (145:163)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: e2e
* GitHub Check: Fuzz

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>vms/platformvm/utxo/camino_helpers_test.go (1)</summary>

`53-53`: **Context initialization improvement**

The change from `snow.DefaultContextTest()` to `test.Context(t)` improves the test environment by using a more comprehensive context initialization. The `test.Context(t)` function provides a richer test context with predefined chain IDs, asset IDs, and a mock validator state.

</details>
<details>
<summary>vms/platformvm/deposit/camino_deposit.go (1)</summary>

`73-78`: **Good addition of time-based helper method**

This new method provides a convenient alternative to `UnlockableAmount` by accepting a `time.Time` object instead of requiring a raw Unix timestamp. This makes the API more ergonomic when working with Go's time library and improves code readability in calling code.

</details>
<details>
<summary>vms/platformvm/test/generate/camino_generate.go (1)</summary>

`269-269`: **API improvement with variadic parameters**

Changing the function signature from accepting a slice to using variadic parameters (`...`) makes the API more flexible. Callers can now either pass individual UTXOs directly or a slice with the spread operator, resulting in more convenient usage patterns.

</details>
<details>
<summary>vms/platformvm/txs/executor/camino_tx_executor_test.go (11)</summary>

`3627-3632`: **LGTM! Function signature update.**

The change to use `generate.InsFromUTXOs` instead of manually creating individual inputs simplifies the test code and makes it more readable. This syntactic sugar creates the correct inputs from UTXOs.

---

`4235-4236`: **Modified calculation method for unlockable deposit amounts.**

The code now properly calculates unlockable deposit amounts using `UnlockableAmountByTime()` rather than manually calculating the amount. This ensures consistency in how unlockable amounts are determined throughout the codebase.

---

`4937-4937`: **Updated deposit modification uses the unlockable amount calculation.**

This change ensures that when a deposit is modified, it properly uses the `UnlockableAmountByTime` method to calculate exactly how much can be unlocked at the current chain time. This provides more deterministic behavior for deposit unlocking.

---

`8699-8803`: **New `UnlockExpiredDepositTx` test cases.**

This implementation adds proper test coverage for the new `UnlockExpiredDepositTx` transaction type. The test cases cover basic functionality like input validation and error handling.

The transaction structure properly isolates the input and output handling for expired deposits.

---

`8825-8858`: **Phase verification for CairoPhase.**

This test ensures that expired deposit transactions can only be executed during the Cairo phase of the network. This is a critical check to ensure phase-specific protocol rules are enforced correctly.

---

`8843-8862`: **Input validation for deposit transactions.**

This test ensures that only deposited inputs can be processed in the `UnlockExpiredDepositTx` transaction. This is an important security check to prevent misuse of the transaction type.

---

`8863-8892`: **Deposit expiration validation.**

This test verifies that only expired deposits can be unlocked via the `UnlockExpiredDepositTx` transaction. This prevents premature deposit unlocking and enforces the deposit term requirements.

---

`8893-8952`: **System transaction input validation.**

These tests verify the integrity of system transactions by ensuring that inputs match the expected deposits and that amount validation is properly enforced. This prevents potential manipulation of the system transaction inputs.

---

`8953-9014`: **Transaction output validation.**

These tests ensure that outputs in the expired deposit unlock transaction properly match the expected amounts and ownership information. This prevents potentially malicious manipulation of transaction outputs.

---

`9015-9035`: **Full transaction processing test.**

This test verifies the entire flow of an expired deposit unlock transaction, including:
1. Processing multiple deposits of different types
2. Handling deposits with unclaimed rewards
3. Properly transferring those rewards to claimables
4. Unlocking deposits that are both bonded and unbonded

The test provides comprehensive coverage of the expected behavior.

---

`9036-9055`: **Test execution helper code.**

This well-structured test execution code sets up the testing environment, processes transactions, and verifies the expected results. The approach ensures consistent test execution across all test cases.

</details>
<details>
<summary>vms/platformvm/camino_service.go (1)</summary>

`472-476`: **Allowing zero-length Undeposits**  
Currently, the code returns an error if `len(args.Undeposits) == 0`. This is valid, but ensure other downstream logic does not require strictly more than one scenario.

</details>
<details>
<summary>vms/platformvm/utxo/camino_locked.go (3)</summary>

`52-52`: **New error declaration**  
Declaring `errInsufficientUnlockable` is helpful for clarity.

---

`1588-1613`: **Validate nested multisig**  
`GetOwnerWithID` forcibly checks that the provided secpOwner has no nested owners, returning an error if so. This is consistent with the code's logic around `VerifyMultisigOwner`.

---

`1614-1626`: **Doc comment clarity**  
The doc block for `sortUTXOs` clarifies ordering logic. This is helpful for understanding the sorting priorities and is useful for debugging.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T10:55:53Z
user: login: evlekht
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:00:01Z
user: login: evlekht
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T11:00:06Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T11:00:37Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T11:01:00Z
user: login: Noctunus
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T11:15:21Z

Review comments:
diff_hunk: @@ -448,6 +448,68 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
path: vms/platformvm/camino_service.go
user: login: coderabbitai[bot]
body: _:hammer_and_wrench: Refactor suggestion_

**Add validation for DepositTxIDs.**

The method should validate that `DepositTxIDs` is not empty before proceeding with the undeposit operation.

Apply this diff to add validation:

```diff
 func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
 	s.vm.ctx.Log.Debug("Platform: Undeposit called")
 
+	if len(args.DepositTxIDs) == 0 {
+		return fmt.Errorf("no deposit transaction IDs provided")
+	}
+
 	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
	s.vm.ctx.Log.Debug("Platform: Undeposit called")

	if len(args.DepositTxIDs) == 0 {
		return fmt.Errorf("no deposit transaction IDs provided")
	}

	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
	if err != nil {
		return err
	}
	if len(privKeys) == 0 {
		return errNoKeys
	}
	// ... rest of the method implementation
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-02-10T11:24:20Z
author_association: NONE
diff_hunk: @@ -448,6 +448,100 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	AmountToBurn utilsjson.Uint64    `json:"amountToBurn"`
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+	Owners  string          `json:"owners"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
+
+	depositIns, depositOuts, depositSigners, depositOwners, err := s.vm.txBuilder.UnlockDeposit(
+		s.vm.state,
+		privKeys,
+		args.DepositTxIDs,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins, outs, signers, owners, err := s.vm.txBuilder.Lock(
+		s.vm.state,
+		privKeys,
+		0,
+		uint64(args.AmountToBurn),
+		locked.StateUnlocked,
+		nil,
+		nil,
+		0,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins = append(ins, depositIns...)
+	outs = append(outs, depositOuts...)
+	signers = append(signers, depositSigners...)
+	owners = append(owners, depositOwners...)
+
+	avax.SortTransferableInputsWithSigners(ins, signers)
+	avax.SortTransferableOutputs(outs, txs.Codec)
+
+	bytes, err := txs.Codec.Marshal(txs.Version, ins)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, outs)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	response.Signers = make([][]ids.ShortID, len(signers))
+	for i, cred := range signers {
+		response.Signers[i] = make([]ids.ShortID, len(cred))
+		for j, sig := range cred {
+			response.Signers[i][j] = sig.Address()
+		}
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, owners)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+
+	return nil
+}
path: vms/platformvm/camino_service.go
user: login: coderabbitai[bot]
body: _:hammer_and_wrench: Refactor suggestion_

**Add validation for encoding format.**

The method should validate the `Encoding` field to ensure it's a supported format before attempting to encode the results.

Apply this diff to add validation:

```diff
 func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
 	s.vm.ctx.Log.Debug("Platform: Undeposit called")
 
+	if err := args.Encoding.Valid(); err != nil {
+		return fmt.Errorf("invalid encoding: %w", err)
+	}
+
 	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
	s.vm.ctx.Log.Debug("Platform: Undeposit called")

	if err := args.Encoding.Valid(); err != nil {
		return fmt.Errorf("invalid encoding: %w", err)
	}

	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
	if err != nil {
		return err
	}
	if len(privKeys) == 0 {
		return errNoKeys
	}

	depositIns, depositOuts, depositSigners, depositOwners, err := s.vm.txBuilder.UnlockDeposit(
		s.vm.state,
		privKeys,
		args.DepositTxIDs,
	)
	if err != nil {
		return fmt.Errorf("%w: %s", errCreateTransferables, err)
	}

	ins, outs, signers, owners, err := s.vm.txBuilder.Lock(
		s.vm.state,
		privKeys,
		0,
		uint64(args.AmountToBurn),
		locked.StateUnlocked,
		nil,
		nil,
		0,
	)
	if err != nil {
		return fmt.Errorf("%w: %s", errCreateTransferables, err)
	}

	ins = append(ins, depositIns...)
	outs = append(outs, depositOuts...)
	signers = append(signers, depositSigners...)
	owners = append(owners, depositOwners...)

	avax.SortTransferableInputsWithSigners(ins, signers)
	avax.SortTransferableOutputs(outs, txs.Codec)

	bytes, err := txs.Codec.Marshal(txs.Version, ins)
	if err != nil {
		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
	}

	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
	}

	bytes, err = txs.Codec.Marshal(txs.Version, outs)
	if err != nil {
		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
	}

	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
	}

	response.Signers = make([][]ids.ShortID, len(signers))
	for i, cred := range signers {
		response.Signers[i] = make([]ids.ShortID, len(cred))
		for j, sig := range cred {
			response.Signers[i][j] = sig.Address()
		}
	}

	bytes, err = txs.Codec.Marshal(txs.Version, owners)
	if err != nil {
		return fmt.Errorf("%w: %s", errSerializeOwners, err)
	}
	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
		return fmt.Errorf("%w: %s", errSerializeOwners, err)
	}

	return nil
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-02-12T14:29:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,63 @@
+package main
path: tools/genesis/genesis_test.go
user: login: coderabbitai[bot]
body: _:warning: Potential issue_

**Package name should match the directory structure.**

The test file should be in package `genesis` or `genesis_test` to match its location in the `tools/genesis` directory, not in package `main`.

```diff
-package main
+package genesis_test
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
package genesis_test
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-13T16:39:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,63 @@
+package main
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/ava-labs/avalanchego/genesis"
+	"github.com/ava-labs/avalanchego/ids"
+	"github.com/ava-labs/avalanchego/utils/constants"
+	"github.com/ava-labs/avalanchego/utils/formatting/address"
+	"github.com/stretchr/testify/require"
+)
+
+func TestA(t *testing.T) {
+	hrp := constants.KopernikusHRP
+
+	addrs, err := address.ParseToIDs([]string{
+		"X-kopernikus18pry238vq8uzzpar32wa4x4p2rkyc7wx2qnctc",
+		"X-kopernikus18l0l8dz03vmk7k7y5cgax8k2l5mphj4p5k040u",
+		"X-kopernikus1d0ydd4wyq670lzrp0y7xd3wej4z7ysp2surum8",
+		"X-kopernikus1h87ampd07hwtnqywpsagrxm9ygkmcr34ndjah9",
+		"X-kopernikus102uap4au55t22m797rr030wyrw0jlgw25ut8vj",
+	})
+	require.NoError(t, err)
+
+	ma, err := newMultisigAlias(ids.Empty, addrs, 2, "1931")
+	require.NoError(t, err)
+	printAlias(t, &ma, hrp)
+
+	fmt.Println("")
+
+	ma, err = newMultisigAlias(ids.Empty, addrs, 2, "1932")
+	require.NoError(t, err)
+	printAlias(t, &ma, hrp)
+}
path: tools/genesis/genesis_test.go
user: login: coderabbitai[bot]
body: _:hammer_and_wrench: Refactor suggestion_

**Improve test naming and add assertions.**

The test has several issues:
1. The name `TestA` is not descriptive and doesn't follow Go's test naming conventions.
2. The test lacks meaningful assertions beyond error checking.
3. The test uses an undefined function `newMultisigAlias`.

Consider renaming and restructuring the test:

```diff
-func TestA(t *testing.T) {
+func TestMultisigAliasCreation(t *testing.T) {
     hrp := constants.KopernikusHRP
 
     addrs, err := address.ParseToIDs([]string{
         "X-kopernikus18pry238vq8uzzpar32wa4x4p2rkyc7wx2qnctc",
         "X-kopernikus18l0l8dz03vmk7k7y5cgax8k2l5mphj4p5k040u",
         "X-kopernikus1d0ydd4wyq670lzrp0y7xd3wej4z7ysp2surum8",
         "X-kopernikus1h87ampd07hwtnqywpsagrxm9ygkmcr34ndjah9",
         "X-kopernikus102uap4au55t22m797rr030wyrw0jlgw25ut8vj",
     })
     require.NoError(t, err)
 
     ma, err := newMultisigAlias(ids.Empty, addrs, 2, "1931")
     require.NoError(t, err)
+    require.Equal(t, 2, ma.Threshold, "threshold should be 2")
+    require.Equal(t, "1931", ma.Memo, "memo should match")
+    require.Len(t, ma.Addresses, 5, "should have 5 addresses")
     printAlias(t, &ma, hrp)
 
     fmt.Println("")
 
     ma, err = newMultisigAlias(ids.Empty, addrs, 2, "1932")
     require.NoError(t, err)
+    require.Equal(t, 2, ma.Threshold, "threshold should be 2")
+    require.Equal(t, "1932", ma.Memo, "memo should match")
+    require.Len(t, ma.Addresses, 5, "should have 5 addresses")
     printAlias(t, &ma, hrp)
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
func TestMultisigAliasCreation(t *testing.T) {
	hrp := constants.KopernikusHRP

	addrs, err := address.ParseToIDs([]string{
		"X-kopernikus18pry238vq8uzzpar32wa4x4p2rkyc7wx2qnctc",
		"X-kopernikus18l0l8dz03vmk7k7y5cgax8k2l5mphj4p5k040u",
		"X-kopernikus1d0ydd4wyq670lzrp0y7xd3wej4z7ysp2surum8",
		"X-kopernikus1h87ampd07hwtnqywpsagrxm9ygkmcr34ndjah9",
		"X-kopernikus102uap4au55t22m797rr030wyrw0jlgw25ut8vj",
	})
	require.NoError(t, err)

	ma, err := newMultisigAlias(ids.Empty, addrs, 2, "1931")
	require.NoError(t, err)
	require.Equal(t, 2, ma.Threshold, "threshold should be 2")
	require.Equal(t, "1931", ma.Memo, "memo should match")
	require.Len(t, ma.Addresses, 5, "should have 5 addresses")
	printAlias(t, &ma, hrp)

	fmt.Println("")

	ma, err = newMultisigAlias(ids.Empty, addrs, 2, "1932")
	require.NoError(t, err)
	require.Equal(t, 2, ma.Threshold, "threshold should be 2")
	require.Equal(t, "1932", ma.Memo, "memo should match")
	require.Len(t, ma.Addresses, 5, "should have 5 addresses")
	printAlias(t, &ma, hrp)
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-13T16:39:33Z
author_association: NONE
diff_hunk: @@ -23,11 +23,12 @@ const (
 	PhaseCortina  Phase = 3 // avax, included into Berlin phase
 	PhaseBerlin   Phase = 3
 	PhaseCairo    Phase = 4
+	PhaseD        Phase = 5
 )
 
 const (
 	PhaseFirst = PhaseSunrise
-	PhaseLast  = PhaseBerlin
+	PhaseLast  = PhaseCairo
path: vms/platformvm/test/camino_phase.go
user: login: coderabbitai[bot]
body: _:warning: Potential issue_

**Update `PhaseLast` to `PhaseD`.**

The `PhaseLast` constant should be updated to `PhaseD` since it's the latest phase in the consecutive order.

Apply this diff to fix the inconsistency:

```diff
-PhaseLast  = PhaseCairo
+PhaseLast  = PhaseD
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
	PhaseLast  = PhaseD
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-02-19T15:39:02Z
author_association: NONE
diff_hunk: @@ -448,6 +448,100 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	AmountToBurn utilsjson.Uint64    `json:"amountToBurn"`
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+	Owners  string          `json:"owners"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
+
+	depositIns, depositOuts, depositSigners, depositOwners, err := s.vm.txBuilder.UnlockDeposit(
+		s.vm.state,
+		privKeys,
+		args.DepositTxIDs,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins, outs, signers, owners, err := s.vm.txBuilder.Lock(
+		s.vm.state,
+		privKeys,
+		0,
+		uint64(args.AmountToBurn),
+		locked.StateUnlocked,
+		nil,
+		nil,
+		0,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins = append(ins, depositIns...)
+	outs = append(outs, depositOuts...)
+	signers = append(signers, depositSigners...)
+	owners = append(owners, depositOwners...)
+
+	avax.SortTransferableInputsWithSigners(ins, signers)
+	avax.SortTransferableOutputs(outs, txs.Codec)
+
+	bytes, err := txs.Codec.Marshal(txs.Version, ins)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, outs)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	response.Signers = make([][]ids.ShortID, len(signers))
+	for i, cred := range signers {
+		response.Signers[i] = make([]ids.ShortID, len(cred))
+		for j, sig := range cred {
+			response.Signers[i][j] = sig.Address()
+		}
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, owners)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+
+	return nil
+}
path: vms/platformvm/camino_service.go
user: login: evlekht
body: its checked on unmarshaling
created_at: 2025-03-18T10:47:00Z
author_association: MEMBER
diff_hunk: @@ -448,6 +448,100 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	AmountToBurn utilsjson.Uint64    `json:"amountToBurn"`
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+	Owners  string          `json:"owners"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
+
+	depositIns, depositOuts, depositSigners, depositOwners, err := s.vm.txBuilder.UnlockDeposit(
+		s.vm.state,
+		privKeys,
+		args.DepositTxIDs,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins, outs, signers, owners, err := s.vm.txBuilder.Lock(
+		s.vm.state,
+		privKeys,
+		0,
+		uint64(args.AmountToBurn),
+		locked.StateUnlocked,
+		nil,
+		nil,
+		0,
+	)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errCreateTransferables, err)
+	}
+
+	ins = append(ins, depositIns...)
+	outs = append(outs, depositOuts...)
+	signers = append(signers, depositSigners...)
+	owners = append(owners, depositOwners...)
+
+	avax.SortTransferableInputsWithSigners(ins, signers)
+	avax.SortTransferableOutputs(outs, txs.Codec)
+
+	bytes, err := txs.Codec.Marshal(txs.Version, ins)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Ins, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, outs)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeTransferables, err)
+	}
+
+	if response.Outs, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errEncodeTransferables, err)
+	}
+
+	response.Signers = make([][]ids.ShortID, len(signers))
+	for i, cred := range signers {
+		response.Signers[i] = make([]ids.ShortID, len(cred))
+		for j, sig := range cred {
+			response.Signers[i][j] = sig.Address()
+		}
+	}
+
+	bytes, err = txs.Codec.Marshal(txs.Version, owners)
+	if err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+	if response.Owners, err = formatting.Encode(args.Encoding, bytes); err != nil {
+		return fmt.Errorf("%w: %s", errSerializeOwners, err)
+	}
+
+	return nil
+}
path: vms/platformvm/camino_service.go
user: login: coderabbitai[bot]
body: @evlekht You're right - if the encoding validation is already happening during unmarshalling, then adding an explicit check would be redundant. Thanks for the clarification.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T10:47:23Z
author_association: NONE
diff_hunk: @@ -448,6 +448,68 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
path: vms/platformvm/camino_service.go
user: login: evlekht
body: added
created_at: 2025-03-18T10:47:57Z
author_association: MEMBER
diff_hunk: @@ -448,6 +448,68 @@ func (s *CaminoService) Spend(_ *http.Request, args *SpendArgs, response *SpendR
 	return nil
 }
 
+type UndepositArgs struct {
+	api.JSONFromAddrs
+
+	DepositTxIDs []ids.ID            `json:"depositTxIDs"`
+	Encoding     formatting.Encoding `json:"encoding"`
+}
+
+type UndepositReply struct {
+	Ins     string          `json:"ins"`
+	Outs    string          `json:"outs"`
+	Signers [][]ids.ShortID `json:"signers"`
+}
+
+func (s *CaminoService) Undeposit(_ *http.Request, args *UndepositArgs, response *UndepositReply) error {
+	s.vm.ctx.Log.Debug("Platform: Undeposit called")
+
+	privKeys, err := s.getFakeKeys(&args.JSONFromAddrs)
+	if err != nil {
+		return err
+	}
+	if len(privKeys) == 0 {
+		return errNoKeys
+	}
path: vms/platformvm/camino_service.go
user: login: coderabbitai[bot]
body: @evlekht I see you've implemented the validation for empty input, checking `len(args.Undeposits) == 0` instead of the originally suggested `DepositTxIDs` check. This is appropriate since your implementation uses the `Undeposits` array structure. Thank you for making this change!

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T10:48:33Z
author_association: NONE
diff_hunk: @@ -254,6 +254,52 @@ func StateVerifyMultisigOwner(
 	}
 }
 
+func VerifyMultisigOwner(
+	t *testing.T,
+	s *state.MockChain,
+	owner *secp256k1fx.OutputOwners,
+	msigAliasAddresses []ids.ShortID,
+	msigAliases []*multisig.AliasWithNonce,
+	collectAddresses bool,
+) {
+	t.Helper()
+	if owner == nil {
+		return
+	}
+
+	aliases := make(map[ids.ShortID]*multisig.AliasWithNonce)
+	for i := range msigAliasAddresses {
+		aliases[msigAliasAddresses[i]] = msigAliases[i]
+	}
+
+	addresses := set.Set[ids.ShortID]{}
+
+	if collectAddresses {
+		for _, addr := range owner.Addrs {
+			addresses.Add(addr)
+		}
+		for _, alias := range msigAliases {
+			owner, ok := alias.Owners.(*secp256k1fx.OutputOwners)
+			require.True(t, ok)
+			for _, addr := range owner.Addrs {
+				addresses.Add(addr)
+			}
+		}
+	}
+
+	for _, msigAliasAddress := range msigAliasAddresses {
+		addresses.Add(msigAliasAddress)
+	}
+
+	for addr := range addresses {
+		if _, ok := aliases[addr]; ok {
+			s.EXPECT().GetMultisigAlias(addr).Return(aliases[addr], nil)
+		} else {
+			s.EXPECT().GetMultisigAlias(addr).Return(nil, database.ErrNotFound)
+		}
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Check for potential out-of-range error when indexing msigAliases**  
You rely on `aliases[msigAliasAddresses[i]] = msigAliases[i]`, but there's no explicit check if `len(msigAliasAddresses) == len(msigAliases).` This could lead to `index out of range` if the slices differ in length.  

Add a simple length check to prevent unexpected panics in test code.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T10:55:51Z
author_association: NONE
diff_hunk: @@ -367,3 +413,20 @@
 		}
 	}
 }
+
+func GetDepositUnlockableAmounts(
+	t *testing.T,
+	s *state.MockChain,
+	depositTxIDs []ids.ID,
+	deposits []*deposit.Deposit,
+	offers []*deposit.Offer,
+) {
+	t.Helper()
+	require.Len(t, depositTxIDs, len(deposits))
+	for i := range depositTxIDs {
+		s.EXPECT().GetDeposit(depositTxIDs[i]).Return(deposits[i], nil)
+	}
+	for i := range offers {
+		s.EXPECT().GetDepositOffer(deposits[i].DepositOfferID).Return(offers[i], nil)
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Validate offers slice length to match deposits**  
In this function, there's no length check for `offers` to ensure it corresponds to `deposits`. The indexing at `offers[i]` may trigger an out-of-range panic if `len(offers) < len(deposits)`.  

Consider adding a length check or panic to avoid potential slice indexing errors.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T10:55:51Z
author_association: NONE
diff_hunk: @@ -254,6 +254,52 @@ func StateVerifyMultisigOwner(
 	}
 }
 
+func VerifyMultisigOwner(
+	t *testing.T,
+	s *state.MockChain,
+	owner *secp256k1fx.OutputOwners,
+	msigAliasAddresses []ids.ShortID,
+	msigAliases []*multisig.AliasWithNonce,
+	collectAddresses bool,
+) {
+	t.Helper()
+	if owner == nil {
+		return
+	}
+
+	aliases := make(map[ids.ShortID]*multisig.AliasWithNonce)
+	for i := range msigAliasAddresses {
+		aliases[msigAliasAddresses[i]] = msigAliases[i]
+	}
+
+	addresses := set.Set[ids.ShortID]{}
+
+	if collectAddresses {
+		for _, addr := range owner.Addrs {
+			addresses.Add(addr)
+		}
+		for _, alias := range msigAliases {
+			owner, ok := alias.Owners.(*secp256k1fx.OutputOwners)
+			require.True(t, ok)
+			for _, addr := range owner.Addrs {
+				addresses.Add(addr)
+			}
+		}
+	}
+
+	for _, msigAliasAddress := range msigAliasAddresses {
+		addresses.Add(msigAliasAddress)
+	}
+
+	for addr := range addresses {
+		if _, ok := aliases[addr]; ok {
+			s.EXPECT().GetMultisigAlias(addr).Return(aliases[addr], nil)
+		} else {
+			s.EXPECT().GetMultisigAlias(addr).Return(nil, database.ErrNotFound)
+		}
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: evlekht
body: done
created_at: 2025-03-18T11:00:01Z
author_association: MEMBER
diff_hunk: @@ -367,3 +413,20 @@
 		}
 	}
 }
+
+func GetDepositUnlockableAmounts(
+	t *testing.T,
+	s *state.MockChain,
+	depositTxIDs []ids.ID,
+	deposits []*deposit.Deposit,
+	offers []*deposit.Offer,
+) {
+	t.Helper()
+	require.Len(t, depositTxIDs, len(deposits))
+	for i := range depositTxIDs {
+		s.EXPECT().GetDeposit(depositTxIDs[i]).Return(deposits[i], nil)
+	}
+	for i := range offers {
+		s.EXPECT().GetDepositOffer(deposits[i].DepositOfferID).Return(offers[i], nil)
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: evlekht
body: done
created_at: 2025-03-18T11:00:05Z
author_association: MEMBER
diff_hunk: @@ -367,3 +413,20 @@
 		}
 	}
 }
+
+func GetDepositUnlockableAmounts(
+	t *testing.T,
+	s *state.MockChain,
+	depositTxIDs []ids.ID,
+	deposits []*deposit.Deposit,
+	offers []*deposit.Offer,
+) {
+	t.Helper()
+	require.Len(t, depositTxIDs, len(deposits))
+	for i := range depositTxIDs {
+		s.EXPECT().GetDeposit(depositTxIDs[i]).Return(deposits[i], nil)
+	}
+	for i := range offers {
+		s.EXPECT().GetDepositOffer(deposits[i].DepositOfferID).Return(offers[i], nil)
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: coderabbitai[bot]
body: @evlekht Great! I see the length check has been properly implemented with `require.Len(t, depositTxIDs, len(offers))`, which ensures all three slices have matching lengths. This effectively prevents potential out-of-range panics.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Just%20received%20a%20valuable%20refactor%20suggestion%20from%20%40coderabbitai!%20%F0%9F%9B%A0%EF%B8%8F%20Always%20validate%20your%20slices%20to%20prevent%20out-of-range%20errors.%20A%20small%20check%20can%20save%20a%20lot%20of%20headaches!%20%F0%9F%92%A1&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcaminogo%2Fpull%2F404%23discussion_r2000761246)?
							Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T11:00:37Z
author_association: NONE
diff_hunk: @@ -254,6 +254,52 @@ func StateVerifyMultisigOwner(
 	}
 }
 
+func VerifyMultisigOwner(
+	t *testing.T,
+	s *state.MockChain,
+	owner *secp256k1fx.OutputOwners,
+	msigAliasAddresses []ids.ShortID,
+	msigAliases []*multisig.AliasWithNonce,
+	collectAddresses bool,
+) {
+	t.Helper()
+	if owner == nil {
+		return
+	}
+
+	aliases := make(map[ids.ShortID]*multisig.AliasWithNonce)
+	for i := range msigAliasAddresses {
+		aliases[msigAliasAddresses[i]] = msigAliases[i]
+	}
+
+	addresses := set.Set[ids.ShortID]{}
+
+	if collectAddresses {
+		for _, addr := range owner.Addrs {
+			addresses.Add(addr)
+		}
+		for _, alias := range msigAliases {
+			owner, ok := alias.Owners.(*secp256k1fx.OutputOwners)
+			require.True(t, ok)
+			for _, addr := range owner.Addrs {
+				addresses.Add(addr)
+			}
+		}
+	}
+
+	for _, msigAliasAddress := range msigAliasAddresses {
+		addresses.Add(msigAliasAddress)
+	}
+
+	for addr := range addresses {
+		if _, ok := aliases[addr]; ok {
+			s.EXPECT().GetMultisigAlias(addr).Return(aliases[addr], nil)
+		} else {
+			s.EXPECT().GetMultisigAlias(addr).Return(nil, database.ErrNotFound)
+		}
+	}
+}
path: vms/platformvm/test/expect/camino_expect.go
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@evlekht Thank you for implementing the fix! I can see the length check has been added to properly validate that `msigAliasAddresses` and `msigAliases` have matching lengths. This will help prevent potential index out of range errors during testing.



---

🏁 Script executed:
```
#!/bin/bash
# Check if the file has been updated with the length check
cat vms/platformvm/test/expect/camino_expect.go | grep -A20 "func VerifyMultisigOwner" | grep "require.Len(t, msigAliasAddresses, len(msigAliases))"
```

Length of output: 204

---



</details>

@evlekht Perfect! The script output confirms that the length check has been successfully implemented. This ensures the arrays will always have matching lengths before you access them by index, preventing any potential runtime errors.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Great%20advice%20from%20%40coderabbitai!%20%F0%9F%9B%A0%EF%B8%8F%20Always%20check%20lengths%20before%20indexing%20to%20avoid%20out-of-range%20errors.%20A%20simple%20fix%20can%20prevent%20unexpected%20panics%20in%20your%20code.%20Give%20it%20a%20try!&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcaminogo%2Fpull%2F404%23discussion_r2000761233)?
							Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-18T11:00:59Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-02-10T11:21:34Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Summary by CodeRabbit

- **New Features**
  - Introduced functionality that enables users to unlock previously deposited funds using transaction identifiers.
  - Added new tests for multisig alias creation and formatting to ensure expected functionality.
  - Enhanced configuration management with the addition of a new DPhaseTime parameter and related methods.

- **Bug Fixes**
  - Removed outdated methods related to unlocking deposits, streamlining the transaction handling process.
  - Updated error handling to reflect new phase terminology in proposal verification.

- **Tests**
  - Removed tests related to the deprecated unlock deposit functionality, affecting overall test coverage.
  - Updated existing tests to align with new phase definitions and error messages.
## Walkthrough
This PR introduces an undeposit functionality to the platform. It adds a new `Undeposit` method and related types in the CaminoService to unlock deposits by transaction ID. Simultaneously, it removes legacy unlock deposit functionality from the transaction builder and its tests. Moreover, the PR updates the deposit unlocking process in the UTXO and deposit packages and modifies associated tests, expectations, and helper functions to align with the revised unlocking parameters and control flows.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `vms/platformvm/camino_service.go` | Added new method `Undeposit` and types: `Undeposit`, `UndepositArgs`, and `UndepositReply` to support unlocking deposits. |
| `vms/platformvm/txs/builder/camino_builder.go` and <br>`vms/platformvm/txs/builder/camino_builder_test.go` | Removed `NewUnlockDepositTx` method and its tests from the CaminoTxBuilder interface and implementation. |
| `vms/platformvm/utxo/camino_locked.go` and <br>`vms/platformvm/utxo/camino_locked_test.go` | Updated the `UnlockDeposit` method signature: replaced `depositTxIDs` with `amountsToUndeposit` and added `undepositToOwner`; enhanced error handling and returned owners. |
| `vms/platformvm/deposit/camino_deposit.go` | Added method `UnlockableAmountByTime` to the Deposit struct for time-based unlockable amount calculations. |
| `vms/platformvm/test/expect/camino_expect.go` | Added two functions: `VerifyMultisigOwner` and `GetDepositUnlockableAmounts` to support enhanced deposit and multisig testing. |
| `vms/platformvm/test/generate/camino_generate.go` | Updated the `InsFromUTXOs` function signature to accept a variadic parameter. |
| `vms/platformvm/txs/executor/camino_tx_executor_test.go` | Updated tests for `UnlockDepositTx` and introduced handling for `UnlockExpiredDepositTx` transactions, addressing input/output validations and deposit states. |
| `vms/platformvm/utxo/camino_helpers_test.go` | Changed context initialization: removed the `snow` package in favor of using `test.Context(t)`. |

## Sequence Diagram(s)
```mermaid
sequenceDiagram
    participant C as Client
    participant S as CaminoService
    participant U as UTXO/Deposit Handler
    participant T as Tx Builder
    C->>S: Send HTTP request with UndepositArgs
    S->>S: Log call and validate undeposit arguments
    S->>S: Retrieve keys via getFakeKeys
    S->>T: Process undeposits into a map and prepare burning amount
    T->>U: Call UnlockDeposit with amountsToUndeposit & undepositToOwner
    U-->>T: Return unlocked UTXOs and signers
    T->>S: Provide transaction inputs/outputs and details
    S->>C: Return UndepositReply with transaction results
```

## Possibly related PRs
- **chain4travel/caminogo#411**: Enhances deposit unlocking processes by modifying the `UnlockDepositTx` method, aligning with similar functionality introduced in this PR.
- **chain4travel/caminogo#409**: Adds the `UnlockExpiredDepositTx` method, reflecting related changes in deposit unlocking behavior.

## Suggested reviewers
- **Noctunus**

## Poem
> I'm a rabbit on the run, hopping through the code,  
> Unlocking deposits, easin' the load.  
> Undeposit is here with a twitch of my nose,  
> Changes so crisp, like fresh spring rose.  
> With keys and maps, our data flows,  
> Merry bytes dance as the new feature glows!  
> 🐇💻 Hop on, let’s keep the system in mode!

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=chain4travel/caminogo&utm_content=404):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: evlekht
created_at: 2025-03-18T10:48:46Z
body: @coderabbitai review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-03-18T10:48:52Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE