SIP-3: Implement, adopt and enforce Samurai Vaults

Motivation

This proposal suggests for the protocol to adopt and enforce a smart contract (specified below) that enables, facilitates and provides access to a novel feature of the Samurai protocol, called “Samurai Vault”.

The protocol and its growing ecosystem currently contains a number of features, smart contracts and utilities. However, in its current state it is lacking a feature that would enable and facilitate a deeper immersion to its long term, power users.

The “Samurai Vault” feature would effectively address the absence of such feature and need, by enabling Samurai NFT Node holders and xHNR governance token holders to lock up the ERC20 token along with the NFT (ERC 721) in the smart contract outlined below.

The “Samurai Vault” smart contracts would effectively store and retain the provided tokens and NFTs for a pre-determined period of time (also referred to as lockup period) and release an incentive upon the Vault’s maturity period (unlock time).

The incentive would comprise of a boosted xHNR governance token reward rate being dispersed at the Vault’s maturity period. The provided incentive upon maturity of the Vault would represent a higher APY reward rate than the Samurai Node NFT would produce during the same time period with no lockup in the Vault feature. The total xHNR token allocation issued for the purposes of incentivising Samurai Vaults shall be minted at the maturation of Vaults and in accordance with the reward rates outlined below - in total 22 100 000 xHNR.

Furthermore, upon depositing to Samurai’s Vault, a vexHNR token, representative of the depositor’s share in the Samurai Vault smart contract, would become instantly claimable. Similarly to the mechanics and implementations of veCRV (of Curve Finance), the vexHNR (voting escrow xHNR) would comprise of a non-transferrable, non-tradeable token, representing an extended weighting in governance voting of 1.5. The vexHNR token would only be usable until the maturity of the Samurai Vault and would be blacklisted (non-usable) right after the maturity of the specific Vault. The claimable vexHNR token allocation shall be determined by: xHNR token deposit * 0.5 * number of NFT nodes.

The vexHNR token would have no other utility besides enhanced participation in the protocol’s governance and the only manner in which the vexHNR token can be attained is by contributing to the Samurai Vault smart contract, by locking up Samurai NFT Nodes and the xHNR governance token.

The introduction of Samurai Vault would thus ultimately cater to the protocol’s power users with long-term prospects that currently have limited ability to immerse themselves to the protocol and its governance in such manner, while effectively aligning incentives.

To ensure alignment with the above statement, the participation in Samurai’s Vault would contain the following parametric pre-requisites:

A quantity of 100 Samurai NFT Nodes must be deposited (of any tier) AND A quantity of 100 000 xHNR governance tokens must be deposited (These parameters are not mutually exclusive and must be deposited simultaneously to successfully attain a Samurai Vault)

In accordance with the block speed, the lockup period of the Vault is estimated to be approximately 72 months (locked for 170 000 000 blocks).

The total number of Samurai Vault’s would be finite and capped at 1300 nodes + 1 300 000 xHNR

In the event of the governance vote being in favour of this proposal, the Samurai Vault functionality and smart contracts shall be deployed at the “End date” of this snapshot.

Specification

The Samurai Vault is an immutable contract that contains the primary functions to Lock and Unlock a combination of NFT nodes and xHNR.

Upon deploying the contract, the following parameters of interest are set:

Target Block Number Boosted Reward Rate Minimum Deposit Amount Minimum Nodes Amount Max Nodes

The Target Block Number denotes the ending block number at which the contract will automatically enable the vault depositors to unlock their Nodes and xHNR along with a lump sum reward, which is calculated using the Boosted Reward Rate metric. This metric is calculated using the following equation:

(Target Block Number - Current Block Number) * Boosted Reward Rate = Unlock Reward

The Minimum Deposit Amount denotes the required amount of xHNR which the depositor must meet to be partly eligible. The Minimum Nodes Amount is the required amount of NFT nodes, which the depositor must meet, along with the previous requirements, to complete their eligibility.

The Max Nodes parameter denotes the size of the Contract Vault, which will only be open for a specified size of depositors’ nodes.

It is important to note, that the Boosted Reward Rate and the Target Block Number CANNOT be altered through any set of setters nor proxy contracts. Thus, the Samurai Vault is truly an immutable piece of bytecode with no alternative way of changing these two constant values. The overall architecture of the contract has also been simplified and refactored to increase the code readability and to further ensure that the business logic remains simple enough to not introduce any complex vulnerabilities.

Finally, the Lock function, once the depositor approves the contract twice via the xHNR and Honour Nodes contracts, will transfer the Minimum Required Amount of Nodes and xHNR from the depositor’s wallet to the contract address. The Lock function remains available for anyone to use for as long as the depositor meets the eligibility requirements and the Max Nodes parameter is not fulfilled.

The Unlock function is guarded by the Target Block Number parameter and thus, can only be used once the current block number passes this stated target. Upon the invocation of this function, the depositor’s originally deposited NFTs are transferred back along with the original deposit amount and the unlocked reward amount in xHNR.

Warning:

The deposited NFT nodes and xHNR using the Lock function WILL NOT be in the depositor’s possession as their location will be placed into the storage of the Samurai Vault contract. As such, the depositor loses the ability to use their xHNR for other Samurai related utility features such as Samurai AI. Furthermore, the NFT nodes cannot be used to claim rewards for the entire duration that they are locked. However, rewards accumulation on the deposited nodes will continue as the feature is independent of a holder wallet.

The above mentioned reasons are partly the reason why the Lock mechanism requires both, the NFT nodes and xHNR. This requirement forces the potential depositor to approve 2 contracts instead of 1 single contract, which tends to be the norm. The dual approval mechanism prevents users from accidental usage of the Lock function as the locking mechanism does not allow anyone to prematurely unlock their NFT nodes or deposited xHNR.

The proposed smart contract/Solidity code:

SamuraiVaults:

// SPDX-License-Identifier: MIT pragma solidity 0.8.13;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/utils/math/SafeMath.sol"; import "@openzeppelin/contracts/interfaces/IERC721.sol"; import "@openzeppelin/contracts/interfaces/IERC20.sol";

interface IvexHnr { function mint(uint256 amount, address depositor) external; }

contract SamuraiVaults is Ownable, ReentrancyGuard { uint256 public depositAmount; uint256 public minimumNodes; uint256 public targetBlockNumber; uint256 public maxNodes; uint256 public depositedNodes; uint256 public boostedRewardRate; uint256 public vexHnrAmount;

IERC20 public xHnr; IERC721 public hnrNodes; IvexHnr public vexHnr;

struct Vault { uint256[] nodeIds; uint256 depositAmount; uint256 lockedAtBlockNumber; uint256 unlockReward; // this is just a flag for a require statement check bool isValid; bool isClaimed; }

mapping(address => Vault) public depositors;

using SafeMath for uint256;

constructor( address _xHnr, address _hnrNodes, address _vexHnr, uint256 _baseDeposit, uint256 _baseRewardRate, uint256 _maxNodes, uint256 _minimumNodes, uint256 _vexHnrAmount ) { xHnr = IERC20(_xHnr); hnrNodes = IERC721(_hnrNodes); vexHnr = IvexHnr(_vexHnr);

uint256 pow = 10**18;
// amount of xHNR that must be deposited
depositAmount = _baseDeposit.mul(pow);
// reward rate for each passing block
boostedRewardRate = _baseRewardRate.mul(pow);
// amount of minimum nodes which must be locked
minimumNodes = _minimumNodes;
// amount of maximum nodes which can be deposited
maxNodes = _maxNodes;
// amount of vexHNR
vexHnrAmount = _vexHnrAmount;
// this is roughly 6 years
targetBlockNumber = block.number + 170_000_000;
depositedNodes = 0;

}

modifier ownsAll(uint256[] calldata _tokenIds, bool isContractOwner) { uint256 arrSize = _tokenIds.length;

address tokenOwner = isContractOwner ? address(this) : msg.sender;
for (uint256 i = 0; i < arrSize; i = uncheckedIncrement(i)) {
  require(
    hnrNodes.ownerOf(_tokenIds[i]) == tokenOwner,
    isContractOwner
      ? "Contract: token ID unavailable"
      : "Owner: not an owner!"
  );
}
_;

}

function lock(uint256[] calldata _tokenIds) external nonReentrant ownsAll(_tokenIds, false) { // add to struct require( depositedNodes + minimumNodes <= maxNodes, "Contract: Max Vaults reached!" ); require( depositAmount <= xHnr.balanceOf(msg.sender), "Contract: Not enough funds!" ); require(_tokenIds.length == minimumNodes, "Contract: Not enough nodes!"); // could run out of gas fees if not true Vault memory senderVault = depositors[msg.sender]; require(senderVault.isValid == false, "Contract: Wallet already locked!");

batchTransfer(_tokenIds, true);
xHnr.transferFrom(msg.sender, address(this), depositAmount);
uint256 lockedAt = block.number;
uint256 unlockReward = (targetBlockNumber - lockedAt) * boostedRewardRate;

depositors[msg.sender] = Vault(
  _tokenIds,
  depositAmount,
  lockedAt,
  unlockReward,
  true,
  false
);
// increment the node count
depositedNodes += minimumNodes;

}

function unlock() external nonReentrant { require(targetBlockNumber < block.number, "Contract: Cannot be unlocked!");

Vault storage senderVault = depositors[msg.sender];

require(senderVault.isValid, "Contract: No Vault!");
// block future claiming
senderVault.isValid = false;

batchTransfer(senderVault.nodeIds, false);
xHnr.transfer(
  msg.sender,
  senderVault.unlockReward + senderVault.depositAmount
);

}

function claim() external nonReentrant { Vault storage senderVault = depositors[msg.sender]; require(senderVault.isValid, "Contract: Not a depositor!"); require(senderVault.isClaimed == false, "Contract: Already claimed!");

senderVault.isClaimed = true;
vexHnr.mint(vexHnrAmount * 10**18, msg.sender);

}

function remainingBlocks() external view returns (uint256) { return targetBlockNumber - block.number; }

function getVaultNodeCount() external view returns (uint256) { return depositedNodes; }

function batchTransfer(uint256[] memory _tokenIds, bool isLock) internal { uint256 length = _tokenIds.length; address sender = msg.sender; address contractAddress = address(this);

for (uint256 i = 0; i < length; i = uncheckedIncrement(i)) {
  isLock
    ? hnrNodes.transferFrom(sender, contractAddress, _tokenIds[i])
    : hnrNodes.transferFrom(contractAddress, sender, _tokenIds[i]);
}

}

// gas optimisation function uncheckedIncrement(uint256 i) internal pure returns (uint256) { unchecked { return i + 1; } } }

———

vexHNR:

// SPDX-License-Identifier: MIT pragma solidity 0.8.13;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol";

contract vexHNR is ERC20, Ownable { address public vault;

constructor() ERC20("vexHonour", "vexHNR") { // do nothing }

modifier onlyVault() { require(msg.sender == vault, "Contract: Not Vault"); _; }

function mint(uint256 amount, address depositor) external onlyVault { _mint(depositor, amount); }

function _beforeTokenTransfer( address from, address to, uint256 amount ) internal virtual override { super._beforeTokenTransfer(from, to, amount); // only allow the vault to transfer if (from != address(0)) { require(from == vault, "Contract: Not Vault"); } }

function setVault(address _vault) external onlyOwner { vault = _vault; } }

Last updated