Tenbin is an asset tokenization protocol which uses futures contracts to enable highly liquid assets. Read the full docs here: DOCS
Tenbin uses a continuous auditing policy with a target of one audit cycle per quarter. Two rounds of audits have been conducted on the tenbin contracts codebase. The first on 1/22/26 and the second on 4/12/26. Notable auditors include Spearbit, Zellic, and Fuzzland.
The auditing firms zellic and fuzzland performed a full protocol audit. Additionally, AI audits were run using claude code and cantina's AI auditor tool.
Four smart contract audits were performed on the solidity codebase. An initial independent audit was conducted, followed by three major audits by Spearbit, Fuzzland, and Verilog. The scope was initially created based on a monorepo, then moved to a public repository at https://github.com/tenbinlabs/contracts.
Ensure rust is installed: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Ensure rust is up-to-date: rustup update
Ensure foundry is installed: curl -L https://foundry.paradigm.xyz | bash
Ensure foundry is up-to-date: foundryup
Install dependencies: forge install
Build contracts: forge build
Create .env file and fill in variables: cp .env.example .env
Set permissions for .env: chmod 600 .env
Build production contracts:
FOUNDRY_PROFILE=production forge build
forge test
forge test --skip test/fork/*
forge test --match-path "test/invariant/*"
FOUNDRY_PROFILE=coverage forge coverage
View coverage with uncovered branches and lines:
FOUNDRY_PROFILE=coverage forge coverage --report debug
forge test --gas-report
forge doc
./sync_foundry_deps.sh
repair_foundry_deps.sh
When updating dependencies, ensure to add/update tag names in foundry.lock and repair_foundry_deps.sh.
Install echidna: https://github.com/crytic/echidna?tab=readme-ov-file#installation
Run all echidna tests: ./echidna.sh
echidna test/echidna/<contract-file-name>.sol --contract <contract-name> --config echidna.yaml"
Install Certora: https://docs.certora.com/en/latest/docs/user-guide/install.html#installation
Ensure CERTORAKEY is set in .env
Run certora: certoraRun contractFile:contractName --verify contractName:specFile
Install slither: https://github.com/crytic/slither?tab=readme-ov-file#how-to-install
Run slither: slither .
Install cyfrin: https://github.com/Cyfrin/up
Run forge build && aderyn and review report.md.
Install mythril: https://mythril-classic.readthedocs.io/en/develop/installation.html
Run mythril: myth analyze {your_contract}
Configuration: https://getfoundry.sh/config/static-analyzers/#mythril
Use config/ to configure roles and parameters when running deploy scripts. Roles and existing deployments are tracked in deployments.json. When running the deployment script, a file is created in broadcast/{chainid}/{script_name}/contracts.json containing the recently deployed contracts and roles.
-
Ensure BROADCASTER_KEY and BROADCASTER_ADDRESS is NOT set in .env
-
Run anvil:
anvil --mnemonic $TEST_MNEMONIC -
Run
FOUNDRY_PROFILE=production forge script script/DeployDevelopmentMock.s.sol --rpc-url ws://localhost:8545 --broadcast
-
Ensure BROADCASTER_KEY and BROADCASTER_ADDRESS is set in .env
-
Run FOUNDRY_PROFILE=production forge script script/DeployVault.s.sol --rpc-url $SEPOLIA_RPC_URL --broadcast
- Ensure
BROADCASTER_KEY,BROADCASTER_ADDRESSandETHERSCAN_API_KEYare correctly set up.
Run FOUNDRY_PROFILE=production forge script script/DeployDevelopmentMock.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $BROADCASTER_KEY --verify --verifier etherscan --verifier-api-key $ETHERSCAN_API_KEY --slow
Use --broadcast to broadcast
-
Ensure
CONFIG_DIR,BROADCASTER_KEY,BROADCASTER_ADDRESSandETHERSCAN_API_KEYare correctly set up. -
Run
FOUNDRY_PROFILE=production forge script script/DeployProduction.s.sol $CONFIG_DIR --rpc-url $MAINNET_RPC_URL --private-key $BROADCASTER_KEY --verify --verifier etherscan --verifier-api-key $ETHERSCAN_API_KEY --slow
Use --broadcast to broadcast.
Note: To create a mock version, i.e. use the current development on mainnet, use the following command instead:
FOUNDRY_PROFILE=production forge script script/DeployDevelopment.s.sol --rpc-url $MAINNET_RPC_URL --private-key $BROADCASTER_KEY --verifier etherscan --verifier-api-key $ETHERSCAN_API_KEY --slow
- Ensure
COLLATERAL_ADDRESS,CONTROLLER_ADDRESS,MINTER_ADDRESS,MINTER_KEY, andSIGNER_KEYare set in.env - Ensure scripts/MintTestnet.s.sol has the correct addresses set as constants
- Run
source .env - Ensure approval is granted from payer key
cast send $COLLATERAL_ADDRESS "approve(address,uint256)" $CONTROLLER_ADDRESS 1000000000000000000000000000000 --rpc-url $SEPOLIA_RPC_URL --private-key $SIGNER_KEY - Run the mint script
forge script script/MintTestnet.s.sol --rpc-url $SEPOLIA_RPC_URL --private-key $MINTER_KEY --broadcastTHIS SCRIPT IS NOT SAFE TO RUN ON MAINNET!!
Certain contracts support a restricted addresses registry to prevent sanctioned accounts from interacting with them. Only an account with the RESTRICTER_ROLE can call their respective setIsRestricted method to restrict a specific account, but for cases when we need to create a batch of addresses to restrict, or remove restricted accounts, in a single transaction we can use the BuildSafeBatch script as follows:
-
Ensure
CSV_ADDRESSESis set in .env, or the execution environment, with the addresses to update. -
Run the following command:
forge script script/BuildSafeBatch.s.sol:BuildSafeBatch --sig "run(address,bool,string)" <target_contract_address> <bool_status> $CSV_ADDRESSES
Tenbin is an asset token issuance platform with the goal of creating liquid, composable financial assets. Assets in the Tenbin protocol are backed by two positions: off-chain futures contracts and on-chain collateral. The off-chain hedging system maintains a delta one exposure of an underlying asset. The on-chain collateral is used to earn low-risk yield. So long as the on-chain yield equals or exceeds the off-chain funding costs, the protocol is able to peg Tenbin assets to the spot price of the real asset.
Each asset in the Tenbin protocol has a set of contracts unique to that asset. Key contracts include the AssetToken, Controller, StakedAsset, and CollateralManager. This document is an outline of the smart contract system used to facilitate mints, redemptions, staking, and on-chain collateral management.
An asset token represents an asset in the Tenbin protocol. The AssetToken contract is an immutable, non-upgradeable ERC20 token with an extension to allow minting and burning. There is an owner and minter role. The owner is a multisig that can set the minter account, and the minter can mint new tokens. In all cases, the minter will be set as the Controller contract.
The controller contract is responsible for minting and redeeming assets. Mint and redemptions are encoded as an Order. Orders are signed by KYC-approved signers and specify order details such as collateral amount, asset amount, and deadline. All orders must include a context signed by a minter key stored in a hardware security module and controlled by the Tenbin backend. Orders are executed either by a minter key or an approved signer. Orders are executed atomically: collateral is transferred and tokens are minted/burned in a single transaction.
struct Order {
OrderType order_type; // Order type (MINT or REDEEM)
uint256 nonce; // Payer unique nonce
uint256 expiry; // Order expiration timestamp
address payer; // Account to transfer tokens from
address recipient; // Account to receive tokens
address collateral_token; // Collateral used for this order
uint256 collateral_amount; // Amount of collateral tokens
address order_token; // Specify AssetToken or StakedAsset
uint256 asset_amount; // Amount of asset tokens
}
- Order signer goes through KYC and is added to the allowed signers list.
- An order signer submits an
orderand signed ordersignatureto the back end - The back end processes the order and provides a
contextand signed contextapproval - Token are transferred accordingly and payer nonce is marked as used.
The controller keeps track of what accounts are allowed to sign orders. An account must be on the allowed signer list in order to submit a successful order. Note that the minter account can never modify the contents of an order, and that an order can only ever be executed once by specifying a unique nonce for every order. The controller supports both EIP712 and EIP1271 signatures.
Signers can set which accounts can receive tokens when an order is executed. This control prevents signers from sending tokens to an incorrect account. Signers can manage recipients on-chain by calling setRecipientStatus(). By default, a signer is approved to be a recipient for its own orders.
Any account can delegate a signer to sign orders on its behalf. During execution of an order, the payer is checked against the delegates for a signer. This allows for EOAs or smart contracts to let a signer sign an order where the delegate pays for the order. An account can add a delegate signer by calling setDelegateStatus().
As long as an order is valid with a valid signature, context, and approval, any account can execute an order. All orders need an approval signed by the MINTER_ROLE to be valid. This design allows for users to execute their own orders rather then relying on a dedicated account to execute orders.
The controller has a collateral ratio which specifies the percentage of collateral which is sent to custodian and manager. The custodian amount represents the portion of collateral which is moved off-chain to fund futures hedge positions. The manager amount represents the portion of collateral designated to earn on-chain yield. When a mint occurs, the total collateral amount is split according to theratio value set in the Controller.
The custodian module receives collateral during a mint order. Custodians are added to a list of approved accounts. Only approved custodians can receive collateral from a mint event. A keeper role is assigned to interact with the custodian module and transfer collateral accordingly.
The controller has a configurable oracle adapter which can provide a price when executing orders. When enabled, the oracle price acts as a backstop to prevent order pricing from exceeding a threshold. The oracle DOES NOT determine the price of assets, rather it acts as a security measure to prevent minting and redeeming assets at a price off-peg. For example, with the oracle configured it is impossible to mint new assets with a price of $0.
The Context struct allows passing in a flag to indicate an order should be curated as part of the transaction. When performing an on demand curation, the CollateralManager withdraw() or deposit() function is called before a redemption or after a mint, respectively. An additional share_price is passed along with curated orders to act as a slippage guard when interacting with the CollateralManager vaults.
Mint and redemption limits are configurable per block. If set to type(uint).max, limits are not enforced.
Staked assets can be redeemed directly through the controller by specifying the staked asset address as order_token in the Order struct. Redeeming staked assets also requires the payer approves the controller to spend staked assets on its behalf. During staked asset redemption, assets are instantly unstaked by the controller and pricing is determined by the back end pricing engine.
Note: the asset_amount specified in order.asset_amount refers to the asset token amount burned, and is used for pricing. The actual number of shares redeemed is determined by the staking contract via previewWithdraw(asset_amount)
The controller uses a restricted registry of accounts which cannot interact with the controller. During order execution, the payer and recipient are checked against this registry.
The manager contract stores collateral to earn on-chain yield and provide liquidity for redemptions. When executing a mint order, collateral is transferred directly to the CollateralManager. The manager is non-custodial: there is no way to withdraw protocol collateral from the manager except during execution of a redemption order or a rebalancing action. For each collateral there is an associated ERC4626 vault, typically a Morpho V2 vault, used to earn yield. The manager uses accounting to separate collateral from revenue.
The main actions that can be performed by the collateral manager are:
- Deposit collateral into vaults
- Withdraw collateral from vaults
- Swap collateral tokens
- Withdraw revenue
- Rebalance collateral between on/off chain
The two main functions provided by the collateral manager are deposit()and withdraw(). Each collateral has a unique vault dedicated to earning yield for that collateral type. Depositing will deposit collateral into the vault in exchange for vault shares. Withdrawing will redeem vault shares in exchange for the underlying collateral. Deposit and withdraw functions include a share price which acts as a slippage guard when performing curations.
The most important role of the manager is to ensure there is sufficient liquidity to redeem assets. Collateral that is in the collateral manager is approved to be transferred by the controller when a redemption order is filled. If there is insufficient collateral in the manager, a redemption is not possible.
In order for Tenbin to optimize yield versus liquidity, a curator role is assigned by the manager to perform deposits and withdrawals. By using a multicall contract, it is possible to bundle deposits and withdrawals with mints and redemptions atomically. For example: [mint(), deposit()] or [withdraw(), redeem()].
Additionally, the Controller contract has the ability of performing curations as part of a mint or redemption. This allows for users to execute their own orders where the controller performs curation actions. This allows for the maximum amount of yield to be earned by the protocol while maintaining flexibility for users. Curation options for controller-curated orders are limited to deposit() and withdraw().
Rebalancing occurs when there is a surplus or deficit of on-chain vs off-chain collateral. If the off-chain position is over-collateralized, collateral is moved on-chain through a custodian account and transferred directly to the Collateral Manager. If the off-chain position is under-collateralized, the rebalance() function can be called by a permissioned role to withdraw collateral from the manager.
In order to limit the amount of collateral that can be withdrawn during a rebalance, there is a cap set on the amounts that can be withdrawn during this function. Additionally, the rebalancer withdraw can only withdraw to a registered set of custodians in the CustodianModule. This design guarantees that protocol collateral is always in custody and cannot be directly controlled by protocol operators.
The RevenueModule manages revenue earned by the protocol. In most cases, revenue is transferred back to the collateral manager in order to pay for the off-chain hedging costs.
The revenue module can perform the following actions:
- Withdraw revenue from the collateral manager
- Transfer revenue back to the collateral manager
- Transfer revenue to a multisig contract
- Provide liquidity to mint new asset tokens as a reward
- Reward the staking pool with asset tokens
A keeper role is assigned by the revenue module to automate these tasks. For example, the keeper might be called 2x per day to transfer revenue back to the collateral manager, and 1x per day to reward the staking contract.
The swap module is used to perform on-chain swaps between collateral types. The goal of this module is to strictly restrict what types of swaps are possible and limit the slippage between different collaterals. Each swap performed in the CollateralManager passes in a set of constraints for that swap plus the call data to perform the swap. In addition, there are configurable slippage limits in the contract storage for swaps between specific collaterals. This security measure limits the possibility of performing a poorly priced swap. Additionally, there is a swap capacity for each token. The swap cap prevents the curator from swapping an excessive amount of collateral (for instance, human or backend error)
It is possible to bundle swaps as part of a multicall. For example, if there is insufficient USDT in the manager and a redemption order is placed requesting USDT, the following bundle can be created: [swap(), redeem()].
The manager is a UUPS upgradeable smart contract. The intention of upgradeability is to support new on-chain yield structures in the future. In the case the design is considered stable and immutability is desired, the upgrade feature can be permanently disabled.
The staking contract allows accounts to stake asset tokens in exchange for a staking token. If the protocol is profitable, revenue can be used to mint new asset tokens and reward them to the staking pool. Staking allows for the creation of compounding, yield-bearing assets in the Tenbin protocol. The staking contract is implemented as a custom ERC4626 vault.
Locking assets in the staking contract has the added benefit of enabling advanced yield strategies in the manager vaults. For example, if it is known that 20% of assets are locked in the staking pool for 7 days, the portion of collateral backing those assets can be committed to locked yield strategies such as sUSDe. Additionally, understanding how much of an asset is locked allows more efficient liquidity management in the manager.
The asset value of a staking token can only increase over time. When unstaking, the staking token is burned in exchange for the original amount deposited, plus a share of any rewards earned since staking.
A vesting period is used for rewards in order to prevent abuse of the staking contract. The vesting period encourages depositors to remain staked through the vesting period. This mechanism prevents negative MEV actions such as sandwiching reward transactions. Once the vesting period is set, it cannot be set below MIN_VESTING_PERIOD.
Vesting spreads out rewards over a longer period of time in order to reduce reward volatility. For some assets, minting reward tokens is not always possible due to futures off-market hours. Using a longer vesting period allows stakers to earn a consistent yield despite possibly inconsistent reward schedules and amounts.
note Rewarding the contract excessively and with low reward amounts can cause vesting to reset and extend currently vesting rewards. Rewards should be distributed infrequently (once per 1-3 days) and in consistent amounts to ensure smooth vesting.
A cooldown period is present in the staking contract to encourage assets to remain staked and allow response time for liquidity management. When a staker calls the cooldownShares() or cooldownAssets() function, staked tokens are burned and transferred to the AssetSilo contract. An account can have multiple cooldowns at once as determined by a unique set of IDs per account. After the cooldown end time has passed, a user can call unstake(id) to withdraw the underlying asset tokens.
In order to withdraw tokens, a staker needs to call cooldown() and wait until the cooldown period has passed to withdraw their stake. A cooldown can be cancelled by calling cancelCooldown(id). When cancelling a cooldown, new shares are minted via the AssetSilo contract and transferred to the account which cancelled the cooldown.
Instant unstaking is possible via a privileged role. Typically, the Controller is allowed to instant unstake when executing redemptions for staked assets. There is an instantUnstakeCap which sets a limit on the total amount that can be instantly unstaked. The instant unstake function takes in an asset amount and calculates how many shares to redeem via previewWithdraw(assets).
Due to legal restrictions, yield cannot be paid to stakers without regulatory compliance. For this reason, a restricted registry is present in the staking contract. Accounts added to this registry cannot stake, unstake, or transfer staked tokens. If an account is restricted, the contract default admin can burn the account’s staking tokens and withdraw the underlying assets.
The manager is a UUPS upgradeable smart contract. The intention of upgradeability is to support new staking models in the future. In the case the design is considered stable and immutability is desired, the upgrade feature can be permanently disabled.